Files
EpicNext-Cms/src/components/auth/captcha-widget.tsx
T
SimoandCursor ed7db6e048
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 2m55s
feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
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]>
2026-07-21 21:08:33 +02:00

61 lines
1.4 KiB
TypeScript

"use client";
import Script from "next/script";
export type CaptchaPublicConfig = {
provider: string;
siteKey?: string;
field?: string;
};
/** Renders Turnstile / reCAPTCHA widget + script when a provider is configured. */
export function CaptchaWidget({
captcha,
nonce,
className,
}: {
captcha: CaptchaPublicConfig;
nonce?: string;
className?: string;
}) {
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
if (!showCaptcha) return null;
return (
<div className={className}>
{captcha.provider === "turnstile" ? (
<>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
nonce={nonce}
/>
<div className="cf-turnstile" data-sitekey={captcha.siteKey} />
</>
) : null}
{captcha.provider === "recaptcha" ? (
<>
<Script
src="https://www.google.com/recaptcha/api.js"
async
defer
nonce={nonce}
/>
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
</>
) : null}
</div>
);
}
/** Read the provider token from a form (widget injects a hidden input). */
export function readCaptchaToken(
form: HTMLFormElement,
captcha: CaptchaPublicConfig,
): string {
if (captcha.provider === "none" || !captcha.field) return "";
const fd = new FormData(form);
return String(fd.get(captcha.field) ?? "").normalize("NFC");
}