feat: protect auth with Turnstile
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { getRememberMePref, setAuthPersistencePreference } from "../auth";
|
||||
import GoogleAuthCard from "../components/GoogleAuthCard";
|
||||
import MicrosoftAuthCard from "../components/MicrosoftAuthCard";
|
||||
import TwoFactorChallenge from "../components/TwoFactorChallenge";
|
||||
import TurnstileWidget from "../components/TurnstileWidget";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
@@ -19,6 +20,8 @@ type AuthConfig = {
|
||||
localEnabled: boolean;
|
||||
allowRegistration: boolean;
|
||||
requireEmailVerification: boolean;
|
||||
turnstileEnabled?: boolean;
|
||||
turnstileSiteKey?: string;
|
||||
};
|
||||
|
||||
export default function LoginPage({ initialMode = "login" }: { initialMode?: "login" | "register" }) {
|
||||
@@ -41,6 +44,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo
|
||||
const [verificationResent, setVerificationResent] = useState(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<{ email?: string; password?: string; confirmPassword?: string }>({});
|
||||
const [registerMode, setRegisterMode] = useState(initialMode === "register");
|
||||
const [turnstileToken, setTurnstileToken] = useState("");
|
||||
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
@@ -83,7 +87,8 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo
|
||||
setVerificationResent(false);
|
||||
try {
|
||||
const url = mode === "register" ? "/auth/register" : "/auth/login";
|
||||
const res = await api.post<{ requiresTwoFactor?: boolean; pendingToken?: string }>(url, { email, password, rememberMe });
|
||||
const payload = { email, password, rememberMe, ...(cfg?.turnstileEnabled ? { turnstileToken } : {}) };
|
||||
const res = await api.post<{ requiresTwoFactor?: boolean; pendingToken?: string }>(url, payload);
|
||||
if (res.data?.requiresTwoFactor && res.data.pendingToken) {
|
||||
setPendingToken(res.data.pendingToken);
|
||||
return;
|
||||
@@ -230,6 +235,10 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{cfg?.turnstileEnabled && cfg.turnstileSiteKey ? (
|
||||
<TurnstileWidget key={registerMode ? "register" : "login"} siteKey={cfg.turnstileSiteKey} action={registerMode ? "register" : "login"} onToken={setTurnstileToken} />
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, mt: 1, flexWrap: "wrap" }}>
|
||||
{(allowReg || initialMode === "register") && (
|
||||
<Button
|
||||
@@ -244,7 +253,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo
|
||||
{registerMode ? t("backToLogin") : t("createAccount")}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" variant="contained" disableRipple disabled={loading || (registerMode && cfg !== null && !allowReg)} sx={{ ml: "auto" }}>
|
||||
<Button type="submit" variant="contained" disableRipple disabled={loading || (registerMode && cfg !== null && !allowReg) || Boolean(cfg?.turnstileEnabled && !turnstileToken)} sx={{ ml: "auto" }}>
|
||||
{registerMode ? t("createAccount") : t("signInTitle")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user