From 2db00b51573740c59f3cc811d2bc40c19f14b238 Mon Sep 17 00:00:00 2001 From: m4 Date: Sat, 8 Aug 2026 09:35:44 +0800 Subject: [PATCH] feat(webui): captcha input on login page after repeated failures Co-Authored-By: Claude Opus 4.7 --- src/app/login/page.tsx | 55 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index da5c053..9ef0d1a 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -18,6 +18,21 @@ export default function LoginPage() { const [error, setError] = useState(null); const [rememberMe, setRememberMe] = useState(false); const [submitting, setSubmitting] = useState(false); + const [captchaRequired, setCaptchaRequired] = useState(false); + const [captchaId, setCaptchaId] = useState(""); + const [captchaQuestion, setCaptchaQuestion] = useState(""); + const [captchaAnswer, setCaptchaAnswer] = useState(""); + + const refreshCaptcha = async () => { + setCaptchaAnswer(""); + const response = await fetch("/api/auth/captcha", { cache: "no-store" }); + if (!response.ok) return; + const body = (await response.json()) as { id?: string; question?: string }; + if (body.id && body.question) { + setCaptchaId(body.id); + setCaptchaQuestion(body.question); + } + }; const submit = async (event: FormEvent) => { event.preventDefault(); @@ -29,13 +44,26 @@ export default function LoginPage() { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", - body: JSON.stringify({ username, password, next: returnPath(), rememberMe }), + body: JSON.stringify({ + username, + password, + next: returnPath(), + rememberMe, + ...(captchaRequired ? { captchaId, captchaAnswer } : {}), + }), }); const body = (await response.json().catch(() => ({}))) as { error?: string; redirectTo?: string; + requiresCaptcha?: boolean; }; - if (!response.ok) throw new Error(body.error || "Unable to sign in."); + if (!response.ok) { + if (body.requiresCaptcha) { + setCaptchaRequired(true); + await refreshCaptcha(); + } + throw new Error(body.error || "Unable to sign in."); + } window.location.replace(body.redirectTo || "/"); } catch (reason) { setError(reason instanceof Error ? reason.message : "Unable to sign in."); @@ -99,6 +127,29 @@ export default function LoginPage() { /> Remember me for 30 days + {captchaRequired && ( + + )} {error && (