From 4e2075c638ddc801cef250a9e1226c1a630b31ca Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 16 Jul 2025 23:13:16 +0200 Subject: [PATCH] fix: prevent double date formatting in email templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove pre-formatting of dates before passing to email processor - Let email processor handle all date formatting based on recipient language - Fix Invalid Date errors by passing raw date values instead of formatted strings - Remove unused formatDate imports and variables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/config/validateEnv.js | 2 +- backend/src/database/db.js | 6 +-- backend/src/routes/adminArchives.js | 2 +- backend/src/routes/adminEmail.js | 18 ++++----- backend/src/routes/adminEvents.js | 21 +++++------ backend/src/routes/events.js | 4 +- backend/src/services/watermarkService.js | 48 ++++++++++++------------ backend/src/utils/sqlSecurity.js | 2 +- 8 files changed, 50 insertions(+), 53 deletions(-) diff --git a/backend/src/config/validateEnv.js b/backend/src/config/validateEnv.js index f2ffae9..90789c3 100644 --- a/backend/src/config/validateEnv.js +++ b/backend/src/config/validateEnv.js @@ -34,7 +34,7 @@ function validateEnvironment() { if (name === 'JWT_SECRET' && value) { // Check for the insecure default value if (value === 'your-secret-key') { - errors.push(`CRITICAL: JWT_SECRET is set to the insecure default value. Please set a secure secret key.`); + errors.push('CRITICAL: JWT_SECRET is set to the insecure default value. Please set a secure secret key.'); } // Check minimum length (should be at least 32 characters for security) diff --git a/backend/src/database/db.js b/backend/src/database/db.js index 1f50866..5eacda8 100644 --- a/backend/src/database/db.js +++ b/backend/src/database/db.js @@ -59,9 +59,9 @@ async function initializeDatabase() { ) `); - await db.raw(`INSERT INTO events_new SELECT * FROM events`); - await db.raw(`DROP TABLE events`); - await db.raw(`ALTER TABLE events_new RENAME TO events`); + await db.raw('INSERT INTO events_new SELECT * FROM events'); + await db.raw('DROP TABLE events'); + await db.raw('ALTER TABLE events_new RENAME TO events'); } catch (error) { // If the migration fails, it might already have been applied console.log('Color theme migration may have already been applied'); diff --git a/backend/src/routes/adminArchives.js b/backend/src/routes/adminArchives.js index f2102d2..9bd4c2c 100644 --- a/backend/src/routes/adminArchives.js +++ b/backend/src/routes/adminArchives.js @@ -251,7 +251,7 @@ router.post('/:id/restore', adminAuth, async (req, res) => { } catch (statError) { console.error(`Failed to stat file: ${actualFilePath}`); console.error(`Entry name was: ${entry.entryName}`); - console.error(`Error:`, statError.message); + console.error('Error:', statError.message); // Skip this file if we can't stat it continue; } diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index fcfcd84..45d17be 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -210,15 +210,15 @@ router.get('/templates', adminAuth, async (req, res) => { id: template.id, template_key: template.template_key, 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 []; - } - })(), + 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/adminEvents.js b/backend/src/routes/adminEvents.js index d209192..4894c60 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -10,7 +10,7 @@ const path = require('path'); const { archiveEvent } = require('../services/archiveService'); const { queueEmail } = require('../services/emailProcessor'); const { escapeLikePattern } = require('../utils/sqlSecurity'); -const { formatDate } = require('../utils/dateFormatter'); +// formatDate import removed - dates are formatted by email processor const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation'); const { formatBoolean } = require('../utils/dbCompat'); @@ -135,8 +135,7 @@ router.post('/', adminAuth, [ ); // Queue creation email - // Determine language based on email domain - const emailLang = host_email.endsWith('.de') ? 'de' : 'en'; + // Language detection is handled by email processor await db('email_queue').insert({ event_id: eventId, @@ -145,10 +144,10 @@ router.post('/', adminAuth, [ email_data: JSON.stringify({ host_name: host_name, event_name, - event_date: await formatDate(event_date, emailLang), + event_date: event_date, // Pass raw date - will be formatted by email processor gallery_link: shareLink, gallery_password: password, - expiry_date: await formatDate(expires_at, emailLang), + expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor welcome_message: welcome_message || '' }), status: 'pending', @@ -553,10 +552,10 @@ router.post('/:id/reset-password', adminAuth, async (req, res) => { await queueEmail(id, event.host_email, 'gallery_created', { host_name: event.host_email.split('@')[0], event_name: event.event_name, - event_date: new Date(event.event_date).toLocaleDateString(), + event_date: event.event_date, // Pass raw date - will be formatted by email processor gallery_link: event.share_link, gallery_password: newPassword, - expiry_date: new Date(event.expires_at).toLocaleDateString() + expiry_date: event.expires_at // Pass raw date - will be formatted by email processor }); } @@ -603,18 +602,16 @@ router.post('/:id/resend-email', adminAuth, async (req, res) => { galleryPassword = '{{password_security_message}}'; } - // Format dates in a neutral format - the email processor will localize them - const eventDate = new Date(event.event_date); - const expiryDate = new Date(event.expires_at); + // Dates will be formatted by the email processor based on recipient language // Queue the email await queueEmail(id, event.host_email, 'gallery_created', { host_name: event.host_name || event.host_email.split('@')[0], event_name: event.event_name, - event_date: eventDate.toISOString().split('T')[0], // YYYY-MM-DD format + event_date: event.event_date, // Pass raw date - will be formatted by email processor gallery_link: event.share_link, gallery_password: galleryPassword, - expiry_date: expiryDate.toISOString().split('T')[0], // YYYY-MM-DD format + expiry_date: event.expires_at, // Pass raw date - will be formatted by email processor welcome_message: event.welcome_message || '', eventId: id, isResend: true // Flag to indicate this is a resend diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js index e8efd8c..a23ee0c 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.js @@ -87,10 +87,10 @@ router.post('/', adminAuth, [ await queueEmail(eventId, host_email, 'gallery_created', { host_name: host_email.split('@')[0], // Extract name from email event_name, - event_date: new Date(event_date).toLocaleDateString(), + event_date: event_date, // Pass raw date - will be formatted by email processor gallery_link: shareLink, gallery_password: password, - expiry_date: expires_at.toLocaleDateString(), + expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor welcome_message: welcome_message || '' }); diff --git a/backend/src/services/watermarkService.js b/backend/src/services/watermarkService.js index 613b415..8c5de36 100644 --- a/backend/src/services/watermarkService.js +++ b/backend/src/services/watermarkService.js @@ -56,30 +56,30 @@ class WatermarkService { let left, top; switch (position) { - case 'top-left': - left = padding; - top = padding; - break; - case 'top-right': - left = imageWidth - watermarkWidth - padding; - top = padding; - break; - case 'bottom-left': - left = padding; - top = imageHeight - watermarkHeight - padding; - break; - case 'bottom-right': - left = imageWidth - watermarkWidth - padding; - top = imageHeight - watermarkHeight - padding; - break; - case 'center': - left = Math.floor((imageWidth - watermarkWidth) / 2); - top = Math.floor((imageHeight - watermarkHeight) / 2); - break; - default: - // Default to bottom-right - left = imageWidth - watermarkWidth - padding; - top = imageHeight - watermarkHeight - padding; + case 'top-left': + left = padding; + top = padding; + break; + case 'top-right': + left = imageWidth - watermarkWidth - padding; + top = padding; + break; + case 'bottom-left': + left = padding; + top = imageHeight - watermarkHeight - padding; + break; + case 'bottom-right': + left = imageWidth - watermarkWidth - padding; + top = imageHeight - watermarkHeight - padding; + break; + case 'center': + left = Math.floor((imageWidth - watermarkWidth) / 2); + top = Math.floor((imageHeight - watermarkHeight) / 2); + break; + default: + // Default to bottom-right + left = imageWidth - watermarkWidth - padding; + top = imageHeight - watermarkHeight - padding; } return { left: Math.max(0, left), top: Math.max(0, top) }; diff --git a/backend/src/utils/sqlSecurity.js b/backend/src/utils/sqlSecurity.js index e385041..9eb0bce 100644 --- a/backend/src/utils/sqlSecurity.js +++ b/backend/src/utils/sqlSecurity.js @@ -47,7 +47,7 @@ function escapeLikePattern(input) { .replace(/\\/g, '\\\\') // Escape backslashes first .replace(/%/g, '\\%') // Escape percent signs .replace(/_/g, '\\_') // Escape underscores - .replace(/'/g, "''"); // Escape single quotes for safety + .replace(/'/g, '\'\''); // Escape single quotes for safety } /**