feat(web): add Cloudflare Turnstile to the public forms

Render the widget on the contact, event registration and both application
forms, and verify the token with Siteverify in each action before anything
is written to the CMS. Verification fails closed. The check is off unless
both TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY are set.

Allow challenges.cloudflare.com in script-src and frame-src.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:05:51 +02:00
co-authored by Claude Opus 5.5
parent 0b2c8d581f
commit 709a978da5
17 changed files with 475 additions and 15 deletions
+23
View File
@@ -20,6 +20,13 @@ export interface ServerEnv {
readonly STRAPI_TOKEN?: string;
/** Public origin of this site, no trailing slash. Used for canonical URLs. */
readonly SITE_URL: string;
/**
* Cloudflare Turnstile keys. Set together or not at all: the check on the
* public forms is on only when BOTH are present, so a half-configured deploy
* leaves the forms unprotected rather than rejecting every submission.
*/
readonly TURNSTILE_SITE_KEY?: string;
readonly TURNSTILE_SECRET_KEY?: string;
readonly isProduction: boolean;
readonly isDevelopment: boolean;
}
@@ -92,14 +99,30 @@ export function getServerEnv(): ServerEnv {
issues,
);
const token = source.STRAPI_TOKEN?.trim();
const turnstileSiteKey = source.TURNSTILE_SITE_KEY?.trim();
const turnstileSecretKey = source.TURNSTILE_SECRET_KEY?.trim();
if (issues.length > 0) throw new EnvError(issues);
/* Not an EnvError: a missing bot check is no reason to take every page
down. It is said out loud once, because the forms are now unguarded. */
if (Boolean(turnstileSiteKey) !== Boolean(turnstileSecretKey)) {
console.warn(
"[env] Only one of TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY is set; Turnstile is off.",
);
}
cached = {
NODE_ENV,
STRAPI_URL,
SITE_URL,
...(token ? { STRAPI_TOKEN: token } : {}),
...(turnstileSiteKey && turnstileSecretKey
? {
TURNSTILE_SITE_KEY: turnstileSiteKey,
TURNSTILE_SECRET_KEY: turnstileSecretKey,
}
: {}),
isProduction: NODE_ENV === "production",
isDevelopment: NODE_ENV === "development",
};