feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 2m55s

Ship product gaps: register/vote pages, friend add/accept/decline/remove, email verify TTL, captcha on login/forgot, soft-fail user actions, SSE abort/shared client, Commando Centrum error toasts, admin delete for events/polls, and IT/NL i18n fills.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-21 21:08:33 +02:00
1 parent 2ff08e5127
commit ed7db6e048
76 files changed
+4834 -1376

No files matched your search

+15 -9
View File
@@ -3,9 +3,12 @@ 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.
* (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)
@@ -23,7 +26,7 @@ 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). */
/** Public config the register/login pages need to render the widget (no secrets). */
export async function captchaConfig(): Promise<CaptchaConfig> {
const provider = (
(await siteSettings.get("captcha_provider", "none")) ?? "none"
@@ -45,18 +48,20 @@ export async function captchaConfig(): Promise<CaptchaConfig> {
return { provider: "none", siteKey: "", field: "" };
}
/** Verify a submitted token. Returns true when allowed (incl. fail-open). */
/** Verify a submitted token. Fail-closed when a provider is configured. */
export async function verifyCaptcha(
token: string | null,
remoteIp?: string,
): Promise<boolean> {
const cfg = await captchaConfig();
if (cfg.provider === "none" || !cfg.siteKey) return true;
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 true; // configured but no secret — don't hard-block
if (!secret) return false;
if (!token) return false;
const url = cfg.provider === "turnstile" ? TURNSTILE_URL : RECAPTCHA_URL;
@@ -74,10 +79,11 @@ export async function verifyCaptcha(
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 — fail-open so a provider outage can't lock out signups.
return true;
// Network/timeout/misconfig — fail-closed so captcha can't be bypassed.
return false;
}
}