From 419a283c620529041013681bd51d23297947a370 Mon Sep 17 00:00:00 2001
From: paul
Date: Wed, 16 Jul 2025 16:24:33 +0200
Subject: [PATCH] feat: add resend creation email button and fix Handlebars
template processing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fixed email template processing to properly handle Handlebars conditionals
- Installed handlebars package
- Updated processTemplate to use Handlebars.compile()
- Now {{#if welcome_message}} blocks work correctly
- Added "Resend Creation Email" button to event details page
- New endpoint: POST /api/admin/events/:id/resend-email
- Button appears below "Reset Gallery Password"
- Shows mail icon and includes success/error notifications
- For security, resent emails show "(Aus Sicherheitsgründen nicht angezeigt)"
instead of the actual password
- Added translations:
- EN: "Resend Creation Email", success/error messages
- DE: "Erstellungs-E-Mail erneut senden", success/error messages
This fixes the issue where Handlebars syntax was showing in emails and provides
a convenient way to resend the initial gallery creation email to hosts.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
backend/package-lock.json | 48 ++++++++++++++++-
backend/package.json | 1 +
backend/src/routes/adminEvents.js | 51 +++++++++++++++++++
backend/src/services/emailProcessor.js | 31 ++++-------
frontend/src/i18n/locales/de.json | 3 ++
frontend/src/i18n/locales/en.json | 3 ++
frontend/src/pages/admin/EventDetailsPage.tsx | 21 +++++++-
frontend/src/services/events.service.ts | 6 +++
8 files changed, 140 insertions(+), 24 deletions(-)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 94e8b07..05d8442 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -19,6 +19,7 @@
"express-rate-limit": "^6.7.0",
"express-validator": "^7.0.1",
"form-data": "^4.0.3",
+ "handlebars": "^4.7.8",
"helmet": "^7.0.0",
"i18next": "^25.3.1",
"i18next-browser-languagedetector": "^8.2.0",
@@ -3995,6 +3996,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5985,6 +6007,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT"
+ },
"node_modules/node-abi": {
"version": "3.75.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
@@ -7606,7 +7634,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -8161,6 +8188,19 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@@ -8412,6 +8452,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+ "license": "MIT"
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index 599e089..8956c31 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -23,6 +23,7 @@
"express-rate-limit": "^6.7.0",
"express-validator": "^7.0.1",
"form-data": "^4.0.3",
+ "handlebars": "^4.7.8",
"helmet": "^7.0.0",
"i18next": "^25.3.1",
"i18next-browser-languagedetector": "^8.2.0",
diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js
index 8d41ad5..94036da 100644
--- a/backend/src/routes/adminEvents.js
+++ b/backend/src/routes/adminEvents.js
@@ -564,6 +564,57 @@ router.post('/:id/reset-password', adminAuth, async (req, res) => {
}
});
+// Resend creation email
+router.post('/:id/resend-email', adminAuth, async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ // Get event details
+ const event = await db('events')
+ .where('id', id)
+ .first();
+
+ if (!event) {
+ return res.status(404).json({ error: 'Event not found' });
+ }
+
+ // Format dates properly for the email
+ const eventDate = new Date(event.event_date);
+ const expiryDate = new Date(event.expires_at);
+
+ // Queue the email
+ const { queueEmail } = require('../services/emailProcessor');
+ 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.toLocaleDateString('de-DE'), // Using German date format
+ gallery_link: event.share_link,
+ gallery_password: '(Aus Sicherheitsgründen nicht angezeigt)', // Security: don't show password in resent emails
+ expiry_date: expiryDate.toLocaleDateString('de-DE'),
+ welcome_message: event.welcome_message || '',
+ eventId: id
+ });
+
+ // Log the activity
+ await db('activity_logs').insert({
+ event_id: id,
+ ip_address: req.ip,
+ user_agent: req.get('user-agent'),
+ action_type: 'email_resent',
+ action_details: JSON.stringify({ email_type: 'gallery_created' }),
+ timestamp: new Date()
+ });
+
+ res.json({
+ success: true,
+ message: 'Creation email has been queued for sending'
+ });
+ } catch (error) {
+ console.error('Error resending creation email:', error);
+ res.status(500).json({ error: 'Failed to resend creation email' });
+ }
+});
+
// Archive event
router.post('/:id/archive', adminAuth, async (req, res) => {
try {
diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js
index 21a17d2..f6d7526 100644
--- a/backend/src/services/emailProcessor.js
+++ b/backend/src/services/emailProcessor.js
@@ -1,4 +1,5 @@
const nodemailer = require('nodemailer');
+const Handlebars = require('handlebars');
const { db } = require('../database/db');
const logger = require('../utils/logger');
@@ -138,27 +139,15 @@ async function processTemplate(template, variables, language = 'en') {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3005';
const logoFullUrl = logoUrl ? `${apiUrl}${logoUrl}` : `${frontendUrl}/picpeak-logo-transparent.png`;
- // Process welcome message section if present
- let welcomeMessageSection = '';
- if (variables.welcome_message && variables.welcome_message.trim() !== '') {
- const welcomeTitle = language === 'de' ? 'Persönliche Nachricht:' : 'Personal Message:';
- welcomeMessageSection = `
-
-
${welcomeTitle}
-
${variables.welcome_message}
-
`;
- }
-
- // Replace variables
- Object.entries(variables).forEach(([key, value]) => {
- const regex = new RegExp(`{{${key}}}`, 'g');
- subject = subject.replace(regex, value || '');
- htmlBody = htmlBody.replace(regex, value || '');
- textBody = textBody.replace(regex, value || '');
- });
-
- // Replace welcome message section placeholder
- htmlBody = htmlBody.replace(/{{welcome_message_section}}/g, welcomeMessageSection);
+ // Compile templates with Handlebars
+ const subjectTemplate = Handlebars.compile(subject);
+ const htmlTemplate = Handlebars.compile(htmlBody);
+ const textTemplate = Handlebars.compile(textBody);
+
+ // Process templates with variables
+ subject = subjectTemplate(variables);
+ htmlBody = htmlTemplate(variables);
+ textBody = textTemplate(variables);
// Wrap HTML body in styled template
const styledHtmlBody = `
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index 2097717..5886846 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -244,6 +244,9 @@
"expires": "Läuft ab",
"shareWithGuests": "Teilen Sie diesen Link mit Gästen. Sie benötigen das Passwort, um auf die Galerie zuzugreifen.",
"resetGalleryPassword": "Galerie-Passwort zurücksetzen",
+ "resendCreationEmail": "Erstellungs-E-Mail erneut senden",
+ "creationEmailResent": "Die Erstellungs-E-Mail wurde zur Warteschlange hinzugefügt",
+ "failedToResendEmail": "Fehler beim erneuten Senden der Erstellungs-E-Mail",
"photoStatistics": "Fotostatistiken",
"managePhotos": "Fotos verwalten",
"actions": "Aktionen",
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index b0443bc..c69d862 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -261,6 +261,9 @@
"expires": "Expires",
"shareWithGuests": "Share this link with guests. They'll need the password to access the gallery.",
"resetGalleryPassword": "Reset Gallery Password",
+ "resendCreationEmail": "Resend Creation Email",
+ "creationEmailResent": "Creation email has been queued for sending",
+ "failedToResendEmail": "Failed to resend creation email",
"photoStatistics": "Photo Statistics",
"totalPhotos": "Total Photos",
"managePhotos": "Manage Photos",
diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx
index e399025..dc963dd 100644
--- a/frontend/src/pages/admin/EventDetailsPage.tsx
+++ b/frontend/src/pages/admin/EventDetailsPage.tsx
@@ -15,7 +15,8 @@ import {
CheckCircle,
Upload,
Image,
- Key
+ Key,
+ Mail
} from 'lucide-react';
import { parseISO, differenceInDays } from 'date-fns';
import { toast } from 'react-toastify';
@@ -620,7 +621,7 @@ export const EventDetailsPage: React.FC = () => {
{!event.is_archived && (
-
+
+ }
+ onClick={async () => {
+ try {
+ await eventsService.resendCreationEmail(event.id);
+ toast.success(t('events.creationEmailResent'));
+ } catch (error) {
+ toast.error(t('events.failedToResendEmail'));
+ }
+ }}
+ className="w-full justify-center"
+ >
+ {t('events.resendCreationEmail')}
+
)}
diff --git a/frontend/src/services/events.service.ts b/frontend/src/services/events.service.ts
index 09c176e..41cb8d1 100644
--- a/frontend/src/services/events.service.ts
+++ b/frontend/src/services/events.service.ts
@@ -118,4 +118,10 @@ export const eventsService = {
const response = await api.post(`/admin/events/${eventId}/reset-password`, { sendEmail });
return response.data;
},
+
+ // Resend creation email
+ async resendCreationEmail(eventId: number): Promise<{ success: boolean; message: string }> {
+ const response = await api.post(`/admin/events/${eventId}/resend-email`);
+ return response.data;
+ },
};
\ No newline at end of file