feat(webui): captcha input on login page after repeated failures
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+53
-2
@@ -18,6 +18,21 @@ export default function LoginPage() {
|
||||
const [error, setError] = useState<string | null>(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<HTMLFormElement>) => {
|
||||
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() {
|
||||
/>
|
||||
<span>Remember me for 30 days</span>
|
||||
</label>
|
||||
{captchaRequired && (
|
||||
<label className="block space-y-1.5 text-sm font-medium">
|
||||
<span>
|
||||
Verification: {captchaQuestion}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void refreshCaptcha()}
|
||||
className="text-muted-foreground underline underline-offset-2"
|
||||
>
|
||||
New question
|
||||
</button>
|
||||
</span>
|
||||
<Input
|
||||
name="captchaAnswer"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
value={captchaAnswer}
|
||||
onChange={(event) => setCaptchaAnswer(event.target.value)}
|
||||
disabled={submitting}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="mt-4 text-sm text-destructive" role="alert">
|
||||
|
||||
Reference in New Issue
Block a user