import { NextResponse } from "next/server"; import { sendeFlyerBenachrichtigung, sendeFlyerLink } from "@/lib/mailer"; import { createServiceClient } from "@/lib/supabase"; const FLYER_DOWNLOAD_URL = "/downloads/MBO_FamilyGuard_Flyer_01.pdf"; const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; const RATE_LIMIT_MAX = 3; const requestLog = new Map(); let cleanupCounter = 0; const CLEANUP_INTERVAL = 500; function isRateLimited(key: string): boolean { const now = Date.now(); cleanupCounter++; if (cleanupCounter >= CLEANUP_INTERVAL) { cleanupCounter = 0; for (const [k, timestamps] of requestLog) { const fresh = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS); if (fresh.length === 0) requestLog.delete(k); else requestLog.set(k, fresh); } } const timestamps = (requestLog.get(key) ?? []).filter( (t) => now - t < RATE_LIMIT_WINDOW_MS ); if (timestamps.length >= RATE_LIMIT_MAX) { requestLog.set(key, timestamps); return true; } timestamps.push(now); requestLog.set(key, timestamps); return false; } // Nimmt an, dass genau ein Reverse-Proxy vorgeschaltet ist, der die beobachtete // Peer-IP anhängt (nicht ersetzt), und dass die App nie unter Umgehung dieses // Proxys direkt erreichbar ist — falls das nicht zutrifft, fällt der Limiter auf // den gemeinsamen "unknown"-Bucket zurück, wird aber nicht umgehbar. function getClientIp(request: Request): string { const forwarded = request.headers.get("x-forwarded-for"); if (forwarded) { const parts = forwarded.split(",").map((p) => p.trim()).filter(Boolean); if (parts.length > 0) return parts[parts.length - 1]; } return "unknown"; } function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } export async function POST(request: Request) { if (isRateLimited(`ip:${getClientIp(request)}`)) { return NextResponse.json( { ok: false, error: "Zu viele Anfragen. Bitte versuchen Sie es später erneut." }, { status: 429 } ); } let body: { email?: string; dsgvoEinwilligung?: boolean }; try { body = await request.json(); } catch { return NextResponse.json({ ok: false, error: "Ungültige Anfrage" }, { status: 400 }); } const email = typeof body.email === "string" ? body.email.trim() : ""; if (!isValidEmail(email)) { return NextResponse.json( { ok: false, error: "Bitte geben Sie eine gültige E-Mail-Adresse ein" }, { status: 400 } ); } if (isRateLimited(`email:${email}`)) { return NextResponse.json( { ok: false, error: "Zu viele Anfragen. Bitte versuchen Sie es später erneut." }, { status: 429 } ); } if (body.dsgvoEinwilligung !== true) { return NextResponse.json( { ok: false, error: "Bitte stimmen Sie der Datenverarbeitung zu" }, { status: 400 } ); } try { const db = createServiceClient(); await db.from("flyer_downloads").insert({ email, dsgvo_einwilligung: true }); } catch (err) { console.error("[FamilyGuard-Flyer] Supabase insert error:", err); } await sendeFlyerBenachrichtigung({ email }); sendeFlyerLink({ email }).catch((err) => console.error("[FamilyGuard-Flyer] Flyer-Mail an Interessenten fehlgeschlagen:", err) ); return NextResponse.json({ ok: true, downloadUrl: FLYER_DOWNLOAD_URL }); }