feat: protect auth with Turnstile
CI and Deploy / test (push) Successful in 2m41s
CI and Deploy / deploy (push) Successful in 59s

This commit is contained in:
cesnimda
2026-07-30 23:08:36 +02:00
parent 6382e83e28
commit 9cd2e5c2e3
7 changed files with 127 additions and 7 deletions
@@ -0,0 +1,34 @@
import { useEffect, useRef } from "react";
import { Box, Typography } from "@mui/material";
type Props = { siteKey: string; action: "login" | "register"; onToken: (token: string) => void; };
export default function TurnstileWidget({ siteKey, action, onToken }: Props) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
let widgetId: string | undefined;
let cancelled = false;
const render = () => {
if (cancelled || !ref.current || !(window as any).turnstile) return;
widgetId = (window as any).turnstile.render(ref.current, {
sitekey: siteKey, action, theme: "auto", size: "flexible",
callback: onToken,
"expired-callback": () => onToken(""),
"error-callback": () => onToken(""),
});
};
const existing = document.querySelector<HTMLScriptElement>('script[data-turnstile]');
if ((window as any).turnstile) render();
else if (existing) existing.addEventListener("load", render, { once: true });
else {
const script = document.createElement("script");
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
script.async = true; script.defer = true; script.dataset.turnstile = "true";
script.addEventListener("load", render, { once: true });
document.head.appendChild(script);
}
return () => { cancelled = true; if (widgetId && (window as any).turnstile) (window as any).turnstile.remove(widgetId); };
}, [action, onToken, siteKey]);
return <Box><Typography variant="caption" color="text.secondary">Security check</Typography><Box ref={ref} sx={{ minHeight: 65, mt: 0.5 }} /></Box>;
}