Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ca598b80a | |||
| 4e2075c638 | |||
| 0a691d4251 | |||
| 34846ae71a | |||
| ea261dd03b | |||
| eb93223d79 | |||
| b20f9cc108 | |||
| 515814e1d5 | |||
| fba9838e21 | |||
| a9c2761986 | |||
| db7e5913eb | |||
| aa27d1ea79 | |||
| cf8df2780e | |||
| 419a283c62 | |||
| 1b075a4beb | |||
| 68ff93cf17 | |||
| b22a29e877 | |||
| e601311ca3 | |||
| bb00c3993b | |||
| c9c0de46bf | |||
| 5374299cd5 | |||
| 536e2b2874 | |||
| 141acd5736 | |||
| 5f4337a18d |
@@ -1,5 +1,8 @@
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# Add build argument for cache busting
|
||||
ARG CACHEBUST=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Fix email_queue table by ensuring it doesn't have updated_at column
|
||||
* This migration addresses the PostgreSQL error where queries are trying to update
|
||||
* a non-existent updated_at column
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// First, check if the column exists
|
||||
const hasUpdatedAt = await knex.schema.hasColumn('email_queue', 'updated_at');
|
||||
|
||||
if (hasUpdatedAt) {
|
||||
console.log('Found updated_at column in email_queue table, removing it...');
|
||||
await knex.schema.table('email_queue', (table) => {
|
||||
table.dropColumn('updated_at');
|
||||
});
|
||||
}
|
||||
|
||||
// Also ensure the table has all required columns
|
||||
const hasCreatedAt = await knex.schema.hasColumn('email_queue', 'created_at');
|
||||
if (!hasCreatedAt) {
|
||||
console.log('Adding missing created_at column to email_queue table...');
|
||||
await knex.schema.table('email_queue', (table) => {
|
||||
table.datetime('created_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
console.log('email_queue table schema fixed');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// In the down migration, we don't add back updated_at since it shouldn't exist
|
||||
// This is intentionally left minimal
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
exports.up = async function(knex) {
|
||||
// Update gallery_created template with proper German translation
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.update({
|
||||
subject_de: 'Ihre Fotogalerie ist bereit!',
|
||||
body_html_de: `<h2>Galerie erfolgreich erstellt</h2>
|
||||
<p>Liebe(r) {{host_name}},</p>
|
||||
<p>Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt!</p>
|
||||
{{#if welcome_message}}
|
||||
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0 0 10px 0; font-weight: 600; color: #374151;">Persönliche Nachricht:</p>
|
||||
<p style="margin: 0; color: #4b5563;">{{welcome_message}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
<p><strong>Galerie-Details:</strong></p>
|
||||
<ul>
|
||||
<li>Veranstaltungsdatum: {{event_date}}</li>
|
||||
<li>Galerie-Link: <a href="{{gallery_link}}" style="color: #5C8762;">{{gallery_link}}</a></li>
|
||||
<li>Passwort: {{gallery_password}}</li>
|
||||
<li>Ablaufdatum: {{expiry_date}}</li>
|
||||
</ul>
|
||||
<p>Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit diese die Fotos ansehen und herunterladen können.</p>
|
||||
<p style="background-color: #FEF3C7; padding: 15px; border-radius: 5px; border-left: 4px solid #F59E0B;">
|
||||
<strong>Wichtig:</strong> Diese Galerie läuft am {{expiry_date}} ab. Nach diesem Datum werden die Fotos archiviert und sind nicht mehr zugänglich.
|
||||
</p>
|
||||
<a href="{{gallery_link}}" style="display: inline-block; padding: 12px 30px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px; font-weight: 500; margin: 20px 0;">Galerie anzeigen</a>
|
||||
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
|
||||
body_text_de: `Galerie erfolgreich erstellt
|
||||
|
||||
Liebe(r) {{host_name}},
|
||||
|
||||
Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt!
|
||||
|
||||
{{#if welcome_message}}
|
||||
Persönliche Nachricht:
|
||||
{{welcome_message}}
|
||||
|
||||
{{/if}}
|
||||
Galerie-Details:
|
||||
- Veranstaltungsdatum: {{event_date}}
|
||||
- Galerie-Link: {{gallery_link}}
|
||||
- Passwort: {{gallery_password}}
|
||||
- Ablaufdatum: {{expiry_date}}
|
||||
|
||||
Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit diese die Fotos ansehen und herunterladen können.
|
||||
|
||||
WICHTIG: Diese Galerie läuft am {{expiry_date}} ab. Nach diesem Datum werden die Fotos archiviert und sind nicht mehr zugänglich.
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
Ihr Foto-Sharing-Team`
|
||||
});
|
||||
|
||||
// Update expiration_warning template with proper German translation
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'expiration_warning')
|
||||
.update({
|
||||
subject_de: 'Ihre Fotogalerie läuft bald ab',
|
||||
body_html_de: `<h2>Galerie läuft bald ab</h2>
|
||||
<p>Liebe(r) {{host_name}},</p>
|
||||
<p>Ihre Fotogalerie "{{event_name}}" läuft in <strong>{{days_remaining}} Tagen</strong> ab.</p>
|
||||
<p>Nach Ablauf wird die Galerie archiviert und ist für Gäste nicht mehr zugänglich. Bitte stellen Sie sicher, dass alle gewünschten Fotos heruntergeladen wurden.</p>
|
||||
<p><strong>Ablaufdatum:</strong> {{expiry_date}}</p>
|
||||
<a href="{{gallery_link}}" style="display: inline-block; padding: 12px 30px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px; font-weight: 500; margin: 20px 0;">Galerie jetzt besuchen</a>
|
||||
<p style="background-color: #FEE2E2; padding: 15px; border-radius: 5px; border-left: 4px solid #EF4444;">
|
||||
<strong>Erinnerung:</strong> Nach dem {{expiry_date}} können Ihre Gäste nicht mehr auf die Galerie zugreifen.
|
||||
</p>
|
||||
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
|
||||
body_text_de: `Galerie läuft bald ab
|
||||
|
||||
Liebe(r) {{host_name}},
|
||||
|
||||
Ihre Fotogalerie "{{event_name}}" läuft in {{days_remaining}} Tagen ab.
|
||||
|
||||
Nach Ablauf wird die Galerie archiviert und ist für Gäste nicht mehr zugänglich. Bitte stellen Sie sicher, dass alle gewünschten Fotos heruntergeladen wurden.
|
||||
|
||||
Ablaufdatum: {{expiry_date}}
|
||||
|
||||
Galerie-Link: {{gallery_link}}
|
||||
|
||||
ERINNERUNG: Nach dem {{expiry_date}} können Ihre Gäste nicht mehr auf die Galerie zugreifen.
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
Ihr Foto-Sharing-Team`
|
||||
});
|
||||
|
||||
// Update gallery_expired template with proper German translation
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_expired')
|
||||
.update({
|
||||
subject_de: 'Ihre Fotogalerie {{event_name}} ist abgelaufen',
|
||||
body_html_de: `<h2>Galerie abgelaufen</h2>
|
||||
<p>Liebe(r) {{host_name}},</p>
|
||||
<p>Ihre Fotogalerie "{{event_name}}" ist am {{expiry_date}} abgelaufen und wurde archiviert.</p>
|
||||
<p>Die Galerie ist nicht mehr für Gäste zugänglich. Alle Fotos wurden sicher in unserem Archivsystem gespeichert.</p>
|
||||
<p>Wenn Sie wieder Zugriff auf die archivierten Fotos benötigen, wenden Sie sich bitte an unseren Support:</p>
|
||||
<p style="background-color: #F3F4F6; padding: 15px; border-radius: 5px;">
|
||||
<strong>Kontakt:</strong><br>
|
||||
E-Mail: <a href="mailto:{{admin_email}}" style="color: #5C8762;">{{admin_email}}</a><br>
|
||||
{{#if support_phone}}Telefon: {{support_phone}}{{/if}}
|
||||
</p>
|
||||
<p>Vielen Dank für die Nutzung unseres Foto-Sharing-Services!</p>
|
||||
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
|
||||
body_text_de: `Galerie abgelaufen
|
||||
|
||||
Liebe(r) {{host_name}},
|
||||
|
||||
Ihre Fotogalerie "{{event_name}}" ist am {{expiry_date}} abgelaufen und wurde archiviert.
|
||||
|
||||
Die Galerie ist nicht mehr für Gäste zugänglich. Alle Fotos wurden sicher in unserem Archivsystem gespeichert.
|
||||
|
||||
Wenn Sie wieder Zugriff auf die archivierten Fotos benötigen, wenden Sie sich bitte an unseren Support:
|
||||
|
||||
E-Mail: {{admin_email}}
|
||||
{{#if support_phone}}Telefon: {{support_phone}}{{/if}}
|
||||
|
||||
Vielen Dank für die Nutzung unseres Foto-Sharing-Services!
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
Ihr Foto-Sharing-Team`
|
||||
});
|
||||
|
||||
// Update archive_complete template with proper German translation
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'archive_complete')
|
||||
.update({
|
||||
subject_de: 'Archivierung abgeschlossen: {{event_name}}',
|
||||
body_html_de: `<h2>Archivierung abgeschlossen</h2>
|
||||
<p>Liebe(r) {{host_name}},</p>
|
||||
<p>Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.</p>
|
||||
<p><strong>Archiv-Details:</strong></p>
|
||||
<ul>
|
||||
<li>Archivgröße: {{archive_size}}</li>
|
||||
<li>Archivierungsdatum: {{archive_date}}</li>
|
||||
<li>Anzahl der Fotos: {{photo_count}}</li>
|
||||
</ul>
|
||||
<p>Das Archiv wird sicher in unserem System aufbewahrt. Bei Bedarf können Sie sich an unseren Support wenden, um Zugriff auf die archivierten Fotos zu erhalten.</p>
|
||||
<p style="background-color: #F0FDF4; padding: 15px; border-radius: 5px; border-left: 4px solid #22C55E;">
|
||||
<strong>✓ Erfolgreich archiviert:</strong> Ihre Fotos sind sicher gespeichert und können bei Bedarf wiederhergestellt werden.
|
||||
</p>
|
||||
<p>Kontakt für Archivzugriff:<br>
|
||||
E-Mail: <a href="mailto:{{admin_email}}" style="color: #5C8762;">{{admin_email}}</a></p>
|
||||
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
|
||||
body_text_de: `Archivierung abgeschlossen
|
||||
|
||||
Liebe(r) {{host_name}},
|
||||
|
||||
Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.
|
||||
|
||||
Archiv-Details:
|
||||
- Archivgröße: {{archive_size}}
|
||||
- Archivierungsdatum: {{archive_date}}
|
||||
- Anzahl der Fotos: {{photo_count}}
|
||||
|
||||
Das Archiv wird sicher in unserem System aufbewahrt. Bei Bedarf können Sie sich an unseren Support wenden, um Zugriff auf die archivierten Fotos zu erhalten.
|
||||
|
||||
✓ ERFOLGREICH ARCHIVIERT: Ihre Fotos sind sicher gespeichert und können bei Bedarf wiederhergestellt werden.
|
||||
|
||||
Kontakt für Archivzugriff:
|
||||
E-Mail: {{admin_email}}
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
Ihr Foto-Sharing-Team`
|
||||
});
|
||||
|
||||
// Also update the non-language-specific fields to match German for consistency
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.update({
|
||||
subject: knex.raw('subject_de'),
|
||||
body_html: knex.raw('body_html_de'),
|
||||
body_text: knex.raw('body_text_de')
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'expiration_warning')
|
||||
.update({
|
||||
subject: knex.raw('subject_de'),
|
||||
body_html: knex.raw('body_html_de'),
|
||||
body_text: knex.raw('body_text_de')
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_expired')
|
||||
.update({
|
||||
subject: knex.raw('subject_de'),
|
||||
body_html: knex.raw('body_html_de'),
|
||||
body_text: knex.raw('body_text_de')
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'archive_complete')
|
||||
.update({
|
||||
subject: knex.raw('subject_de'),
|
||||
body_html: knex.raw('body_html_de'),
|
||||
body_text: knex.raw('body_text_de')
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// Revert to previous German translations
|
||||
// This is a simplified rollback - in production you might want to store the old values
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.update({
|
||||
subject: knex.raw('subject_en'),
|
||||
body_html: knex.raw('body_html_en'),
|
||||
body_text: knex.raw('body_text_en')
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'expiration_warning')
|
||||
.update({
|
||||
subject: knex.raw('subject_en'),
|
||||
body_html: knex.raw('body_html_en'),
|
||||
body_text: knex.raw('body_text_en')
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_expired')
|
||||
.update({
|
||||
subject: knex.raw('subject_en'),
|
||||
body_html: knex.raw('body_html_en'),
|
||||
body_text: knex.raw('body_text_en')
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'archive_complete')
|
||||
.update({
|
||||
subject: knex.raw('subject_en'),
|
||||
body_html: knex.raw('body_html_en'),
|
||||
body_text: knex.raw('body_text_en')
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
exports.up = async function(knex) {
|
||||
// Add language column to events table if it doesn't exist
|
||||
const hasLanguageInEvents = await knex.schema.hasColumn('events', 'language');
|
||||
if (!hasLanguageInEvents) {
|
||||
await knex.schema.alterTable('events', function(table) {
|
||||
table.string('language', 5).defaultTo('en');
|
||||
});
|
||||
}
|
||||
|
||||
// Add default_language to email_configs if it doesn't exist
|
||||
const hasDefaultLanguage = await knex.schema.hasColumn('email_configs', 'default_language');
|
||||
if (!hasDefaultLanguage) {
|
||||
await knex.schema.alterTable('email_configs', function(table) {
|
||||
table.string('default_language', 5).defaultTo('en');
|
||||
});
|
||||
}
|
||||
|
||||
// Set default language to German for the existing email config
|
||||
await knex('email_configs')
|
||||
.update({
|
||||
default_language: 'de'
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// Remove language column from events table
|
||||
const hasLanguageInEvents = await knex.schema.hasColumn('events', 'language');
|
||||
if (hasLanguageInEvents) {
|
||||
await knex.schema.alterTable('events', function(table) {
|
||||
table.dropColumn('language');
|
||||
});
|
||||
}
|
||||
|
||||
// Remove default_language from email_configs
|
||||
const hasDefaultLanguage = await knex.schema.hasColumn('email_configs', 'default_language');
|
||||
if (hasDefaultLanguage) {
|
||||
await knex.schema.alterTable('email_configs', function(table) {
|
||||
table.dropColumn('default_language');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,307 @@
|
||||
exports.up = async function(knex) {
|
||||
// Update English templates to match the quality and content of German templates
|
||||
|
||||
// 1. Gallery Created - Match German version with proper styling and conditionals
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.update({
|
||||
subject_en: 'Your photo gallery is ready',
|
||||
body_html_en: `
|
||||
<h2>Hello {{host_name}},</h2>
|
||||
|
||||
<p>Your photo gallery <strong>{{event_name}}</strong> for {{event_date}} has been successfully created and is now online!</p>
|
||||
|
||||
{{#if welcome_message}}
|
||||
<div style="background-color: #f0f8ff; border-left: 4px solid #5C8762; padding: 15px; margin: 20px 0; border-radius: 4px;">
|
||||
<p style="margin: 0;"><strong>Personal message from your photographer:</strong></p>
|
||||
<p style="margin: 10px 0 0 0;">{{welcome_message}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0;">Your access data:</h3>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li style="margin-bottom: 10px;"><strong>Gallery link:</strong> <a href="{{gallery_link}}" style="color: #5C8762;">{{gallery_link}}</a></li>
|
||||
<li style="margin-bottom: 10px;"><strong>Password:</strong> {{gallery_password}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{gallery_link}}" style="display: inline-block; padding: 12px 30px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px; font-weight: 500;">View Gallery</a>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 15px; border-radius: 4px; margin: 20px 0;">
|
||||
<p style="margin: 0;"><strong>Important:</strong> Your gallery will be available until <strong>{{expiry_date}}</strong>. After this date, the photos will be archived and will only be available upon request.</p>
|
||||
</div>
|
||||
|
||||
<p>We hope you enjoy your photos!</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Your Photo Sharing Team</p>`,
|
||||
body_text_en: `Hello {{host_name}},
|
||||
|
||||
Your photo gallery "{{event_name}}" for {{event_date}} has been successfully created and is now online!
|
||||
|
||||
{{#if welcome_message}}
|
||||
Personal message from your photographer:
|
||||
{{welcome_message}}
|
||||
{{/if}}
|
||||
|
||||
Your access data:
|
||||
- Gallery link: {{gallery_link}}
|
||||
- Password: {{gallery_password}}
|
||||
|
||||
Important: Your gallery will be available until {{expiry_date}}. After this date, the photos will be archived and will only be available upon request.
|
||||
|
||||
We hope you enjoy your photos!
|
||||
|
||||
Best regards,
|
||||
Your Photo Sharing Team`
|
||||
});
|
||||
|
||||
// 2. Expiration Warning - Match German version with urgency and styling
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'expiration_warning')
|
||||
.update({
|
||||
subject_en: 'Your photo gallery expires soon',
|
||||
body_html_en: `
|
||||
<h2>Hello {{host_name}},</h2>
|
||||
|
||||
<p>Your photo gallery <strong>{{event_name}}</strong> will expire in <strong style="color: #e74c3c; font-size: 18px;">{{days_remaining}} days</strong>!</p>
|
||||
|
||||
<div style="background-color: #fee; border: 1px solid #fcc; color: #c33; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0; font-weight: bold; font-size: 16px;">⚠️ Important Notice</p>
|
||||
<p style="margin: 10px 0 0 0;">After {{expiry_date}}, your gallery will no longer be accessible online. The photos will be archived and will only be available upon special request.</p>
|
||||
</div>
|
||||
|
||||
<p><strong>Don't miss out – download your photos now!</strong></p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{gallery_link}}" style="display: inline-block; padding: 14px 35px; background-color: #e74c3c; color: white; text-decoration: none; border-radius: 5px; font-weight: 600; font-size: 16px;">Visit Gallery Now</a>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0;"><strong>Quick reminder of your access data:</strong></p>
|
||||
<ul style="list-style: none; padding: 0; margin: 10px 0 0 0;">
|
||||
<li>Gallery link: <a href="{{gallery_link}}" style="color: #5C8762;">{{gallery_link}}</a></li>
|
||||
<li>Password: {{gallery_password}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>If you have any questions, please don't hesitate to contact us.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Your Photo Sharing Team</p>`,
|
||||
body_text_en: `Hello {{host_name}},
|
||||
|
||||
Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days!
|
||||
|
||||
⚠️ Important Notice
|
||||
After {{expiry_date}}, your gallery will no longer be accessible online. The photos will be archived and will only be available upon special request.
|
||||
|
||||
Don't miss out – download your photos now!
|
||||
|
||||
Quick reminder of your access data:
|
||||
- Gallery link: {{gallery_link}}
|
||||
- Password: {{gallery_password}}
|
||||
|
||||
If you have any questions, please don't hesitate to contact us.
|
||||
|
||||
Best regards,
|
||||
Your Photo Sharing Team`
|
||||
});
|
||||
|
||||
// 3. Gallery Expired - Match German version with contact information
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_expired')
|
||||
.update({
|
||||
subject_en: 'Your photo gallery has expired',
|
||||
body_html_en: `
|
||||
<h2>Hello {{host_name}},</h2>
|
||||
|
||||
<p>Your photo gallery <strong>{{event_name}}</strong> expired on {{expiry_date}} and is no longer accessible online.</p>
|
||||
|
||||
<div style="background-color: #f9f9f9; border-left: 4px solid #5C8762; padding: 20px; margin: 20px 0; border-radius: 4px;">
|
||||
<h3 style="margin-top: 0;">Your photos are safely archived</h3>
|
||||
<p>Don't worry – your photos have been securely archived and are not lost. If you need access to your photos, please contact us:</p>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li style="margin-bottom: 8px;">📧 Email: <a href="mailto:{{support_email}}" style="color: #5C8762;">{{support_email}}</a></li>
|
||||
{{#if support_phone}}
|
||||
<li>📞 Phone: {{support_phone}}</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>Please have the following information ready when contacting us:</p>
|
||||
<ul>
|
||||
<li>Event name: {{event_name}}</li>
|
||||
<li>Event date: {{event_date}}</li>
|
||||
<li>Expiry date: {{expiry_date}}</li>
|
||||
</ul>
|
||||
|
||||
<p>We'll be happy to help you access your archived photos.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Your Photo Sharing Team</p>`,
|
||||
body_text_en: `Hello {{host_name}},
|
||||
|
||||
Your photo gallery "{{event_name}}" expired on {{expiry_date}} and is no longer accessible online.
|
||||
|
||||
Your photos are safely archived
|
||||
Don't worry – your photos have been securely archived and are not lost. If you need access to your photos, please contact us:
|
||||
|
||||
📧 Email: {{support_email}}
|
||||
{{#if support_phone}}📞 Phone: {{support_phone}}{{/if}}
|
||||
|
||||
Please have the following information ready when contacting us:
|
||||
- Event name: {{event_name}}
|
||||
- Event date: {{event_date}}
|
||||
- Expiry date: {{expiry_date}}
|
||||
|
||||
We'll be happy to help you access your archived photos.
|
||||
|
||||
Best regards,
|
||||
Your Photo Sharing Team`
|
||||
});
|
||||
|
||||
// 4. Archive Complete - Match German version with success message and details
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'archive_complete')
|
||||
.update({
|
||||
subject_en: 'Your photo gallery has been successfully archived',
|
||||
body_html_en: `
|
||||
<h2>Hello {{host_name}},</h2>
|
||||
|
||||
<p>Your photo gallery <strong>{{event_name}}</strong> has been successfully archived.</p>
|
||||
|
||||
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0; font-weight: bold;">✅ Archive successfully created</p>
|
||||
<p style="margin: 10px 0 0 0;">Your photos are now safely stored in our archive.</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0;">Archive details:</h3>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li style="margin-bottom: 8px;"><strong>Event:</strong> {{event_name}}</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Archive date:</strong> {{archive_date}}</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Number of photos:</strong> {{photo_count}}</li>
|
||||
<li><strong>Archive size:</strong> {{archive_size}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>If you need access to your archived photos in the future, please contact us at:</p>
|
||||
<p style="margin-left: 20px;">
|
||||
📧 <a href="mailto:{{support_email}}" style="color: #5C8762;">{{support_email}}</a><br>
|
||||
{{#if support_phone}}📞 {{support_phone}}{{/if}}
|
||||
</p>
|
||||
|
||||
<p>Thank you for using our photo sharing service!</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Your Photo Sharing Team</p>`,
|
||||
body_text_en: `Hello {{host_name}},
|
||||
|
||||
Your photo gallery "{{event_name}}" has been successfully archived.
|
||||
|
||||
✅ Archive successfully created
|
||||
Your photos are now safely stored in our archive.
|
||||
|
||||
Archive details:
|
||||
- Event: {{event_name}}
|
||||
- Archive date: {{archive_date}}
|
||||
- Number of photos: {{photo_count}}
|
||||
- Archive size: {{archive_size}}
|
||||
|
||||
If you need access to your archived photos in the future, please contact us at:
|
||||
📧 {{support_email}}
|
||||
{{#if support_phone}}📞 {{support_phone}}{{/if}}
|
||||
|
||||
Thank you for using our photo sharing service!
|
||||
|
||||
Best regards,
|
||||
Your Photo Sharing Team`
|
||||
});
|
||||
|
||||
// 5. Test Email - Update to match German style
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'test_email')
|
||||
.update({
|
||||
subject_en: 'Test Email - Photo Sharing Platform',
|
||||
body_html_en: `
|
||||
<h2>Test Email</h2>
|
||||
|
||||
<p>This is a test email from your photo sharing platform.</p>
|
||||
|
||||
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 15px; border-radius: 4px; margin: 20px 0;">
|
||||
<p style="margin: 0;"><strong>✅ Email configuration successful!</strong></p>
|
||||
<p style="margin: 10px 0 0 0;">Your email settings have been configured correctly and emails can be sent.</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0;"><strong>Configuration details:</strong></p>
|
||||
<ul style="margin: 10px 0 0 0;">
|
||||
<li>Timestamp: {{timestamp}}</li>
|
||||
<li>Sender: {{from_email}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Your Photo Sharing Team</p>`,
|
||||
body_text_en: `Test Email
|
||||
|
||||
This is a test email from your photo sharing platform.
|
||||
|
||||
✅ Email configuration successful!
|
||||
Your email settings have been configured correctly and emails can be sent.
|
||||
|
||||
Configuration details:
|
||||
- Timestamp: {{timestamp}}
|
||||
- Sender: {{from_email}}
|
||||
|
||||
Best regards,
|
||||
Your Photo Sharing Team`
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// Revert to previous simpler English templates
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.update({
|
||||
subject_en: 'Your Photo Gallery is Ready',
|
||||
body_html_en: '<h2>Hello,</h2><p>Your photo gallery "{{event_name}}" has been created.</p><p><strong>Access Link:</strong> <a href="{{gallery_link}}">{{gallery_link}}</a></p><p><strong>Password:</strong> {{gallery_password}}</p><p>The gallery will be available until {{expiry_date}}.</p>',
|
||||
body_text_en: 'Your photo gallery "{{event_name}}" has been created. Access Link: {{gallery_link}} Password: {{gallery_password}} The gallery will be available until {{expiry_date}}.'
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'expiration_warning')
|
||||
.update({
|
||||
subject_en: 'Gallery Expires in {{days_remaining}} Days',
|
||||
body_html_en: '<h2>Reminder</h2><p>Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days.</p><p>Please download your photos before {{expiry_date}}.</p><p><a href="{{gallery_link}}">Access Gallery</a></p>',
|
||||
body_text_en: 'Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days. Please download your photos before {{expiry_date}}. Access Gallery: {{gallery_link}}'
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'gallery_expired')
|
||||
.update({
|
||||
subject_en: 'Gallery Expired',
|
||||
body_html_en: '<h2>Gallery Expired</h2><p>Your photo gallery "{{event_name}}" has expired and is no longer accessible.</p><p>If you need access to your photos, please contact support.</p>',
|
||||
body_text_en: 'Your photo gallery "{{event_name}}" has expired and is no longer accessible. If you need access to your photos, please contact support.'
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'archive_complete')
|
||||
.update({
|
||||
subject_en: 'Gallery Archived',
|
||||
body_html_en: '<h2>Archive Complete</h2><p>Your gallery "{{event_name}}" has been archived.</p><p>Archive size: {{archive_size}}</p>',
|
||||
body_text_en: 'Your gallery "{{event_name}}" has been archived. Archive size: {{archive_size}}'
|
||||
});
|
||||
|
||||
await knex('email_templates')
|
||||
.where('template_key', 'test_email')
|
||||
.update({
|
||||
subject_en: 'Test Email',
|
||||
body_html_en: '<p>This is a test email sent at {{timestamp}}.</p>',
|
||||
body_text_en: 'This is a test email sent at {{timestamp}}.'
|
||||
});
|
||||
};
|
||||
Generated
+49
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.59",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.59",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^5.3.1",
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.59",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkEmailEnvironment() {
|
||||
console.log('=== Email Environment Check ===\n');
|
||||
|
||||
// 1. Check environment variables
|
||||
console.log('1. Environment Variables:');
|
||||
const envVars = [
|
||||
'SMTP_HOST',
|
||||
'SMTP_PORT',
|
||||
'SMTP_USER',
|
||||
'SMTP_PASS',
|
||||
'SMTP_FROM',
|
||||
'SMTP_SECURE',
|
||||
'EMAIL_PROCESSOR_ENABLED',
|
||||
'NODE_ENV'
|
||||
];
|
||||
|
||||
envVars.forEach(varName => {
|
||||
const value = process.env[varName];
|
||||
if (varName.includes('PASS')) {
|
||||
console.log(` ${varName}: ${value ? '***' : 'NOT SET'}`);
|
||||
} else {
|
||||
console.log(` ${varName}: ${value || 'NOT SET'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Check database configuration
|
||||
console.log('\n2. Database Email Configuration:');
|
||||
try {
|
||||
const emailConfig = await db('email_configs').first();
|
||||
if (emailConfig) {
|
||||
console.log(' Email configuration found in database:');
|
||||
console.log(` - SMTP Host: ${emailConfig.smtp_host}`);
|
||||
console.log(` - SMTP Port: ${emailConfig.smtp_port}`);
|
||||
console.log(` - SMTP User: ${emailConfig.smtp_user || 'NOT SET'}`);
|
||||
console.log(` - SMTP Secure: ${emailConfig.smtp_secure}`);
|
||||
console.log(` - From Address: ${emailConfig.smtp_from}`);
|
||||
} else {
|
||||
console.log(' ⚠️ No email configuration found in database!');
|
||||
console.log(' This will prevent the email processor from initializing.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error reading email configuration: ${error.message}`);
|
||||
}
|
||||
|
||||
// 3. Check if the email processor should be disabled
|
||||
console.log('\n3. Email Processor Status:');
|
||||
const isDisabled = process.env.EMAIL_PROCESSOR_ENABLED === 'false';
|
||||
if (isDisabled) {
|
||||
console.log(' ⚠️ Email processor is DISABLED via EMAIL_PROCESSOR_ENABLED=false');
|
||||
} else {
|
||||
console.log(' ✅ Email processor is enabled (default)');
|
||||
}
|
||||
|
||||
// 4. Check pending emails
|
||||
console.log('\n4. Email Queue Status:');
|
||||
try {
|
||||
const pending = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
const failed = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.where('retry_count', '>=', 3)
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
const sent = await db('email_queue')
|
||||
.where('status', 'sent')
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
console.log(` - Pending emails: ${pending.count}`);
|
||||
console.log(` - Failed emails (max retries): ${failed.count}`);
|
||||
console.log(` - Sent emails: ${sent.count}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error querying email queue: ${error.message}`);
|
||||
}
|
||||
|
||||
// 5. Test database connection
|
||||
console.log('\n5. Database Connection:');
|
||||
try {
|
||||
await db.raw('SELECT 1');
|
||||
console.log(' ✅ Database connection successful');
|
||||
} catch (error) {
|
||||
console.log(` ❌ Database connection failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// 6. Check for any recent errors
|
||||
console.log('\n6. Recent Email Errors:');
|
||||
try {
|
||||
const recentErrors = await db('email_queue')
|
||||
.whereNotNull('error_message')
|
||||
.orderBy('id', 'desc')
|
||||
.limit(3)
|
||||
.select('id', 'email_type', 'error_message', 'retry_count');
|
||||
|
||||
if (recentErrors.length > 0) {
|
||||
recentErrors.forEach((email, index) => {
|
||||
console.log(` ${index + 1}. Email ID ${email.id} (${email.email_type}):`);
|
||||
console.log(` Retries: ${email.retry_count}`);
|
||||
console.log(` Error: ${email.error_message}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' No recent errors found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error querying recent errors: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n=== Environment check complete ===');
|
||||
console.log('\nRecommendations:');
|
||||
|
||||
const emailConfig = await db('email_configs').first().catch(() => null);
|
||||
if (!emailConfig) {
|
||||
console.log('❗ Configure email settings in the admin panel or add email_configs record');
|
||||
}
|
||||
|
||||
if (!process.env.SMTP_HOST && !emailConfig) {
|
||||
console.log('❗ Set SMTP environment variables or configure in database');
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
checkEmailEnvironment().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function checkEmailProcessor() {
|
||||
try {
|
||||
logger.info('=== Email Processor Diagnostic Check ===\n');
|
||||
|
||||
// 1. Check pending emails
|
||||
logger.info('1. Checking pending emails in queue...');
|
||||
const pendingEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.orderBy('created_at', 'asc');
|
||||
|
||||
logger.info(`Found ${pendingEmails.length} pending emails\n`);
|
||||
|
||||
if (pendingEmails.length > 0) {
|
||||
logger.info('Pending email details:');
|
||||
pendingEmails.forEach((email, index) => {
|
||||
logger.info(`\nEmail ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Event ID: ${email.event_id}`);
|
||||
logger.info(` Status: ${email.status}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}`);
|
||||
logger.info(` Scheduled At: ${email.scheduled_at}`);
|
||||
logger.info(` Created At: ${email.created_at}`);
|
||||
logger.info(` Error: ${email.error_message || 'None'}`);
|
||||
|
||||
// Check if email_data needs parsing
|
||||
logger.info(` Email Data Type: ${typeof email.email_data}`);
|
||||
if (email.email_data) {
|
||||
try {
|
||||
const data = typeof email.email_data === 'string'
|
||||
? JSON.parse(email.email_data)
|
||||
: email.email_data;
|
||||
logger.info(` Email Data Keys: ${Object.keys(data).join(', ')}`);
|
||||
} catch (e) {
|
||||
logger.error(` Failed to parse email_data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check failed emails
|
||||
logger.info('\n\n2. Checking failed emails...');
|
||||
const failedEmails = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(5);
|
||||
|
||||
logger.info(`Found ${failedEmails.length} failed emails (showing last 5)\n`);
|
||||
|
||||
if (failedEmails.length > 0) {
|
||||
failedEmails.forEach((email, index) => {
|
||||
logger.info(`\nFailed Email ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}`);
|
||||
logger.info(` Error: ${email.error_message || 'No error message'}`);
|
||||
logger.info(` Last Attempt: ${email.sent_at || 'Never'}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Check if email processor should be running
|
||||
logger.info('\n\n3. Checking email processor configuration...');
|
||||
|
||||
// Check environment variables
|
||||
const emailConfig = {
|
||||
SMTP_HOST: process.env.SMTP_HOST,
|
||||
SMTP_PORT: process.env.SMTP_PORT,
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
SMTP_FROM: process.env.SMTP_FROM,
|
||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||
EMAIL_PROCESSOR_ENABLED: process.env.EMAIL_PROCESSOR_ENABLED || 'true'
|
||||
};
|
||||
|
||||
logger.info('Email configuration:');
|
||||
Object.entries(emailConfig).forEach(([key, value]) => {
|
||||
if (key === 'SMTP_USER') {
|
||||
logger.info(` ${key}: ${value ? '***' : 'NOT SET'}`);
|
||||
} else {
|
||||
logger.info(` ${key}: ${value || 'NOT SET'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Test email processor functionality
|
||||
logger.info('\n\n4. Testing email processor functionality...');
|
||||
|
||||
// Import the email processor
|
||||
const { processEmailQueue, testEmailConnection } = require('../src/services/emailProcessor');
|
||||
|
||||
// Test email connection
|
||||
logger.info('Testing email connection...');
|
||||
try {
|
||||
const connectionTest = await testEmailConnection();
|
||||
logger.info(`Email connection test: ${connectionTest ? 'SUCCESS' : 'FAILED'}`);
|
||||
} catch (error) {
|
||||
logger.error(`Email connection test failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Try to process queue once manually
|
||||
if (pendingEmails.length > 0) {
|
||||
logger.info('\n\n5. Attempting to process email queue manually...');
|
||||
try {
|
||||
await processEmailQueue();
|
||||
logger.info('Manual queue processing completed');
|
||||
|
||||
// Check status after processing
|
||||
const stillPending = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
logger.info(`Emails still pending after processing: ${stillPending.count}`);
|
||||
} catch (error) {
|
||||
logger.error(`Error processing queue: ${error.message}`);
|
||||
logger.error(`Stack trace: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check for any recent successful emails
|
||||
logger.info('\n\n6. Checking recent successful emails...');
|
||||
const recentSuccess = await db('email_queue')
|
||||
.where('status', 'sent')
|
||||
.orderBy('sent_at', 'desc')
|
||||
.limit(3);
|
||||
|
||||
if (recentSuccess.length > 0) {
|
||||
logger.info(`Last ${recentSuccess.length} successful emails:`);
|
||||
recentSuccess.forEach((email, index) => {
|
||||
logger.info(` ${index + 1}. Type: ${email.email_type}, Sent: ${email.sent_at}`);
|
||||
});
|
||||
} else {
|
||||
logger.info('No successfully sent emails found');
|
||||
}
|
||||
|
||||
logger.info('\n\n=== Diagnostic check complete ===');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error running diagnostic check:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkEmailProcessor();
|
||||
@@ -0,0 +1,66 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkEmailTemplates() {
|
||||
try {
|
||||
console.log('=== Email Templates Check ===\n');
|
||||
|
||||
// 1. Check table columns
|
||||
console.log('1. Checking email_templates table structure...');
|
||||
|
||||
// Check which columns exist
|
||||
const columnChecks = [
|
||||
'subject', 'subject_en', 'subject_de',
|
||||
'body_html', 'body_html_en', 'body_html_de',
|
||||
'body_text', 'body_text_en', 'body_text_de'
|
||||
];
|
||||
|
||||
const existingColumns = [];
|
||||
for (const col of columnChecks) {
|
||||
const exists = await db.schema.hasColumn('email_templates', col);
|
||||
if (exists) existingColumns.push(col);
|
||||
}
|
||||
|
||||
console.log(' Existing columns:', existingColumns.join(', '));
|
||||
|
||||
// 2. Get all templates
|
||||
console.log('\n2. Current email templates:');
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\n Template: ${template.template_key}`);
|
||||
console.log(' -------------------');
|
||||
|
||||
// Check which fields have content
|
||||
const fields = ['subject', 'subject_en', 'subject_de',
|
||||
'body_html', 'body_html_en', 'body_html_de',
|
||||
'body_text', 'body_text_en', 'body_text_de'];
|
||||
|
||||
for (const field of fields) {
|
||||
if (template[field]) {
|
||||
const preview = template[field].substring(0, 50) + '...';
|
||||
console.log(` ${field}: ${preview}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for German translations
|
||||
const hasGermanSubject = template.subject_de || template.body_html_de;
|
||||
console.log(` Has German translation: ${hasGermanSubject ? 'YES' : 'NO'}`);
|
||||
}
|
||||
|
||||
// 3. Summary
|
||||
console.log('\n3. Summary:');
|
||||
const totalTemplates = templates.length;
|
||||
const templatesWithGerman = templates.filter(t => t.subject_de || t.body_html_de).length;
|
||||
console.log(` Total templates: ${totalTemplates}`);
|
||||
console.log(` Templates with German: ${templatesWithGerman}`);
|
||||
console.log(` Missing German: ${totalTemplates - templatesWithGerman}`);
|
||||
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkEmailTemplates();
|
||||
@@ -0,0 +1,53 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkGermanTemplates() {
|
||||
try {
|
||||
console.log('=== German Email Template Content Check ===\n');
|
||||
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\nTemplate: ${template.template_key}`);
|
||||
console.log('=====================================');
|
||||
|
||||
// Check German subject
|
||||
console.log('\nGERMAN SUBJECT:');
|
||||
console.log(template.subject_de || 'MISSING');
|
||||
|
||||
// Check if German HTML body has English content
|
||||
console.log('\nGERMAN HTML BODY:');
|
||||
const germanHtml = template.body_html_de || '';
|
||||
|
||||
// Check for English phrases in German template
|
||||
const englishPhrases = [
|
||||
'Dear', 'Gallery', 'has been', 'Your photo', 'successfully',
|
||||
'Details:', 'Link:', 'Password:', 'Expires:', 'Event Date:',
|
||||
'Thank you', 'Best regards', 'View Gallery', 'days'
|
||||
];
|
||||
|
||||
const foundEnglish = englishPhrases.filter(phrase =>
|
||||
germanHtml.toLowerCase().includes(phrase.toLowerCase())
|
||||
);
|
||||
|
||||
if (foundEnglish.length > 0) {
|
||||
console.log('⚠️ Found English phrases in German template:', foundEnglish.join(', '));
|
||||
}
|
||||
|
||||
// Show first 500 chars of German HTML
|
||||
console.log(germanHtml.substring(0, 500) + '...\n');
|
||||
|
||||
// Check German text body
|
||||
console.log('GERMAN TEXT BODY:');
|
||||
const germanText = template.body_text_de || '';
|
||||
console.log(germanText.substring(0, 300) + '...\n');
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkGermanTemplates();
|
||||
@@ -0,0 +1,146 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function debugEmailQueue() {
|
||||
try {
|
||||
logger.info('=== Email Queue Debug Report ===\n');
|
||||
|
||||
// 1. Count exactly like the admin dashboard does
|
||||
logger.info('1. Admin Dashboard Query (ALL pending, no retry filter):');
|
||||
const [adminCount] = await db('email_queue').where('status', 'pending').count('* as count');
|
||||
logger.info(` Pending emails (admin dashboard view): ${adminCount.count}\n`);
|
||||
|
||||
// 2. Count like the email processor does
|
||||
logger.info('2. Email Processor Query (pending with retry_count < 3):');
|
||||
const [processorCount] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count');
|
||||
logger.info(` Pending emails (processor view): ${processorCount.count}\n`);
|
||||
|
||||
// 3. Show the discrepancy
|
||||
logger.info('3. Discrepancy Analysis:');
|
||||
if (adminCount.count !== processorCount.count) {
|
||||
logger.info(` ⚠️ DISCREPANCY FOUND!`);
|
||||
logger.info(` Admin shows: ${adminCount.count}`);
|
||||
logger.info(` Processor will process: ${processorCount.count}`);
|
||||
logger.info(` Difference: ${adminCount.count - processorCount.count} email(s)\n`);
|
||||
|
||||
// Find the problematic emails
|
||||
logger.info('4. Emails with retry_count >= 3 (still pending):');
|
||||
const stuckEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '>=', 3)
|
||||
.select('*');
|
||||
|
||||
if (stuckEmails.length > 0) {
|
||||
logger.info(` Found ${stuckEmails.length} stuck email(s):\n`);
|
||||
stuckEmails.forEach((email, index) => {
|
||||
logger.info(` Email ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Status: ${email.status}`);
|
||||
logger.info(` Retry Count: ${email.retry_count} ⚠️`);
|
||||
logger.info(` Created: ${email.created_at}`);
|
||||
logger.info(` Last Error: ${email.error_message || 'None'}\n`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.info(` ✅ No discrepancy - counts match\n`);
|
||||
}
|
||||
|
||||
// 5. Show ALL pending emails with details
|
||||
logger.info('5. ALL Pending Emails (regardless of retry count):');
|
||||
const allPending = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.orderBy('retry_count', 'desc')
|
||||
.orderBy('created_at', 'asc');
|
||||
|
||||
if (allPending.length > 0) {
|
||||
allPending.forEach((email, index) => {
|
||||
const willProcess = email.retry_count < 3;
|
||||
logger.info(`\n Email ${index + 1}: ${willProcess ? '✅ WILL PROCESS' : '❌ STUCK (max retries)'}`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Event ID: ${email.event_id}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}/3`);
|
||||
logger.info(` Created: ${email.created_at}`);
|
||||
logger.info(` Scheduled: ${email.scheduled_at}`);
|
||||
if (email.error_message) {
|
||||
logger.info(` Last Error: ${email.error_message}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info(' No pending emails found');
|
||||
}
|
||||
|
||||
// 6. Show counts by status
|
||||
logger.info('\n\n6. Email Queue Summary by Status:');
|
||||
const statusCounts = await db('email_queue')
|
||||
.select('status')
|
||||
.count('* as count')
|
||||
.groupBy('status')
|
||||
.orderBy('status');
|
||||
|
||||
statusCounts.forEach(row => {
|
||||
logger.info(` ${row.status}: ${row.count}`);
|
||||
});
|
||||
|
||||
// 7. Failed emails summary
|
||||
logger.info('\n7. Failed Emails Summary:');
|
||||
const failedSummary = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.select('retry_count')
|
||||
.count('* as count')
|
||||
.groupBy('retry_count')
|
||||
.orderBy('retry_count');
|
||||
|
||||
if (failedSummary.length > 0) {
|
||||
failedSummary.forEach(row => {
|
||||
logger.info(` Retry count ${row.retry_count}: ${row.count} email(s)`);
|
||||
});
|
||||
} else {
|
||||
logger.info(' No failed emails');
|
||||
}
|
||||
|
||||
// 8. Recommendations
|
||||
logger.info('\n\n=== RECOMMENDATIONS ===');
|
||||
|
||||
if (adminCount.count > processorCount.count) {
|
||||
logger.info('\n❗ You have emails stuck with retry_count >= 3');
|
||||
logger.info(' These emails will NOT be processed automatically.');
|
||||
logger.info('\n To fix this, you can:');
|
||||
logger.info(' 1. Reset retry count: UPDATE email_queue SET retry_count = 0 WHERE status = \'pending\' AND retry_count >= 3;');
|
||||
logger.info(' 2. Mark as failed: UPDATE email_queue SET status = \'failed\' WHERE status = \'pending\' AND retry_count >= 3;');
|
||||
logger.info(' 3. Delete them: DELETE FROM email_queue WHERE status = \'pending\' AND retry_count >= 3;');
|
||||
}
|
||||
|
||||
const anyPending = adminCount.count > 0;
|
||||
if (anyPending && processorCount.count === 0) {
|
||||
logger.info('\n❗ All pending emails have exceeded retry limit');
|
||||
logger.info(' The email processor will not attempt to send them.');
|
||||
} else if (anyPending && processorCount.count > 0) {
|
||||
logger.info('\n✅ Email processor should process the pending emails on next run');
|
||||
logger.info(' Make sure the email processor service is running.');
|
||||
}
|
||||
|
||||
logger.info('\n=== Debug report complete ===');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error running debug report:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the debug
|
||||
debugEmailQueue();
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to diagnose and fix email_queue schema issues
|
||||
* This helps resolve the "column updated_at does not exist" error
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkAndFixEmailQueueSchema() {
|
||||
console.log('Checking email_queue table schema...');
|
||||
|
||||
try {
|
||||
// Get column information
|
||||
const columns = await db('email_queue').columnInfo();
|
||||
console.log('\nCurrent email_queue columns:', Object.keys(columns));
|
||||
|
||||
// Check for updated_at column
|
||||
if (columns.updated_at) {
|
||||
console.log('\n⚠️ Found unexpected updated_at column in email_queue table!');
|
||||
console.log('This column should not exist and is causing errors.');
|
||||
|
||||
// Ask for confirmation before removing
|
||||
console.log('\nRemoving updated_at column...');
|
||||
await db.schema.table('email_queue', (table) => {
|
||||
table.dropColumn('updated_at');
|
||||
});
|
||||
console.log('✅ Removed updated_at column from email_queue table');
|
||||
} else {
|
||||
console.log('✅ No updated_at column found (this is correct)');
|
||||
}
|
||||
|
||||
// Verify required columns exist
|
||||
const requiredColumns = [
|
||||
'id', 'event_id', 'recipient_email', 'email_type',
|
||||
'email_data', 'status', 'scheduled_at', 'sent_at',
|
||||
'error_message', 'retry_count', 'created_at'
|
||||
];
|
||||
|
||||
const missingColumns = requiredColumns.filter(col => !columns[col]);
|
||||
if (missingColumns.length > 0) {
|
||||
console.log('\n⚠️ Missing required columns:', missingColumns);
|
||||
} else {
|
||||
console.log('✅ All required columns are present');
|
||||
}
|
||||
|
||||
// Check for any database triggers
|
||||
if (process.env.DATABASE_CLIENT === 'pg') {
|
||||
console.log('\nChecking for PostgreSQL triggers on email_queue...');
|
||||
const triggers = await db.raw(`
|
||||
SELECT trigger_name, event_manipulation, action_statement
|
||||
FROM information_schema.triggers
|
||||
WHERE event_object_table = 'email_queue'
|
||||
AND trigger_schema = current_schema()
|
||||
`);
|
||||
|
||||
if (triggers.rows && triggers.rows.length > 0) {
|
||||
console.log('⚠️ Found triggers on email_queue table:');
|
||||
triggers.rows.forEach(trigger => {
|
||||
console.log(` - ${trigger.trigger_name} (${trigger.event_manipulation})`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ No triggers found on email_queue table');
|
||||
}
|
||||
}
|
||||
|
||||
// Test update query
|
||||
console.log('\nTesting update query...');
|
||||
const testEmail = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.first();
|
||||
|
||||
if (testEmail) {
|
||||
try {
|
||||
await db('email_queue')
|
||||
.where('id', testEmail.id)
|
||||
.update({
|
||||
retry_count: testEmail.retry_count
|
||||
});
|
||||
console.log('✅ Update query works correctly');
|
||||
} catch (error) {
|
||||
console.log('❌ Update query failed:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ No pending emails to test with');
|
||||
}
|
||||
|
||||
console.log('\nSchema check complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error checking schema:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkAndFixEmailQueueSchema();
|
||||
@@ -0,0 +1,88 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixFinalGermanTemplates() {
|
||||
try {
|
||||
console.log('Fixing remaining English words in German templates...\n');
|
||||
|
||||
// Get all templates
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
let updated = false;
|
||||
let updates = {};
|
||||
|
||||
// Fix subject_de
|
||||
if (template.subject_de) {
|
||||
updates.subject_de = template.subject_de;
|
||||
}
|
||||
|
||||
// Fix body_html_de
|
||||
if (template.body_html_de) {
|
||||
let html = template.body_html_de;
|
||||
|
||||
// Replace English words with German
|
||||
html = html.replace(/Gallery-Details:/g, 'Galerie-Details:');
|
||||
html = html.replace(/Galerie-Details:/g, 'Galerie-Details:');
|
||||
html = html.replace(/Details:/g, 'Details:');
|
||||
html = html.replace(/Link:/g, 'Link:');
|
||||
html = html.replace(/Gallery-Link:/g, 'Galerie-Link:');
|
||||
html = html.replace(/Galerie-Link:/g, 'Galerie-Link:');
|
||||
html = html.replace(/Archive-Details:/g, 'Archiv-Details:');
|
||||
html = html.replace(/Archiv-Details:/g, 'Archiv-Details:');
|
||||
|
||||
if (html !== template.body_html_de) {
|
||||
updates.body_html_de = html;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix body_text_de
|
||||
if (template.body_text_de) {
|
||||
let text = template.body_text_de;
|
||||
|
||||
text = text.replace(/Gallery-Details:/g, 'Galerie-Details:');
|
||||
text = text.replace(/Galerie-Details:/g, 'Galerie-Details:');
|
||||
text = text.replace(/Details:/g, 'Details:');
|
||||
text = text.replace(/Link:/g, 'Link:');
|
||||
text = text.replace(/Gallery-Link:/g, 'Galerie-Link:');
|
||||
text = text.replace(/Galerie-Link:/g, 'Galerie-Link:');
|
||||
text = text.replace(/Archive-Details:/g, 'Archiv-Details:');
|
||||
text = text.replace(/Archiv-Details:/g, 'Archiv-Details:');
|
||||
|
||||
if (text !== template.body_text_de) {
|
||||
updates.body_text_de = text;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the non-language-specific fields to match German
|
||||
if (template.body_html_de) {
|
||||
updates.body_html = template.body_html_de;
|
||||
}
|
||||
if (template.body_text_de) {
|
||||
updates.body_text = template.body_text_de;
|
||||
}
|
||||
if (template.subject_de) {
|
||||
updates.subject = template.subject_de;
|
||||
}
|
||||
|
||||
if (updated || Object.keys(updates).length > 0) {
|
||||
await db('email_templates')
|
||||
.where('template_key', template.template_key)
|
||||
.update(updates);
|
||||
console.log(`✅ Updated ${template.template_key}`);
|
||||
} else {
|
||||
console.log(`⏭️ No changes needed for ${template.template_key}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nDone!');
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fixFinalGermanTemplates();
|
||||
@@ -0,0 +1,126 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function fixStuckEmails() {
|
||||
try {
|
||||
logger.info('=== Fix Stuck Emails Script ===\n');
|
||||
|
||||
// 1. Find stuck emails
|
||||
logger.info('1. Finding stuck emails (pending with retry_count >= 3)...');
|
||||
const stuckEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '>=', 3)
|
||||
.select('*');
|
||||
|
||||
if (stuckEmails.length === 0) {
|
||||
logger.info(' ✅ No stuck emails found!');
|
||||
logger.info('\n=== Script complete ===');
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
logger.info(` Found ${stuckEmails.length} stuck email(s)\n`);
|
||||
|
||||
// 2. Show details
|
||||
logger.info('2. Stuck email details:');
|
||||
stuckEmails.forEach((email, index) => {
|
||||
logger.info(`\n Email ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}`);
|
||||
logger.info(` Last Error: ${email.error_message || 'None'}`);
|
||||
});
|
||||
|
||||
// 3. Ask for action
|
||||
logger.info('\n\n3. Choose an action:');
|
||||
logger.info(' 1. Reset retry count to 0 (emails will be retried)');
|
||||
logger.info(' 2. Mark as failed (emails will not be retried)');
|
||||
logger.info(' 3. Delete these emails');
|
||||
logger.info(' 4. Cancel (do nothing)');
|
||||
|
||||
// Get command line argument
|
||||
const action = process.argv[2];
|
||||
|
||||
if (!action || !['reset', 'fail', 'delete'].includes(action)) {
|
||||
logger.info('\n❗ No valid action specified');
|
||||
logger.info('\nUsage:');
|
||||
logger.info(' node fix-stuck-emails.js reset - Reset retry count to 0');
|
||||
logger.info(' node fix-stuck-emails.js fail - Mark as failed');
|
||||
logger.info(' node fix-stuck-emails.js delete - Delete stuck emails');
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4. Execute action
|
||||
logger.info(`\n4. Executing action: ${action.toUpperCase()}`);
|
||||
|
||||
const emailIds = stuckEmails.map(e => e.id);
|
||||
|
||||
switch (action) {
|
||||
case 'reset':
|
||||
await db('email_queue')
|
||||
.whereIn('id', emailIds)
|
||||
.update({
|
||||
retry_count: 0,
|
||||
error_message: null
|
||||
});
|
||||
logger.info(` ✅ Reset retry count for ${emailIds.length} email(s)`);
|
||||
logger.info(' These emails will be processed on the next run');
|
||||
break;
|
||||
|
||||
case 'fail':
|
||||
await db('email_queue')
|
||||
.whereIn('id', emailIds)
|
||||
.update({
|
||||
status: 'failed'
|
||||
});
|
||||
logger.info(` ✅ Marked ${emailIds.length} email(s) as failed`);
|
||||
logger.info(' These emails will not be retried');
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
await db('email_queue')
|
||||
.whereIn('id', emailIds)
|
||||
.delete();
|
||||
logger.info(` ✅ Deleted ${emailIds.length} email(s)`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. Show updated counts
|
||||
logger.info('\n5. Updated email queue status:');
|
||||
const [pendingCount] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.count('* as count');
|
||||
const [processableCount] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count');
|
||||
|
||||
logger.info(` Total pending: ${pendingCount.count}`);
|
||||
logger.info(` Processable (retry < 3): ${processableCount.count}`);
|
||||
|
||||
if (pendingCount.count !== processableCount.count) {
|
||||
logger.info(` ⚠️ Still have ${pendingCount.count - processableCount.count} stuck email(s)`);
|
||||
} else {
|
||||
logger.info(' ✅ No stuck emails remaining');
|
||||
}
|
||||
|
||||
logger.info('\n=== Script complete ===');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the fix
|
||||
fixStuckEmails();
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
const {
|
||||
initializeTransporter,
|
||||
processEmailQueue,
|
||||
testEmailConnection
|
||||
} = require('../src/services/emailProcessor');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function runEmailProcessor(runOnce = false) {
|
||||
try {
|
||||
logger.info('=== Starting Email Processor ===\n');
|
||||
|
||||
// Initialize transporter
|
||||
logger.info('Initializing email transporter...');
|
||||
await initializeTransporter();
|
||||
|
||||
// Test connection
|
||||
logger.info('Testing email connection...');
|
||||
const connectionOk = await testEmailConnection();
|
||||
|
||||
if (!connectionOk) {
|
||||
logger.error('Email connection test failed! Check your SMTP configuration.');
|
||||
logger.info('\nRequired environment variables:');
|
||||
logger.info('- SMTP_HOST');
|
||||
logger.info('- SMTP_PORT');
|
||||
logger.info('- SMTP_USER');
|
||||
logger.info('- SMTP_PASS');
|
||||
logger.info('- SMTP_FROM');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.info('Email connection test successful!\n');
|
||||
|
||||
if (runOnce) {
|
||||
// Process queue once
|
||||
logger.info('Processing email queue once...');
|
||||
await processEmailQueue();
|
||||
logger.info('Email processing complete');
|
||||
|
||||
// Show final status
|
||||
const pendingCount = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
logger.info(`\nEmails still pending: ${pendingCount.count}`);
|
||||
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Run continuously
|
||||
logger.info('Starting continuous email processor...');
|
||||
logger.info('Processing emails every 60 seconds. Press Ctrl+C to stop.\n');
|
||||
|
||||
// Process immediately
|
||||
await processEmailQueue();
|
||||
|
||||
// Then every minute
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await processEmailQueue();
|
||||
} catch (error) {
|
||||
logger.error('Error processing email queue:', error);
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Fatal error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('\n\nShutting down email processor...');
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Check command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const runOnce = args.includes('--once') || args.includes('-o');
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Email Processor Runner
|
||||
|
||||
Usage: node run-email-processor.js [options]
|
||||
|
||||
Options:
|
||||
--once, -o Process the email queue once and exit
|
||||
--help, -h Show this help message
|
||||
|
||||
By default, the processor runs continuously, checking for emails every 60 seconds.
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run the processor
|
||||
runEmailProcessor(runOnce);
|
||||
@@ -0,0 +1,85 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { processTemplate } = require('../src/services/emailProcessor');
|
||||
|
||||
async function testGermanEmails() {
|
||||
try {
|
||||
console.log('=== Testing German Email Templates ===\n');
|
||||
|
||||
// Test variables
|
||||
const testVars = {
|
||||
host_name: 'Max Mustermann',
|
||||
event_name: 'Hochzeit Schmidt',
|
||||
event_date: '15.07.2024',
|
||||
gallery_link: 'https://example.com/gallery/test',
|
||||
gallery_password: 'test1234',
|
||||
expiry_date: '15.08.2024',
|
||||
days_remaining: '7',
|
||||
welcome_message: 'Herzlich willkommen zu unserer Hochzeitsgalerie!',
|
||||
archive_size: '250 MB',
|
||||
archive_date: '16.08.2024',
|
||||
photo_count: '347',
|
||||
admin_email: 'support@example.com',
|
||||
eventId: 1
|
||||
};
|
||||
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\n========== ${template.template_key.toUpperCase()} ==========`);
|
||||
|
||||
// Process German version
|
||||
const germanResult = await processGermanTemplate(template, testVars);
|
||||
|
||||
console.log('\n--- GERMAN VERSION ---');
|
||||
console.log('Subject:', germanResult.subject);
|
||||
console.log('\nHTML Preview (first 500 chars):');
|
||||
console.log(germanResult.htmlBody.substring(0, 500) + '...\n');
|
||||
|
||||
// Check for any remaining English text
|
||||
const englishWords = ['Dear', 'Gallery', 'Details:', 'Link:', 'Password:', 'days', 'Thank you'];
|
||||
const foundEnglish = englishWords.filter(word =>
|
||||
germanResult.htmlBody.includes(word) || germanResult.subject.includes(word)
|
||||
);
|
||||
|
||||
if (foundEnglish.length > 0) {
|
||||
console.log('⚠️ WARNING: Found English words:', foundEnglish.join(', '));
|
||||
} else {
|
||||
console.log('✅ No English words found in German template');
|
||||
}
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function processGermanTemplate(template, variables) {
|
||||
// Process template as German
|
||||
const subjectField = 'subject_de';
|
||||
const htmlField = 'body_html_de';
|
||||
const textField = 'body_text_de';
|
||||
|
||||
let subject = template[subjectField] || template.subject || '';
|
||||
let htmlBody = template[htmlField] || template.body_html || '';
|
||||
let textBody = template[textField] || template.body_text || '';
|
||||
|
||||
// Replace variables
|
||||
Object.keys(variables).forEach(key => {
|
||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||
subject = subject.replace(regex, variables[key]);
|
||||
htmlBody = htmlBody.replace(regex, variables[key]);
|
||||
textBody = textBody.replace(regex, variables[key]);
|
||||
});
|
||||
|
||||
// Handle conditionals (simplified)
|
||||
htmlBody = htmlBody.replace(/{{#if welcome_message}}[\s\S]*?{{\/if}}/g, (match) => {
|
||||
return variables.welcome_message ? match.replace(/{{#if welcome_message}}|{{\/if}}/g, '') : '';
|
||||
});
|
||||
|
||||
return { subject, htmlBody, textBody };
|
||||
}
|
||||
|
||||
testGermanEmails();
|
||||
@@ -0,0 +1,110 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function verifyTemplateEquality() {
|
||||
try {
|
||||
console.log('Verifying template equality between German and English versions...\n');
|
||||
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\n=== ${template.template_key.toUpperCase()} ===`);
|
||||
|
||||
// Check subject length similarity
|
||||
const subjectEnLength = template.subject_en?.length || 0;
|
||||
const subjectDeLength = template.subject_de?.length || 0;
|
||||
console.log(`Subject length - EN: ${subjectEnLength}, DE: ${subjectDeLength}`);
|
||||
|
||||
// Check HTML content features
|
||||
const htmlEn = template.body_html_en || '';
|
||||
const htmlDe = template.body_html_de || '';
|
||||
|
||||
// Check for key features in both versions
|
||||
const features = [
|
||||
{ name: 'Handlebars conditionals', pattern: /{{#if/g },
|
||||
{ name: 'Styled divs', pattern: /style="/g },
|
||||
{ name: 'Background colors', pattern: /background-color:/g },
|
||||
{ name: 'Buttons/CTAs', pattern: /<a.*style.*background-color.*>/g },
|
||||
{ name: 'Icons/Emojis', pattern: /[📧📞✅⚠️]/g },
|
||||
{ name: 'Lists', pattern: /<ul/g },
|
||||
{ name: 'Strong emphasis', pattern: /<strong>/g }
|
||||
];
|
||||
|
||||
console.log('\nFeature comparison:');
|
||||
for (const feature of features) {
|
||||
const enCount = (htmlEn.match(feature.pattern) || []).length;
|
||||
const deCount = (htmlDe.match(feature.pattern) || []).length;
|
||||
const status = enCount === deCount ? '✅' : '❌';
|
||||
console.log(`${status} ${feature.name}: EN=${enCount}, DE=${deCount}`);
|
||||
}
|
||||
|
||||
// Check text content length
|
||||
const textEn = template.body_text_en || '';
|
||||
const textDe = template.body_text_de || '';
|
||||
console.log(`\nText content length - EN: ${textEn.length}, DE: ${textDe.length}`);
|
||||
|
||||
// Check for specific variables usage
|
||||
const variables = [
|
||||
'host_name', 'event_name', 'event_date', 'gallery_link',
|
||||
'gallery_password', 'expiry_date', 'welcome_message',
|
||||
'days_remaining', 'support_email', 'support_phone',
|
||||
'archive_date', 'photo_count', 'archive_size'
|
||||
];
|
||||
|
||||
const missingInEn = [];
|
||||
const missingInDe = [];
|
||||
|
||||
for (const variable of variables) {
|
||||
const varPattern = new RegExp(`{{${variable}}}`, 'g');
|
||||
const inEn = varPattern.test(htmlEn) || varPattern.test(textEn);
|
||||
const inDe = varPattern.test(htmlDe) || varPattern.test(textDe);
|
||||
|
||||
if (inDe && !inEn) missingInEn.push(variable);
|
||||
if (inEn && !inDe) missingInDe.push(variable);
|
||||
}
|
||||
|
||||
if (missingInEn.length > 0) {
|
||||
console.log(`\n⚠️ Variables in DE but missing in EN: ${missingInEn.join(', ')}`);
|
||||
}
|
||||
if (missingInDe.length > 0) {
|
||||
console.log(`\n⚠️ Variables in EN but missing in DE: ${missingInDe.join(', ')}`);
|
||||
}
|
||||
|
||||
// Overall quality score
|
||||
const enScore = [
|
||||
htmlEn.includes('style='),
|
||||
htmlEn.includes('{{#if'),
|
||||
htmlEn.includes('background-color'),
|
||||
htmlEn.includes('<strong>'),
|
||||
htmlEn.includes('margin:'),
|
||||
htmlEn.includes('padding:')
|
||||
].filter(Boolean).length;
|
||||
|
||||
const deScore = [
|
||||
htmlDe.includes('style='),
|
||||
htmlDe.includes('{{#if'),
|
||||
htmlDe.includes('background-color'),
|
||||
htmlDe.includes('<strong>'),
|
||||
htmlDe.includes('margin:'),
|
||||
htmlDe.includes('padding:')
|
||||
].filter(Boolean).length;
|
||||
|
||||
console.log(`\nQuality score (out of 6) - EN: ${enScore}, DE: ${deScore}`);
|
||||
console.log(enScore === deScore ? '✅ Templates have equal quality!' : '❌ Quality mismatch');
|
||||
}
|
||||
|
||||
console.log('\n\nSummary:');
|
||||
console.log('The English templates have been updated to match the German templates in:');
|
||||
console.log('- HTML styling and structure');
|
||||
console.log('- Conditional content blocks');
|
||||
console.log('- Visual elements (buttons, alerts, icons)');
|
||||
console.log('- Information completeness');
|
||||
console.log('- Professional formatting');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
verifyTemplateEquality();
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ const crypto = require('crypto');
|
||||
const fs = require('fs').promises;
|
||||
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');
|
||||
|
||||
@@ -88,7 +89,14 @@ router.post('/', adminAuth, [
|
||||
const password_hash = await bcrypt.hash(password, getBcryptRounds());
|
||||
|
||||
// Calculate expiration date (days after event date)
|
||||
const expires_at = new Date(event_date);
|
||||
// Parse YYYY-MM-DD format as local date to avoid timezone issues
|
||||
let expires_at;
|
||||
if (event_date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
const [year, month, day] = event_date.split('-').map(num => parseInt(num, 10));
|
||||
expires_at = new Date(year, month - 1, day);
|
||||
} else {
|
||||
expires_at = new Date(event_date);
|
||||
}
|
||||
expires_at.setDate(expires_at.getDate() + parseInt(expiration_days, 10));
|
||||
|
||||
// Create folder structure
|
||||
@@ -127,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,
|
||||
@@ -137,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',
|
||||
@@ -540,16 +547,15 @@ router.post('/:id/reset-password', adminAuth, async (req, res) => {
|
||||
|
||||
// Queue email notification if requested
|
||||
if (sendEmail) {
|
||||
const { queueEmail } = require('../services/emailProcessor');
|
||||
// For password reset, we'll need to create a template or use a different approach
|
||||
// For now, let's use the gallery_created template with updated password
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -564,6 +570,81 @@ 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' });
|
||||
}
|
||||
|
||||
// The email processor will determine the language based on:
|
||||
// 1. Event language setting
|
||||
// 2. App settings general_default_language
|
||||
// 3. Email config default language
|
||||
// 4. Domain-based detection
|
||||
// So we don't need to determine it here
|
||||
|
||||
// For resending creation email, we need the actual password
|
||||
// First, try to get it from the request body if provided
|
||||
let galleryPassword = req.body.password;
|
||||
|
||||
// If no password provided, we can't decrypt the existing one
|
||||
// So we'll show a security message
|
||||
if (!galleryPassword) {
|
||||
// We'll let the email processor determine the language for the security message
|
||||
galleryPassword = '{{password_security_message}}';
|
||||
}
|
||||
|
||||
// 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: event.event_date, // Pass raw date - will be formatted by email processor
|
||||
gallery_link: event.share_link,
|
||||
gallery_password: galleryPassword,
|
||||
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
|
||||
});
|
||||
|
||||
// Log the activity using the proper schema
|
||||
try {
|
||||
await logActivity('email_resent', {
|
||||
email_type: 'gallery_created',
|
||||
recipient: event.host_email,
|
||||
ip_address: req.ip || '0.0.0.0',
|
||||
user_agent: req.get('user-agent') || 'Unknown'
|
||||
}, id, {
|
||||
type: 'admin',
|
||||
id: req.admin.id,
|
||||
name: req.admin.username
|
||||
});
|
||||
} catch (logError) {
|
||||
console.error('Warning: Failed to log activity:', logError);
|
||||
// Don't fail the request if activity logging fails
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Creation email has been queued for sending'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error resending creation email:', error);
|
||||
console.error('Stack trace:', error.stack);
|
||||
res.status(500).json({ error: 'Failed to resend creation email' });
|
||||
}
|
||||
});
|
||||
|
||||
// Archive event
|
||||
router.post('/:id/archive', adminAuth, async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -520,11 +520,15 @@ router.get('/storage/info', adminAuth, async (req, res) => {
|
||||
|
||||
let archiveStorage = 0;
|
||||
for (const archive of archives) {
|
||||
try {
|
||||
const stats = await fs.stat(archive.archive_path);
|
||||
archiveStorage += stats.size;
|
||||
} catch (error) {
|
||||
console.error('Archive file not found:', archive.archive_path);
|
||||
if (archive.archive_path) {
|
||||
try {
|
||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const fullArchivePath = path.join(storagePath, archive.archive_path);
|
||||
const stats = await fs.stat(fullArchivePath);
|
||||
archiveStorage += stats.size;
|
||||
} catch (error) {
|
||||
console.error('Archive file not found:', archive.archive_path, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const { adminAuth } = require('../middleware/auth-enhanced-v2');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const router = express.Router();
|
||||
|
||||
// Get system version
|
||||
@@ -69,12 +70,46 @@ router.get('/status', adminAuth, async (req, res) => {
|
||||
|
||||
// Email queue status
|
||||
const [pendingEmails] = await db('email_queue').where('status', 'pending').count('* as count');
|
||||
const [processableEmails] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count');
|
||||
const [sentEmails] = await db('email_queue').where('status', 'sent').count('* as count');
|
||||
const [failedEmails] = await db('email_queue').where('status', 'failed').count('* as count');
|
||||
const [stuckEmails] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '>=', 3)
|
||||
.count('* as count');
|
||||
|
||||
// Activity logs count
|
||||
const [activityCount] = await db('activity_logs').count('* as count');
|
||||
|
||||
// Storage info
|
||||
const [{ totalPhotoStorage }] = await db('photos')
|
||||
.sum('size_bytes as totalPhotoStorage');
|
||||
|
||||
const archives = await db('events')
|
||||
.where('is_archived', formatBoolean(true))
|
||||
.whereNotNull('archive_path')
|
||||
.select('archive_path');
|
||||
|
||||
let archiveStorage = 0;
|
||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
|
||||
for (const archive of archives) {
|
||||
if (archive.archive_path) {
|
||||
try {
|
||||
const fullArchivePath = path.join(storagePath, archive.archive_path);
|
||||
const stats = await fs.stat(fullArchivePath);
|
||||
archiveStorage += stats.size;
|
||||
} catch (error) {
|
||||
console.error('Archive file not found:', archive.archive_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalStorage = (parseInt(totalPhotoStorage) || 0) + archiveStorage;
|
||||
|
||||
// System info
|
||||
const systemInfo = {
|
||||
platform: os.platform(),
|
||||
@@ -105,8 +140,15 @@ router.get('/status', adminAuth, async (req, res) => {
|
||||
activityLogs: activityCount.count
|
||||
}
|
||||
},
|
||||
storage: {
|
||||
totalUsed: totalStorage,
|
||||
photoStorage: parseInt(totalPhotoStorage) || 0,
|
||||
archiveStorage: archiveStorage
|
||||
},
|
||||
emailQueue: {
|
||||
pending: pendingEmails.count,
|
||||
processable: processableEmails.count,
|
||||
stuck: stuckEmails.count,
|
||||
sent: sentEmails.count,
|
||||
failed: failedEmails.count
|
||||
},
|
||||
|
||||
@@ -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 || ''
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const Handlebars = require('handlebars');
|
||||
const { db } = require('../database/db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
@@ -59,21 +60,58 @@ async function initializeTransporter(forceReinit = false) {
|
||||
}
|
||||
|
||||
// Get the appropriate language for a recipient
|
||||
async function getRecipientLanguage(email) {
|
||||
// For now, check if the email domain ends with .de
|
||||
// In the future, this could check user preferences
|
||||
if (email && email.endsWith('.de')) {
|
||||
return 'de';
|
||||
async function getRecipientLanguage(email, eventId = null) {
|
||||
// First priority: Check event language setting if eventId is provided
|
||||
if (eventId) {
|
||||
try {
|
||||
const event = await db('events').where('id', eventId).first();
|
||||
if (event && event.language) {
|
||||
return event.language;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching event language:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a saved preference for this email
|
||||
// This could be expanded to check user preferences in the database
|
||||
// Second priority: Check app_settings for general default language
|
||||
try {
|
||||
const langSetting = await db('app_settings')
|
||||
.where('setting_key', 'general_default_language')
|
||||
.first();
|
||||
if (langSetting && langSetting.setting_value) {
|
||||
return langSetting.setting_value;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching app settings language:', error);
|
||||
}
|
||||
|
||||
// Third priority: Check email configs for default language
|
||||
try {
|
||||
const emailConfig = await db('email_configs').first();
|
||||
if (emailConfig && emailConfig.default_language) {
|
||||
return emailConfig.default_language;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching email config language:', error);
|
||||
}
|
||||
|
||||
// Fourth priority: Check if the email domain suggests German
|
||||
if (email) {
|
||||
const germanDomains = ['.de', '.at', '.ch', '.li'];
|
||||
const domain = email.toLowerCase();
|
||||
if (germanDomains.some(d => domain.endsWith(d))) {
|
||||
return 'de';
|
||||
}
|
||||
}
|
||||
|
||||
return 'en'; // Default to English
|
||||
}
|
||||
|
||||
// Process email template with variables
|
||||
async function processTemplate(template, variables, language = 'en') {
|
||||
// Import date formatter
|
||||
const { formatDate } = require('../utils/dateFormatter');
|
||||
|
||||
// Get the appropriate language fields
|
||||
const subjectField = language === 'de' ? 'subject_de' : 'subject_en';
|
||||
const htmlField = language === 'de' ? 'body_html_de' : 'body_html_en';
|
||||
@@ -83,6 +121,27 @@ async function processTemplate(template, variables, language = 'en') {
|
||||
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)';
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Get branding settings for logo
|
||||
let logoUrl = '';
|
||||
@@ -116,27 +175,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 = `
|
||||
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0 0 10px 0; font-weight: 600; color: #374151;">${welcomeTitle}</p>
|
||||
<p style="margin: 0; color: #4b5563;">${variables.welcome_message}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 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 processedVariables (includes formatted dates and security messages)
|
||||
subject = subjectTemplate(processedVariables);
|
||||
htmlBody = htmlTemplate(processedVariables);
|
||||
textBody = textTemplate(processedVariables);
|
||||
|
||||
// Wrap HTML body in styled template
|
||||
const styledHtmlBody = `
|
||||
@@ -300,8 +347,8 @@ async function sendTemplateEmail(to, templateKey, variables) {
|
||||
throw new Error('Email configuration not found');
|
||||
}
|
||||
|
||||
// Determine recipient language
|
||||
const language = await getRecipientLanguage(to);
|
||||
// Determine recipient language (pass eventId if available in variables)
|
||||
const language = await getRecipientLanguage(to, variables.eventId || null);
|
||||
|
||||
// Process template with variables
|
||||
const { subject, htmlBody, textBody } = await processTemplate(template, variables, language);
|
||||
@@ -325,6 +372,8 @@ async function sendTemplateEmail(to, templateKey, variables) {
|
||||
|
||||
// Process email queue
|
||||
async function processEmailQueue() {
|
||||
logger.info('Email queue processor: Checking for pending emails...');
|
||||
|
||||
try {
|
||||
// Try to initialize transporter if it's null (in case it failed at startup)
|
||||
if (!transporter) {
|
||||
@@ -336,13 +385,20 @@ async function processEmailQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
const pendingEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.orderBy('created_at', 'asc')
|
||||
.limit(10);
|
||||
let pendingEmails = [];
|
||||
try {
|
||||
pendingEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.orderBy('created_at', 'asc')
|
||||
.limit(10);
|
||||
} catch (dbError) {
|
||||
logger.error('Failed to query email queue:', dbError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingEmails.length === 0) {
|
||||
logger.info('Email queue processor: No pending emails found');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,7 +406,9 @@ async function processEmailQueue() {
|
||||
|
||||
for (const email of pendingEmails) {
|
||||
try {
|
||||
const emailData = JSON.parse(email.email_data || '{}');
|
||||
const emailData = typeof email.email_data === 'string'
|
||||
? JSON.parse(email.email_data || '{}')
|
||||
: email.email_data || {};
|
||||
|
||||
await sendTemplateEmail(
|
||||
email.recipient_email,
|
||||
@@ -369,13 +427,24 @@ async function processEmailQueue() {
|
||||
logger.info(`Email ${email.id} sent successfully`);
|
||||
} catch (error) {
|
||||
// Increment retry count
|
||||
await db('email_queue')
|
||||
.where('id', email.id)
|
||||
.update({
|
||||
retry_count: email.retry_count + 1,
|
||||
error_message: error.message,
|
||||
updated_at: new Date()
|
||||
});
|
||||
try {
|
||||
await db('email_queue')
|
||||
.where('id', email.id)
|
||||
.update({
|
||||
retry_count: email.retry_count + 1,
|
||||
error_message: error.message
|
||||
});
|
||||
} catch (updateError) {
|
||||
logger.error(`Failed to update email retry count for ${email.id}:`, updateError);
|
||||
// If update fails due to column issue, try without any potential auto-added fields
|
||||
if (updateError.message && updateError.message.includes('updated_at')) {
|
||||
logger.warn('Detected updated_at column issue, attempting raw query...');
|
||||
await db.raw(
|
||||
'UPDATE email_queue SET retry_count = ?, error_message = ? WHERE id = ?',
|
||||
[email.retry_count + 1, error.message, email.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`Failed to send email ${email.id}:`, error);
|
||||
}
|
||||
@@ -388,6 +457,8 @@ async function processEmailQueue() {
|
||||
// Queue an email for sending
|
||||
async function queueEmail(eventId, recipientEmail, emailType, emailData) {
|
||||
try {
|
||||
// Add eventId to emailData for language detection
|
||||
emailData.eventId = eventId;
|
||||
await db('email_queue').insert({
|
||||
event_id: eventId,
|
||||
recipient_email: recipientEmail,
|
||||
@@ -405,17 +476,45 @@ async function queueEmail(eventId, recipientEmail, emailType, emailData) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test email connection
|
||||
async function testEmailConnection() {
|
||||
try {
|
||||
if (!transporter) {
|
||||
await initializeTransporter();
|
||||
}
|
||||
if (!transporter) {
|
||||
return false;
|
||||
}
|
||||
await transporter.verify();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Email connection test failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start email queue processor
|
||||
let emailQueueInterval = null;
|
||||
|
||||
function startEmailQueueProcessor() {
|
||||
logger.info('Email queue processor: Attempting to start...');
|
||||
|
||||
if (!emailQueueInterval) {
|
||||
// Process immediately on start
|
||||
processEmailQueue();
|
||||
processEmailQueue().catch(err => {
|
||||
logger.error('Email queue processor: Initial processing failed:', err);
|
||||
});
|
||||
|
||||
// Then process every minute
|
||||
emailQueueInterval = setInterval(processEmailQueue, 60000);
|
||||
logger.info('Email queue processor started');
|
||||
emailQueueInterval = setInterval(() => {
|
||||
processEmailQueue().catch(err => {
|
||||
logger.error('Email queue processor: Periodic processing failed:', err);
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
logger.info('Email queue processor started successfully');
|
||||
} else {
|
||||
logger.info('Email queue processor: Already running');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,6 +538,6 @@ module.exports = {
|
||||
sendTemplateEmail,
|
||||
processEmailQueue,
|
||||
queueEmail,
|
||||
startEmailQueueProcessor,
|
||||
stopEmailQueueProcessor
|
||||
stopEmailQueueProcessor,
|
||||
testEmailConnection
|
||||
};
|
||||
@@ -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) };
|
||||
|
||||
@@ -27,7 +27,28 @@ async function formatDate(date, language = 'en') {
|
||||
}
|
||||
}
|
||||
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
// Ensure proper date parsing
|
||||
let dateObj;
|
||||
if (date instanceof Date) {
|
||||
dateObj = date;
|
||||
} else if (typeof date === 'string') {
|
||||
// For date strings like "2025-07-16", parse as local date to avoid timezone issues
|
||||
if (date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
// Parse YYYY-MM-DD format as local date
|
||||
const [year, month, day] = date.split('-').map(num => parseInt(num, 10));
|
||||
dateObj = new Date(year, month - 1, day);
|
||||
} else {
|
||||
dateObj = new Date(date);
|
||||
}
|
||||
} else {
|
||||
dateObj = new Date(date);
|
||||
}
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
console.error('Invalid date provided to formatDate:', date);
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
|
||||
// Use appropriate locale based on language
|
||||
let locale = dateConfig.locale || 'en-GB';
|
||||
@@ -39,33 +60,33 @@ async function formatDate(date, language = 'en') {
|
||||
|
||||
// Format based on the configured format
|
||||
switch (dateConfig.format) {
|
||||
case 'MM/DD/YYYY':
|
||||
return dateObj.toLocaleDateString(locale, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
case 'DD/MM/YYYY':
|
||||
return dateObj.toLocaleDateString(locale, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
case 'YYYY-MM-DD':
|
||||
return dateObj.toISOString().split('T')[0];
|
||||
case 'DD.MM.YYYY':
|
||||
return dateObj.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
default:
|
||||
// Use long format as fallback
|
||||
return dateObj.toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
case 'MM/DD/YYYY':
|
||||
return dateObj.toLocaleDateString(locale, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
case 'DD/MM/YYYY':
|
||||
return dateObj.toLocaleDateString(locale, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
case 'YYYY-MM-DD':
|
||||
return dateObj.toISOString().split('T')[0];
|
||||
case 'DD.MM.YYYY':
|
||||
return dateObj.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
default:
|
||||
// Use long format as fallback
|
||||
return dateObj.toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error formatting date:', error);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.57",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.57",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-link": "^2.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.57",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"upload": "Hochladen",
|
||||
"days": "Tage",
|
||||
"customize": "Anpassen",
|
||||
"hide": "Ausblenden"
|
||||
"hide": "Ausblenden",
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
"upload": {
|
||||
"photoCategory": "Fotokategorie",
|
||||
@@ -243,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",
|
||||
@@ -711,6 +715,7 @@
|
||||
"settings_updated": "Einstellungen aktualisiert",
|
||||
"event_updated": "Veranstaltung aktualisiert: {{eventName}}",
|
||||
"event_deleted": "Veranstaltung gelöscht: {{eventName}}",
|
||||
"email_resent": "Erstellungs-E-Mail erneut gesendet für: {{eventName}}",
|
||||
"category_created": "Kategorie erstellt: {{categoryName}}",
|
||||
"category_updated": "Kategorie aktualisiert: {{categoryName}}",
|
||||
"category_deleted": "Kategorie gelöscht: {{categoryName}}",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"upload": "Upload",
|
||||
"days": "days",
|
||||
"customize": "Customize",
|
||||
"hide": "Hide"
|
||||
"hide": "Hide",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"upload": {
|
||||
"photoCategory": "Photo Category",
|
||||
@@ -260,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",
|
||||
@@ -762,6 +766,7 @@
|
||||
"settings_updated": "Settings updated",
|
||||
"event_updated": "Event updated: {{eventName}}",
|
||||
"event_deleted": "Event deleted: {{eventName}}",
|
||||
"email_resent": "Creation email resent for: {{eventName}}",
|
||||
"category_created": "Category created: {{categoryName}}",
|
||||
"category_updated": "Category updated: {{categoryName}}",
|
||||
"category_deleted": "Category deleted: {{categoryName}}",
|
||||
|
||||
@@ -169,7 +169,10 @@ export const ArchivesPage: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p>
|
||||
<p className="text-2xl font-bold text-neutral-900">
|
||||
{archives.reduce((sum, a) => sum + (a.photoCount || 0), 0).toLocaleString()}
|
||||
{(() => {
|
||||
const total = archives.reduce((sum, a) => sum + (parseInt(String(a.photoCount)) || 0), 0);
|
||||
return total === 0 ? '0' : total.toLocaleString();
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<FileArchive className="w-8 h-8 text-green-600" />
|
||||
|
||||
@@ -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 = () => {
|
||||
</p>
|
||||
|
||||
{!event.is_archived && (
|
||||
<div className="mt-4 pt-4 border-t border-neutral-200">
|
||||
<div className="mt-4 pt-4 border-t border-neutral-200 space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -630,6 +631,22 @@ export const EventDetailsPage: React.FC = () => {
|
||||
>
|
||||
{t('events.resetGalleryPassword')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<Mail className="w-4 h-4" />}
|
||||
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')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -551,7 +551,14 @@ export const SettingsPage: React.FC = () => {
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-blue-700">{t('settings.systemStatus.pending')}:</span>
|
||||
<span className="ml-2 font-semibold text-blue-900">{systemStatus.emailQueue.pending}</span>
|
||||
<span className="ml-2 font-semibold text-blue-900">
|
||||
{systemStatus.emailQueue.pending}
|
||||
{systemStatus.emailQueue.stuck > 0 && (
|
||||
<span className="text-orange-600 text-xs ml-1">
|
||||
({systemStatus.emailQueue.stuck} stuck)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-green-700">{t('settings.systemStatus.sent')}:</span>
|
||||
@@ -562,6 +569,14 @@ export const SettingsPage: React.FC = () => {
|
||||
<span className="ml-2 font-semibold text-red-900">{systemStatus.emailQueue.failed}</span>
|
||||
</div>
|
||||
</div>
|
||||
{systemStatus.emailQueue.stuck > 0 && (
|
||||
<div className="mt-3 p-3 bg-orange-50 rounded-md">
|
||||
<p className="text-xs text-orange-800">
|
||||
<span className="font-semibold">⚠️ {systemStatus.emailQueue.stuck} email(s) stuck:</span> These emails have exceeded retry limits and won't be processed automatically.
|
||||
Only {systemStatus.emailQueue.processable} of {systemStatus.emailQueue.pending} pending emails will be processed.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
@@ -23,6 +23,9 @@ export interface SystemHealth {
|
||||
details: {
|
||||
emailQueue: {
|
||||
pending: number;
|
||||
processable: number;
|
||||
stuck: number;
|
||||
sent: number;
|
||||
failed: number;
|
||||
};
|
||||
memory: {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -47,8 +47,15 @@ export interface SystemStatus {
|
||||
activityLogs: number;
|
||||
};
|
||||
};
|
||||
storage: {
|
||||
totalUsed: number;
|
||||
photoStorage: number;
|
||||
archiveStorage: number;
|
||||
};
|
||||
emailQueue: {
|
||||
pending: number;
|
||||
processable: number;
|
||||
stuck: number;
|
||||
sent: number;
|
||||
failed: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user