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). * * Behaviour: * - provider "none" (or unset) → fail-open (allow) * - provider configured but site key / secret missing, token absent, provider * API error, or network failure → fail-closed (deny) * * 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/login pages need 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. Fail-closed when a provider is configured. */ export async function verifyCaptcha( token: string | null, remoteIp?: string, ): Promise { const cfg = await captchaConfig(); if (cfg.provider === "none") return true; if (!cfg.siteKey) return false; const secretKey = cfg.provider === "turnstile" ? "turnstile_secret" : "recaptcha_secret"; const secret = (await siteSettings.get(secretKey, "")) ?? ""; if (!secret) return false; 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); if (!res.ok) return false; const data = (await res.json()) as { success?: boolean }; return data?.success === true; } catch { // Network/timeout/misconfig — fail-closed so captcha can't be bypassed. return false; } }