cms.c2sgmbh/src/hooks/sendNewsletterConfirmation.ts
Martin Porwoll 411f1a040e fix: newsletter double opt-in bug fixes
- Fix rate limiter: await formLimiter.check() (was missing await)
- Prevent duplicate confirmation emails: add context.skipNewsletterEmail flag
  - Service sets flag when creating/updating subscribers via API
  - Hook skips email sending when flag is present
  - Admin panel creations still trigger the hook
- Fix unsubscribe links: use subscriber ID instead of token for welcome/unsubscribe emails
  - Token is nullified after confirmation, making old links invalid
  - ID-based lookups always work

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 20:17:28 +00:00

119 lines
3.7 KiB
TypeScript

// src/hooks/sendNewsletterConfirmation.ts
import type { CollectionAfterChangeHook } from 'payload'
import type { NewsletterSubscriber, Tenant } from '../payload-types'
import { sendTenantEmail } from '../lib/email/tenant-email-service'
import {
getConfirmationEmailHtml,
getConfirmationEmailText,
type NewsletterTemplateData,
} from '../lib/email/newsletter-templates'
/**
* Hook: Sendet automatisch Double Opt-In E-Mail bei neuen Newsletter-Anmeldungen
*
* Wird nur bei neuen Subscribern mit Status "pending" ausgeführt.
* Vermeidet doppelten Versand durch Prüfung ob E-Mail bereits über API/Service gesendet wurde.
*
* Der Hook wird NICHT ausgeführt wenn:
* - Die Erstellung via overrideAccess erfolgt (Newsletter-Service nutzt dies)
* - Ein Kontext-Flag gesetzt ist
*/
export const sendNewsletterConfirmation: CollectionAfterChangeHook<NewsletterSubscriber> = async ({
doc,
operation,
req,
previousDoc,
context,
}) => {
// Skip wenn via Newsletter-Service erstellt (verwendet overrideAccess)
// Der Service sendet die E-Mail selbst
if (context?.skipNewsletterEmail) {
return doc
}
// Nur bei neuen Anmeldungen (create) oder wenn Status auf "pending" geändert wird
const isNew = operation === 'create'
const isResubscribe =
operation === 'update' &&
previousDoc?.status === 'unsubscribed' &&
doc.status === 'pending'
if (!isNew && !isResubscribe) {
return doc
}
// Nur bei Status "pending" (Double Opt-In ausstehend)
if (doc.status !== 'pending') {
return doc
}
// Prüfen ob Token vorhanden
if (!doc.confirmationToken) {
console.warn('[Newsletter] No confirmation token found for subscriber:', doc.id)
return doc
}
// Tenant-ID ermitteln
const tenantId = typeof doc.tenant === 'object' ? doc.tenant?.id : doc.tenant
if (!tenantId) {
console.warn('[Newsletter] No tenant found for subscriber:', doc.id)
return doc
}
try {
// Tenant laden
const tenant = (await req.payload.findByID({
collection: 'tenants',
id: tenantId,
depth: 0,
})) as Tenant
if (!tenant) {
console.error('[Newsletter] Tenant not found:', tenantId)
return doc
}
// Template-Daten zusammenstellen
const baseUrl = process.env.PAYLOAD_PUBLIC_SERVER_URL || 'https://pl.c2sgmbh.de'
const tenantWebsite = tenant.domains?.[0]?.domain
? `https://${tenant.domains[0].domain}`
: undefined
const templateData: NewsletterTemplateData = {
firstName: doc.firstName || undefined,
email: doc.email,
confirmationUrl: `${baseUrl}/api/newsletter/confirm?token=${doc.confirmationToken}`,
// Für Confirmation-E-Mail Token verwenden (noch gültig), aber ID als Fallback
unsubscribeUrl: `${baseUrl}/api/newsletter/unsubscribe?token=${doc.confirmationToken || doc.id}`,
tenantName: tenant.name,
tenantWebsite,
privacyPolicyUrl: tenantWebsite ? `${tenantWebsite}/datenschutz` : undefined,
}
// Double Opt-In E-Mail senden
const result = await sendTenantEmail(req.payload, tenantId, {
to: doc.email,
subject: `Newsletter-Anmeldung bestätigen - ${tenant.name}`,
html: getConfirmationEmailHtml(templateData),
text: getConfirmationEmailText(templateData),
source: 'newsletter',
metadata: {
type: 'double-opt-in',
subscriberId: doc.id,
triggeredBy: 'hook',
},
})
if (result.success) {
console.log(`[Newsletter] Confirmation email sent to ${doc.email}`)
} else {
console.error(`[Newsletter] Failed to send confirmation email to ${doc.email}:`, result.error)
}
} catch (error) {
console.error('[Newsletter] Error sending confirmation email:', error)
}
return doc
}