diff --git a/backend/migrations/022_fix_json_columns.js b/backend/migrations/022_fix_json_columns.js new file mode 100644 index 0000000..752b02c --- /dev/null +++ b/backend/migrations/022_fix_json_columns.js @@ -0,0 +1,68 @@ +exports.up = async function(knex) { + console.log('Fixing JSON columns in database...'); + + // Fix email_templates variables column + const templates = await knex('email_templates').select('id', 'template_key', 'variables'); + + for (const template of templates) { + if (template.variables && typeof template.variables === 'string') { + try { + // Check if it's already valid JSON + JSON.parse(template.variables); + } catch (e) { + console.log(`Fixing invalid JSON in email template ${template.template_key}`); + // Attempt to fix common issues + let fixed = template.variables; + + // If it looks like an array but isn't valid JSON, try to fix it + if (fixed.startsWith('[') && fixed.endsWith(']')) { + // Extract the content and properly format it + const content = fixed.slice(1, -1); + const items = content.split(',').map(item => item.trim().replace(/['"]/g, '')); + fixed = JSON.stringify(items); + } else { + // Default to empty array if we can't fix it + fixed = JSON.stringify([]); + } + + await knex('email_templates') + .where('id', template.id) + .update({ variables: fixed }); + } + } else if (!template.variables) { + // Set default empty array for null values + await knex('email_templates') + .where('id', template.id) + .update({ variables: JSON.stringify([]) }); + } + } + + // Fix activity_logs metadata column + const activities = await knex('activity_logs').select('id', 'metadata'); + + for (const activity of activities) { + if (activity.metadata && typeof activity.metadata === 'string') { + try { + // Check if it's already valid JSON + JSON.parse(activity.metadata); + } catch (e) { + console.log(`Fixing invalid JSON in activity log ${activity.id}`); + // Default to empty object if we can't parse it + await knex('activity_logs') + .where('id', activity.id) + .update({ metadata: JSON.stringify({}) }); + } + } else if (!activity.metadata) { + // Set default empty object for null values + await knex('activity_logs') + .where('id', activity.id) + .update({ metadata: JSON.stringify({}) }); + } + } + + console.log('JSON columns fixed successfully'); +}; + +exports.down = async function(knex) { + // No rollback needed - data fixes only +}; \ No newline at end of file diff --git a/backend/scripts/fix-production-issues.js b/backend/scripts/fix-production-issues.js new file mode 100644 index 0000000..5fc0c13 --- /dev/null +++ b/backend/scripts/fix-production-issues.js @@ -0,0 +1,145 @@ +require('dotenv').config(); +const { db } = require('../src/database/db'); + +async function fixProductionIssues() { + console.log('Fixing production database issues...\n'); + + try { + // 1. Check and fix email_templates structure + console.log('1. Checking email_templates structure:'); + const emailColumns = await db('email_templates').columnInfo(); + console.log('Current columns:', Object.keys(emailColumns)); + + // Check if we need to add basic columns back + const hasSubject = 'subject' in emailColumns; + const hasSubjectEn = 'subject_en' in emailColumns; + + if (hasSubjectEn && !hasSubject) { + console.log('Adding basic columns back to email_templates...'); + await db.schema.alterTable('email_templates', (table) => { + table.string('subject'); + table.text('body_html'); + table.text('body_text'); + }); + + // Copy values from _en columns + await db('email_templates').update({ + subject: db.raw('subject_en'), + body_html: db.raw('body_html_en'), + body_text: db.raw('body_text_en') + }); + console.log('Basic columns added successfully'); + } + + // 2. Ensure default templates exist + console.log('\n2. Checking email templates:'); + const templateCount = await db('email_templates').count('* as count'); + console.log('Template count:', templateCount[0].count); + + if (templateCount[0].count === 0) { + console.log('No templates found, inserting defaults...'); + const defaultTemplates = [ + { + template_key: 'gallery_created', + subject: 'Your Photo Gallery is Ready!', + body_html: '

Gallery Created Successfully

...', + body_text: 'Gallery Created Successfully...', + variables: JSON.stringify(['host_name', 'event_name', 'event_date', 'gallery_link', 'gallery_password', 'expiry_date']) + }, + { + template_key: 'expiration_warning', + subject: 'Your Photo Gallery Expires Soon', + body_html: '

Gallery Expiring Soon

...', + body_text: 'Gallery Expiring Soon...', + variables: JSON.stringify(['host_name', 'event_name', 'days_remaining', 'gallery_link']) + }, + { + template_key: 'gallery_expired', + subject: 'Your Photo Gallery Has Expired', + body_html: '

Gallery Expired

...', + body_text: 'Gallery Expired...', + variables: JSON.stringify(['host_name', 'event_name']) + }, + { + template_key: 'archive_complete', + subject: 'Gallery Archive Complete', + body_html: '

Archive Complete

...', + body_text: 'Archive Complete...', + variables: JSON.stringify(['host_name', 'event_name', 'archive_size']) + } + ]; + + for (const template of defaultTemplates) { + // Add language columns if they exist + if (hasSubjectEn) { + template.subject_en = template.subject; + template.body_html_en = template.body_html; + template.body_text_en = template.body_text; + template.subject_de = template.subject; + template.body_html_de = template.body_html; + template.body_text_de = template.body_text; + } + + await db('email_templates').insert(template); + } + console.log('Default templates inserted'); + } + + // 3. Check activity_logs structure + console.log('\n3. Checking activity_logs structure:'); + const activityColumns = await db('activity_logs').columnInfo(); + console.log('Columns:', Object.keys(activityColumns)); + + // Check if read_at exists + if (!('read_at' in activityColumns)) { + console.log('Adding read_at column to activity_logs...'); + await db.schema.alterTable('activity_logs', (table) => { + table.datetime('read_at').nullable(); + }); + console.log('read_at column added'); + } + + // 4. Check and add CMS pages + console.log('\n4. Checking CMS pages:'); + const cmsColumns = await db('cms_pages').columnInfo(); + console.log('CMS columns:', Object.keys(cmsColumns)); + + const impressum = await db('cms_pages').where('slug', 'impressum').first(); + const datenschutz = await db('cms_pages').where('slug', 'datenschutz').first(); + + if (!impressum) { + console.log('Adding Impressum page...'); + await db('cms_pages').insert({ + slug: 'impressum', + title_en: 'Legal Notice', + title_de: 'Impressum', + content_en: '

Legal Notice

Your legal information here...

', + content_de: '

Impressum

Ihre rechtlichen Informationen hier...

', + updated_at: new Date() + }); + } + + if (!datenschutz) { + console.log('Adding Datenschutz page...'); + await db('cms_pages').insert({ + slug: 'datenschutz', + title_en: 'Privacy Policy', + title_de: 'Datenschutzerklärung', + content_en: '

Privacy Policy

Your privacy policy here...

', + content_de: '

Datenschutzerklärung

Ihre Datenschutzerklärung hier...

', + updated_at: new Date() + }); + } + + console.log('\n✅ All fixes applied successfully!'); + + } catch (error) { + console.error('Error fixing issues:', error); + console.error('Stack:', error.stack); + } finally { + await db.destroy(); + process.exit(0); + } +} + +fixProductionIssues(); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index f6c35c6..a22fe2a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,6 +28,9 @@ const adminAuthRoutes = require('./src/routes/adminAuth'); const app = express(); const PORT = process.env.PORT || 3000; +// Trust proxy headers (required for Traefik/nginx) +app.set('trust proxy', true); + // Security middleware with custom CSP app.use(helmet({ contentSecurityPolicy: { diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index 615dade..7fe7ed7 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -164,7 +164,16 @@ router.get('/templates', adminAuth, async (req, res) => { const result = { id: template.id, template_key: template.template_key, - variables: template.variables ? JSON.parse(template.variables) : [], + variables: (() => { + try { + if (!template.variables) return []; + if (typeof template.variables === 'object') return template.variables; + return JSON.parse(template.variables); + } catch (e) { + console.warn('Failed to parse variables for template:', template.template_key, e.message); + return []; + } + })(), updated_at: template.updated_at }; @@ -212,7 +221,16 @@ router.get('/templates/:key', adminAuth, async (req, res) => { const response = { id: template.id, template_key: template.template_key, - variables: template.variables ? JSON.parse(template.variables) : [], + variables: (() => { + try { + if (!template.variables) return []; + if (typeof template.variables === 'object') return template.variables; + return JSON.parse(template.variables); + } catch (e) { + console.warn('Failed to parse variables for template:', template.template_key, e.message); + return []; + } + })(), updated_at: template.updated_at }; diff --git a/backend/src/routes/adminNotifications.js b/backend/src/routes/adminNotifications.js index 3732da6..2f3ea2e 100644 --- a/backend/src/routes/adminNotifications.js +++ b/backend/src/routes/adminNotifications.js @@ -32,7 +32,16 @@ router.get('/', adminAuth, async (req, res) => { actorName: notification.actor_name, eventName: notification.event_name, eventId: notification.event_id, - metadata: notification.metadata ? JSON.parse(notification.metadata) : {}, + metadata: (() => { + try { + if (!notification.metadata) return {}; + if (typeof notification.metadata === 'object') return notification.metadata; + return JSON.parse(notification.metadata); + } catch (e) { + console.warn('Failed to parse metadata for notification:', notification.id, e.message); + return {}; + } + })(), createdAt: notification.created_at, readAt: notification.read_at, isRead: !!notification.read_at