From fc0911acf8b7c8a18d71bb4267f1086acd1e0ca1 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 16 Mar 2026 15:00:37 +0100 Subject: [PATCH] fix: wrap email preview with full styled header/footer template The email template preview modal was showing only raw body HTML without the styled wrapper (green header bar, logo, footer with company name) that processTemplate() applies when sending. This made preview not match what recipients actually receive. Extract wrapEmailHtml() from processTemplate() and reuse it in the preview endpoint. Also fix logo URL to use FRONTEND_URL consistently. Closes #229 --- backend/src/routes/adminEmail.js | 6 +- backend/src/services/emailProcessor.js | 141 +++++++++++++------------ 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index 96e5062a..f6944cb6 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -4,6 +4,7 @@ const { body, validationResult } = require('express-validator'); const { db, logActivity } = require('../database/db'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); +const { wrapEmailHtml } = require('../services/emailProcessor'); const router = express.Router(); // Get email configuration @@ -458,9 +459,12 @@ router.post('/templates/:key/preview', adminAuth, requirePermission('email.view' }); } + // Wrap in the full styled email template with header/footer/logo + const wrappedHtml = await wrapEmailHtml(htmlContent, subject, language); + res.json({ subject, - body_html: htmlContent, + body_html: wrappedHtml, body_text: textContent, language }); diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 85c47ae1..f978ab14 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -111,57 +111,8 @@ async function getRecipientLanguage(email, eventId = null) { return 'en'; // Default to English } -// Process email template with variables -async function processTemplate(template, variables, language = 'en') { - // Import date formatter and text formatters - const { formatDate } = require('../utils/dateFormatter'); - const { formatWelcomeMessage } = require('../utils/formatters'); - - // Get the appropriate language fields - const subjectField = language === 'de' ? 'subject_de' : 'subject_en'; - const htmlField = language === 'de' ? 'body_html_de' : 'body_html_en'; - const textField = language === 'de' ? 'body_text_de' : 'body_text_en'; - - // Fall back to non-language-specific fields for backward compatibility - let subject = template[subjectField] || template.subject || ''; - let htmlBody = template[htmlField] || template.body_html || ''; - let textBody = template[textField] || template.body_text || ''; - - // Process variables before template compilation - const processedVariables = { ...variables }; - - // Handle password security message - if (processedVariables.gallery_password === '{{password_security_message}}') { - processedVariables.gallery_password = language === 'de' - ? '(Aus Sicherheitsgründen nicht angezeigt)' - : '(Not shown for security reasons)'; - } - - if (processedVariables.gallery_password === 'No password required') { - processedVariables.gallery_password = language === 'de' - ? 'Kein Passwort erforderlich' - : 'No password required'; - } - - // Format dates if they exist - if (processedVariables.event_date) { - processedVariables.event_date = await formatDate(processedVariables.event_date, language); - } - if (processedVariables.expiry_date) { - processedVariables.expiry_date = await formatDate(processedVariables.expiry_date, language); - } - if (processedVariables.archive_date) { - processedVariables.archive_date = await formatDate(processedVariables.archive_date, language); - } - if (processedVariables.expires_at) { - processedVariables.expires_at = await formatDate(processedVariables.expires_at, language); - } - - // Format welcome message for HTML display (preserve line breaks) - if (processedVariables.welcome_message) { - processedVariables.welcome_message = formatWelcomeMessage(processedVariables.welcome_message); - } - +// Wrap HTML body in the styled email template with header, footer, and logo +async function wrapEmailHtml(htmlBody, subject, language = 'en') { // Get branding settings for logo let logoUrl = ''; let companyName = 'PicPeak'; @@ -169,7 +120,7 @@ async function processTemplate(template, variables, language = 'en') { const brandingSettings = await db('app_settings') .whereIn('setting_key', ['branding_logo_url', 'branding_company_name']) .select('setting_key', 'setting_value'); - + brandingSettings.forEach(setting => { if (setting.setting_key === 'branding_logo_url' && setting.setting_value) { try { @@ -190,22 +141,10 @@ async function processTemplate(template, variables, language = 'en') { } // If no custom logo, use default PicPeak logo - const apiUrl = process.env.API_URL || 'http://localhost:3001'; - const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3005'; - const logoFullUrl = logoUrl ? `${apiUrl}${logoUrl}` : `${frontendUrl}/picpeak-logo-transparent.png`; + const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; + const logoFullUrl = `${frontendUrl}${logoUrl || '/picpeak-logo-transparent.png'}`; - // Compile templates with Handlebars - const subjectTemplate = Handlebars.compile(subject); - const htmlTemplate = Handlebars.compile(htmlBody); - const textTemplate = Handlebars.compile(textBody); - - // Process templates with processedVariables (includes formatted dates and security messages) - subject = subjectTemplate(processedVariables); - htmlBody = htmlTemplate(processedVariables); - textBody = textTemplate(processedVariables); - - // Wrap HTML body in styled template - const styledHtmlBody = ` + return ` @@ -338,6 +277,71 @@ async function processTemplate(template, variables, language = 'en') { `; +} + +// Process email template with variables +async function processTemplate(template, variables, language = 'en') { + // Import date formatter and text formatters + const { formatDate } = require('../utils/dateFormatter'); + const { formatWelcomeMessage } = require('../utils/formatters'); + + // Get the appropriate language fields + const subjectField = language === 'de' ? 'subject_de' : 'subject_en'; + const htmlField = language === 'de' ? 'body_html_de' : 'body_html_en'; + const textField = language === 'de' ? 'body_text_de' : 'body_text_en'; + + // Fall back to non-language-specific fields for backward compatibility + let subject = template[subjectField] || template.subject || ''; + let htmlBody = template[htmlField] || template.body_html || ''; + let textBody = template[textField] || template.body_text || ''; + + // Process variables before template compilation + const processedVariables = { ...variables }; + + // Handle password security message + if (processedVariables.gallery_password === '{{password_security_message}}') { + processedVariables.gallery_password = language === 'de' + ? '(Aus Sicherheitsgründen nicht angezeigt)' + : '(Not shown for security reasons)'; + } + + if (processedVariables.gallery_password === 'No password required') { + processedVariables.gallery_password = language === 'de' + ? 'Kein Passwort erforderlich' + : 'No password required'; + } + + // Format dates if they exist + if (processedVariables.event_date) { + processedVariables.event_date = await formatDate(processedVariables.event_date, language); + } + if (processedVariables.expiry_date) { + processedVariables.expiry_date = await formatDate(processedVariables.expiry_date, language); + } + if (processedVariables.archive_date) { + processedVariables.archive_date = await formatDate(processedVariables.archive_date, language); + } + if (processedVariables.expires_at) { + processedVariables.expires_at = await formatDate(processedVariables.expires_at, language); + } + + // Format welcome message for HTML display (preserve line breaks) + if (processedVariables.welcome_message) { + processedVariables.welcome_message = formatWelcomeMessage(processedVariables.welcome_message); + } + + // Compile templates with Handlebars + const subjectTemplate = Handlebars.compile(subject); + const htmlTemplate = Handlebars.compile(htmlBody); + const textTemplate = Handlebars.compile(textBody); + + // Process templates with processedVariables (includes formatted dates and security messages) + subject = subjectTemplate(processedVariables); + htmlBody = htmlTemplate(processedVariables); + textBody = textTemplate(processedVariables); + + // Wrap HTML body in styled template + const styledHtmlBody = await wrapEmailHtml(htmlBody, subject, language); return { subject, htmlBody: styledHtmlBody, textBody }; } @@ -558,5 +562,6 @@ module.exports = { processEmailQueue, queueEmail, stopEmailQueueProcessor, - testEmailConnection + testEmailConnection, + wrapEmailHtml };