import { json } from "@sveltejs/kit"; import { env as publicEnv } from "$env/dynamic/public"; import { env as privateEnv } from "$env/dynamic/private"; import type { RequestHandler } from "./$types"; import { notifyMatrix } from "$lib/server/matrix"; import { HONEYPOT_FIELDS, LIMITS, validateContact, } from "$lib/components/internal/contact-form-validation"; const TURNSTILE_SECRET = privateEnv.TURNSTILE_SECRET ?? ""; const TURNSTILE_ENABLED = TURNSTILE_SECRET !== ""; async function verifyTurnstile(token: string, ip: string): Promise { if (!TURNSTILE_ENABLED) return true; if (!token) return false; try { const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ secret: TURNSTILE_SECRET, response: token, remoteip: ip }), }); if (!res.ok) return false; const data = (await res.json()) as { success?: boolean }; return data.success === true; } catch (e) { console.error("[contact] turnstile verify failed:", e); return false; } } const ALLOWED_ORIGINS = new Set([ "https://windwiderstand.de", "https://www.windwiderstand.de", "http://localhost:5173", "http://localhost:4173", ]); export const POST: RequestHandler = async ({ request, getClientAddress }) => { // Origin/Referer must match — blocks naive cross-site POSTs const origin = request.headers.get("origin") ?? ""; const referer = request.headers.get("referer") ?? ""; const refererOrigin = referer ? safeOrigin(referer) : ""; if (!ALLOWED_ORIGINS.has(origin) && !ALLOWED_ORIGINS.has(refererOrigin)) { return json({ error: "Ungültige Anfrage." }, { status: 403 }); } const raw = await request.json().catch(() => null); if (!raw || typeof raw !== "object") { return json({ error: "Ungültige Anfrage." }, { status: 400 }); } const body = raw as Record; // Honeypots — must all be empty for (const hp of HONEYPOT_FIELDS) { const val = body[hp]; if (typeof val === "string" && val.trim() !== "") { // Pretend success so bots don't learn the trap exists return json({ ok: true }, { status: 201 }); } } // Client age — bots usually post immediately const clientAge = Number(body._client_age_ms); if (!Number.isFinite(clientAge) || clientAge < LIMITS.MIN_CLIENT_AGE_MS) { return json({ error: "Bitte etwas mehr Zeit lassen." }, { status: 429 }); } // Cloudflare Turnstile (skipped when no secret configured) if (TURNSTILE_ENABLED) { const token = typeof body.cf_turnstile_token === "string" ? body.cf_turnstile_token : ""; const ok = await verifyTurnstile(token, getClientAddress()); if (!ok) { return json({ error: "captcha" }, { status: 403 }); } } // Full validation const fields = { name: typeof body.name === "string" ? body.name : "", email: typeof body.email === "string" ? body.email : "", subject: typeof body.subject === "string" ? body.subject : "", message: typeof body.message === "string" ? body.message : "", consent: body.consent === true, }; const errors = validateContact(fields); if (Object.keys(errors).length > 0) { return json({ errors }, { status: 422 }); } // Forward to CMS forms plugin const cmsBase = (publicEnv.PUBLIC_CMS_URL || "http://localhost:3099").replace(/\/$/, ""); const cmsEnv = publicEnv.PUBLIC_CMS_ENV || ""; const cmsUrl = cmsEnv ? `${cmsBase}/api/forms/kontakt/submit?_environment=${encodeURIComponent(cmsEnv)}` : `${cmsBase}/api/forms/kontakt/submit`; const payload = { name: fields.name.trim(), email: fields.email.trim(), subject: fields.subject.trim(), message: fields.message.trim(), consent_at: new Date().toISOString(), submitted_via: "windwiderstand.de/api/contact", user_agent: (request.headers.get("user-agent") ?? "").slice(0, 200), }; let res: Response; try { // No Origin header — we already validated it at this server. // CMS forms plugin (REQUIRE_ORIGIN=false) accepts requests without one. res = await fetch(cmsUrl, { method: "POST", headers: { "Content-Type": "application/json", "X-Forwarded-For": getClientAddress(), }, body: JSON.stringify(payload), }); } catch (e) { console.error("[contact] CMS fetch failed:", e); return json({ error: "Server nicht erreichbar." }, { status: 502 }); } if (res.ok) { void notifyMatrix( `✉️ Neue Kontakt-Nachricht von ${payload.name} (${payload.email})${payload.subject ? `: ${payload.subject}` : ""}`, ); return json({ ok: true }, { status: 201 }); } if (res.status === 429) { return json({ error: "Zu viele Anfragen. Bitte später erneut versuchen." }, { status: 429 }); } const text = await res.text().catch(() => ""); console.error("[contact] CMS error", res.status, text); return json({ error: "Fehler beim Senden." }, { status: 502 }); }; function safeOrigin(u: string): string { try { return new URL(u).origin; } catch { return ""; } }