mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
- 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>
116 lines
3.1 KiB
TypeScript
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)
|
|
}
|