cms.c2sgmbh/src/app/(payload)/api/newsletter/subscribe/route.ts
Martin Porwoll 79577626e2 feat: add Newsletter Double Opt-In email system
- Add email templates for confirmation, welcome, and unsubscribe
- Create newsletter-service.ts with token validation and 48h expiry
- Add API endpoints: /api/newsletter/subscribe, /confirm, /unsubscribe
- Add afterChange hook for automatic email sending on subscription
- Rate-limiting: 5 subscriptions per 10 minutes per IP
- GDPR-compliant with re-subscription support after unsubscribe

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 16:37:16 +00:00

116 lines
3.1 KiB
TypeScript

// src/app/(payload)/api/newsletter/subscribe/route.ts
import { NextResponse } from 'next/server'
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'
/**
* POST /api/newsletter/subscribe
*
* Newsletter-Anmeldung mit Double Opt-In
*
* Body:
* - email (required): E-Mail-Adresse
* - firstName (optional): Vorname
* - lastName (optional): Nachname
* - interests (optional): Array von Interessen
* - source (optional): Anmeldequelle
* - tenantId (optional): Tenant-ID (falls nicht aus Request ermittelbar)
*/
export async function POST(request: Request): Promise<Response> {
try {
// Rate-Limiting (5 Anmeldungen pro 10 Minuten pro IP)
const clientIp =
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
request.headers.get('x-real-ip') ||
'unknown'
const rateLimitResult = rateLimiters.form.check(clientIp)
if (!rateLimitResult.allowed) {
return NextResponse.json(
{
success: false,
message: 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.',
},
{
status: 429,
headers: {
'Retry-After': String(Math.ceil(rateLimitResult.retryAfter / 1000)),
},
},
)
}
const payload = await getPayload({ config })
const body = await request.json()
// E-Mail validieren
const email = body.email?.trim()?.toLowerCase()
if (!email || !isValidEmail(email)) {
return NextResponse.json(
{
success: false,
message: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
},
{ status: 400 },
)
}
// Tenant ermitteln
let tenantId = body.tenantId
if (!tenantId) {
const tenant = await getTenantFromRequest(payload, request)
if (tenant) {
tenantId = tenant.id
}
}
if (!tenantId) {
return NextResponse.json(
{
success: false,
message: 'Tenant konnte nicht ermittelt werden.',
},
{ status: 400 },
)
}
// Newsletter-Service
const newsletterService = createNewsletterService(payload)
const result = await newsletterService.subscribe(tenantId, {
email,
firstName: body.firstName?.trim(),
lastName: body.lastName?.trim(),
interests: body.interests,
source: body.source || 'website',
ipAddress: clientIp,
userAgent: request.headers.get('user-agent') || undefined,
})
return NextResponse.json(result, {
status: result.success ? 200 : 400,
})
} catch (error) {
console.error('[Newsletter Subscribe] Error:', error)
return NextResponse.json(
{
success: false,
message: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
},
{ status: 500 },
)
}
}
/**
* E-Mail-Validierung
*/
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}