import { siteSettings } from "@/lib/services/site-settings"; /** * Server-side CAPTCHA verification, driven by website_settings so staff pick the * provider in housekeeping. Supports Cloudflare Turnstile and Google reCAPTCHA * (the two AtomCMS offers, mutually exclusive). FAIL-OPEN by configuration: when * no provider/secret is set, registration isn't blocked; only an explicitly * configured provider with a failing/absent token blocks. * * Settings keys: * captcha_provider = "turnstile" | "recaptcha" | "none" (default none) * turnstile_secret / turnstile_site_key * recaptcha_secret / recaptcha_site_key */ export interface CaptchaConfig { provider: "turnstile" | "recaptcha" | "none"; siteKey: string; /** Form field the widget writes the token into. */ field: string; } const TURNSTILE_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; const RECAPTCHA_URL = "https://www.google.com/recaptcha/api/siteverify"; /** Public config the register page needs to render the widget (no secrets). */ export async function captchaConfig(): Promise { const provider = ((await siteSettings.get("captcha_provider", "none")) ?? "none").toLowerCase(); if (provider === "turnstile") { return { provider: "turnstile", siteKey: (await siteSettings.get("turnstile_site_key", "")) ?? "", field: "cf-turnstile-response", }; } if (provider === "recaptcha") { return { provider: "recaptcha", siteKey: (await siteSettings.get("recaptcha_site_key", "")) ?? "", field: "g-recaptcha-response", }; } return { provider: "none", siteKey: "", field: "" }; } /** Verify a submitted token. Returns true when allowed (incl. fail-open). */ export async function verifyCaptcha(token: string | null, remoteIp?: string): Promise { const cfg = await captchaConfig(); if (cfg.provider === "none" || !cfg.siteKey) return true; const secretKey = cfg.provider === "turnstile" ? "turnstile_secret" : "recaptcha_secret"; const secret = (await siteSettings.get(secretKey, "")) ?? ""; if (!secret) return true; // configured but no secret — don't hard-block if (!token) return false; const url = cfg.provider === "turnstile" ? TURNSTILE_URL : RECAPTCHA_URL; const body = new URLSearchParams({ secret, response: token }); if (remoteIp) body.set("remoteip", remoteIp); try { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); const res = await fetch(url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body, signal: controller.signal, cache: "no-store", }); clearTimeout(timer); const data = (await res.json()) as { success?: boolean }; return data?.success === true; } catch { // Network/timeout — fail-open so a provider outage can't lock out signups. return true; } }