feat(contact): add Cloudflare Turnstile captcha
Client renders explicit Turnstile widget (flexible, light theme) when PUBLIC_TURNSTILE_SITE_KEY is set. Token sent with form payload, server verifies via siteverify endpoint before forwarding to CMS. Skipped entirely when no site key configured (dev fallback). New i18n keys: contact_captcha_pending, contact_server_error_captcha. Deploy workflow forwards PUBLIC_TURNSTILE_SITE_KEY + TURNSTILE_SECRET. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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 {
|
||||
HONEYPOT_FIELDS,
|
||||
@@ -7,6 +8,27 @@ import {
|
||||
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<boolean> {
|
||||
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",
|
||||
@@ -44,6 +66,15 @@ export const POST: RequestHandler = async ({ request, getClientAddress, url }) =
|
||||
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 : "",
|
||||
|
||||
Reference in New Issue
Block a user