diff --git a/src/app/(payload)/api/newsletter/subscribe/route.ts b/src/app/(payload)/api/newsletter/subscribe/route.ts index 1566b08..0f0adb2 100644 --- a/src/app/(payload)/api/newsletter/subscribe/route.ts +++ b/src/app/(payload)/api/newsletter/subscribe/route.ts @@ -5,7 +5,7 @@ import { getPayload } from 'payload' import config from '@payload-config' import { createNewsletterService } from '@/lib/email/newsletter-service' import { getTenantFromRequest } from '@/lib/email/tenant-email-service' -import { rateLimiters } from '@/lib/security/rate-limiter' +import { formLimiter } from '@/lib/security/rate-limiter' /** * POST /api/newsletter/subscribe @@ -28,7 +28,7 @@ export async function POST(request: Request): Promise { request.headers.get('x-real-ip') || 'unknown' - const rateLimitResult = rateLimiters.form.check(clientIp) + const rateLimitResult = await formLimiter.check(clientIp) if (!rateLimitResult.allowed) { return NextResponse.json( { @@ -38,7 +38,7 @@ export async function POST(request: Request): Promise { { status: 429, headers: { - 'Retry-After': String(Math.ceil(rateLimitResult.retryAfter / 1000)), + 'Retry-After': String(rateLimitResult.retryAfter || 60), }, }, ) diff --git a/src/hooks/sendNewsletterConfirmation.ts b/src/hooks/sendNewsletterConfirmation.ts index 99ef4e2..7820034 100644 --- a/src/hooks/sendNewsletterConfirmation.ts +++ b/src/hooks/sendNewsletterConfirmation.ts @@ -13,14 +13,25 @@ import { * 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 gesendet wurde. + * 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 = 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 = @@ -74,7 +85,8 @@ export const sendNewsletterConfirmation: CollectionAfterChangeHook { const tenant = await this.getTenant(tenantId) - const templateData = this.buildTemplateData(tenant, subscriber) + // Immer ID für Unsubscribe verwenden, da Token nach Bestätigung null ist + const templateData = this.buildTemplateData(tenant, subscriber, { useIdForUnsubscribe: true }) await sendTenantEmail(this.payload, tenantId, { to: subscriber.email, @@ -384,13 +390,15 @@ export class NewsletterService { /** * Abmelde-Bestätigung senden + * Verwendet die Subscriber-ID für Links, da kein Token mehr benötigt wird. */ private async sendUnsubscribeEmail( tenantId: number, subscriber: NewsletterSubscriber, ): Promise { const tenant = await this.getTenant(tenantId) - const templateData = this.buildTemplateData(tenant, subscriber) + // ID für Links verwenden + const templateData = this.buildTemplateData(tenant, subscriber, { useIdForUnsubscribe: true }) await sendTenantEmail(this.payload, tenantId, { to: subscriber.email, @@ -419,10 +427,15 @@ export class NewsletterService { /** * Template-Daten zusammenstellen + * + * Für Confirmation-E-Mails wird der Token verwendet. + * Für Willkommens- und andere E-Mails wird immer die ID verwendet, + * da der Token nach Bestätigung gelöscht wird. */ private buildTemplateData( tenant: Tenant, subscriber: NewsletterSubscriber, + options?: { useIdForUnsubscribe?: boolean }, ): NewsletterTemplateData { // Tenant-Website URL ermitteln const tenantWebsite = tenant.domains?.[0]?.domain @@ -434,11 +447,17 @@ export class NewsletterService { ? `${tenantWebsite}/datenschutz` : undefined + // Für Unsubscribe immer ID verwenden wenn kein Token vorhanden + // oder wenn explizit angefordert (z.B. für Willkommens-E-Mail nach Bestätigung) + const unsubscribeToken = options?.useIdForUnsubscribe || !subscriber.confirmationToken + ? String(subscriber.id) + : subscriber.confirmationToken + return { firstName: subscriber.firstName || undefined, email: subscriber.email, confirmationUrl: `${this.baseUrl}/api/newsletter/confirm?token=${subscriber.confirmationToken}`, - unsubscribeUrl: `${this.baseUrl}/api/newsletter/unsubscribe?token=${subscriber.confirmationToken || subscriber.id}`, + unsubscribeUrl: `${this.baseUrl}/api/newsletter/unsubscribe?token=${unsubscribeToken}`, tenantName: tenant.name, tenantWebsite, privacyPolicyUrl,