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 && ( -
+
+
)} 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