Compare commits

..

24 Commits

Author SHA1 Message Date
Gitea Actions Bot 5ca598b80a chore: bump backend version to 1.0.59
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 21:19:09 +00:00
paul 4e2075c638 fix: prevent double date formatting in email templates
Mirror to GitHub / mirror (push) Successful in 25s
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
- Remove pre-formatting of dates before passing to email processor
- Let email processor handle all date formatting based on recipient language
- Fix Invalid Date errors by passing raw date values instead of formatted strings
- Remove unused formatDate imports and variables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 23:14:32 +02:00
Gitea Actions Bot 0a691d4251 chore: bump backend version to 1.0.58
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 20:52:25 +00:00
paul 34846ae71a fix: resolve Invalid Date issue in email templates
Mirror to GitHub / mirror (push) Successful in 20s
Test and Lint / backend-test (push) Successful in 1m0s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
- Fix date parsing for YYYY-MM-DD format to use local timezone
- Handle date strings properly to avoid timezone conversion issues
- Add validation to ensure dates are valid before formatting
- Update expiration date calculation to use consistent parsing

This fixes the "Invalid Date" display in gallery creation emails.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 22:43:33 +02:00
Gitea Actions Bot ea261dd03b chore: bump version to 1.0.57 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 15:37:55 +00:00
paul eb93223d79 fix: email resend language and password display issues
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
- Added missing translation for admin.activities.email_resent in both EN and DE
- Fixed language detection in resend email to use email processor's priority system
- Fixed password security message showing German text in English emails
- Updated email processor to handle password placeholder and format dates properly
- Dates are now formatted according to language and system settings
- Password security message now displays in the correct language based on recipient
2025-07-16 17:33:33 +02:00
Gitea Actions Bot b20f9cc108 chore: bump backend version to 1.0.56
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 15:22:56 +00:00
paul 515814e1d5 feat: update English email templates to match German quality and content
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 39s
Version and Release / trigger-drone (push) Successful in 3s
- Enhanced all English email templates with proper HTML styling
- Added conditional content blocks (Handlebars {{#if}} syntax)
- Added styled alert boxes for warnings and success messages
- Added action buttons with proper styling
- Added emoji icons for better visual communication
- Added missing variables: support_email, support_phone, archive_date, photo_count
- Matched professional tone and formatting from German templates
- Improved visual hierarchy with background colors and borders
- All templates now have equal or better quality scores
2025-07-16 17:18:19 +02:00
Gitea Actions Bot fba9838e21 chore: bump backend version to 1.0.55
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 14:52:07 +00:00
paul a9c2761986 fix: add language detection from app_settings and fix activity_logs schema
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m6s
Test and Lint / frontend-test (push) Successful in 2m23s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
continuous-integration/drone/push Build is passing
- Enhanced language detection priority in email processor:
  1. Event-specific language
  2. App settings general_default_language (NEW)
  3. Email config default_language
  4. Domain-based detection
- Fixed activity_logs insertion error by using logActivity function
- Fixed 500 error on resend email endpoint
- Emails now respect language selected in settings page
2025-07-16 16:47:04 +02:00
Gitea Actions Bot db7e5913eb chore: bump backend version to 1.0.54
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 14:44:54 +00:00
paul aa27d1ea79 fix: resolve 500 error on resend email endpoint
Mirror to GitHub / mirror (push) Successful in 31s
Test and Lint / backend-test (push) Successful in 1m16s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m16s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Moved queueEmail import to top of file (no more dynamic require)
- Fixed similar issue in password reset endpoint
- Added error handling for activity log insertion to prevent failures
- Added default values for ip_address and user_agent
- Added detailed error logging for debugging

The email was being sent successfully but the endpoint was returning 500
due to the dynamic require pattern. This is now fixed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 16:39:56 +02:00
Gitea Actions Bot cf8df2780e chore: bump version to 1.0.53 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 14:29:36 +00:00
paul 419a283c62 feat: add resend creation email button and fix Handlebars template processing
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
- Fixed email template processing to properly handle Handlebars conditionals
  - Installed handlebars package
  - Updated processTemplate to use Handlebars.compile()
  - Now {{#if welcome_message}} blocks work correctly

- Added "Resend Creation Email" button to event details page
  - New endpoint: POST /api/admin/events/:id/resend-email
  - Button appears below "Reset Gallery Password"
  - Shows mail icon and includes success/error notifications
  - For security, resent emails show "(Aus Sicherheitsgründen nicht angezeigt)"
    instead of the actual password

- Added translations:
  - EN: "Resend Creation Email", success/error messages
  - DE: "Erstellungs-E-Mail erneut senden", success/error messages

This fixes the issue where Handlebars syntax was showing in emails and provides
a convenient way to resend the initial gallery creation email to hosts.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 16:25:00 +02:00
Gitea Actions Bot 1b075a4beb chore: bump backend version to 1.0.52
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 14:01:47 +00:00
paul 68ff93cf17 feat: complete German email template implementation and language detection
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m3s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 38s
Version and Release / trigger-drone (push) Successful in 3s
- Added comprehensive German translations for all email templates:
  - gallery_created: Welcome email with proper German formatting
  - expiration_warning: Warning email with countdown in German
  - gallery_expired: Expiration notice in German
  - archive_complete: Archive completion notice in German

- Enhanced language detection with priority system:
  1. Event-specific language setting (new 'language' column)
  2. Global email config default language
  3. Domain-based detection (.de, .at, .ch, .li)

- Added language preferences:
  - events.language column for per-event language settings
  - email_configs.default_language for system-wide default
  - Set default to German ('de') for existing installations

- Fixed email processor to:
  - Pass eventId for proper language detection
  - Handle both SQLite and PostgreSQL JSON formats
  - Log language used for each email sent

All German templates are professionally translated with proper:
- Greetings ("Liebe(r)" instead of "Dear")
- Date formatting (DD.MM.YYYY)
- Professional closings ("Mit freundlichen Grüßen")
- UI elements ("Galerie-Details:" instead of "Gallery Details:")

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 15:57:20 +02:00
Gitea Actions Bot b22a29e877 chore: bump version to 1.0.51 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 13:10:04 +00:00
paul e601311ca3 fix: resolve email queue discrepancy between admin dashboard and processor
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
The issue: Admin dashboard showed pending emails that the processor wouldn't
process because they had exceeded retry limits (retry_count >= 3).

Changes:
- Updated backend /admin/system/status to provide detailed email queue stats:
  - pending: total pending emails (as before)
  - processable: emails that will actually be processed (retry_count < 3)
  - stuck: emails that exceeded retry limit but are still pending
- Updated frontend to display stuck emails with warning when present
- Created debug-email-queue.js script to diagnose discrepancies
- Created fix-stuck-emails.js script to handle stuck emails:
  - Can reset retry count, mark as failed, or delete
  - Usage: node fix-stuck-emails.js [reset|fail|delete]

This makes it clear when emails are stuck and won't be processed automatically.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 15:05:34 +02:00
Gitea Actions Bot bb00c3993b chore: bump version to 1.0.50 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 12:46:54 +00:00
paul c9c0de46bf fix: improve email processor reliability and add diagnostic tools
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m19s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 2s
- Enhanced email processor with comprehensive logging
- Added error handling for database queries in email processing
- Fixed duplicate export in module.exports
- Added testEmailConnection() function for connectivity testing
- Created diagnostic scripts:
  - check-email-processor.js: Full diagnostic with manual processing
  - run-email-processor.js: Standalone processor (can run once or continuously)
  - check-email-environment.js: Environment and configuration checker
- Fixed missing translation: added 'common.unknown' in EN/DE locales

This should help identify why emails aren't being processed on production.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 14:30:09 +02:00
Gitea Actions Bot 5374299cd5 chore: bump backend version to 1.0.49
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 09:34:18 +00:00
paul 536e2b2874 fix: handle pre-parsed JSON data in email processor
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m5s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m29s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 2s
The email_data column in PostgreSQL is a JSON type that auto-parses
the data, while SQLite stores it as a string. Updated the code to
check if email_data is already an object before attempting to parse.

This fixes the "Unexpected token o in JSON at position 1" error.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 11:29:36 +02:00
Gitea Actions Bot 141acd5736 chore: bump version to 1.0.48 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 09:17:20 +00:00
paul 5f4337a18d fix: resolve multiple production issues
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Fix archive page showing "00" instead of "0" for empty photo counts
- Fix email queue PostgreSQL "updated_at" column error with migration and fallback
- Fix storage calculation discrepancies between system status and archive pages
- Add proper storage info to system status endpoint
- Fix archive path concatenation in settings endpoint

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 11:12:32 +02:00
41 changed files with 2289 additions and 144 deletions
+3
View File
@@ -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}}.'
});
};
+49 -3
View File
@@ -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",
+2 -1
View File
@@ -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",
+131
View File
@@ -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);
});
+157
View File
@@ -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();
+66
View File
@@ -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();
+53
View File
@@ -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();
+146
View File
@@ -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();
+99
View File
@@ -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();
+126
View File
@@ -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();
+111
View File
@@ -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);
+85
View File
@@ -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();
+110
View File
@@ -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();
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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');
+1 -1
View File
@@ -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;
}
+9 -9
View File
@@ -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
};
+90 -9
View File
@@ -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 {
+9 -5
View File
@@ -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);
}
}
}
+42
View File
@@ -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
},
+2 -2
View File
@@ -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 || ''
});
+147 -48
View File
@@ -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
};
+24 -24
View File
@@ -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) };
+49 -28
View File
@@ -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);
+1 -1
View File
@@ -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
}
/**
+2 -2
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.47",
"version": "1.0.57",
"type": "module",
"scripts": {
"dev": "vite",
+6 -1
View File
@@ -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}}",
+6 -1
View File
@@ -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}}",
+4 -1
View File
@@ -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" />
+19 -2
View File
@@ -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>
+16 -1
View File
@@ -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>
</>
+3
View File
@@ -23,6 +23,9 @@ export interface SystemHealth {
details: {
emailQueue: {
pending: number;
processable: number;
stuck: number;
sent: number;
failed: number;
};
memory: {
+6
View File
@@ -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;
};