Files
tyx_AI_xhs/apps/web/src/admin-auth.tsx
T

151 lines
5.6 KiB
TypeScript

import { useEffect, useId, useState, type FormEvent } from "react";
import "./admin-auth.css";
interface ErrorEnvelopeBody {
error?: {
details?: { field_errors?: Array<{ message_key?: string }> };
};
}
function adminErrorMessage(body: ErrorEnvelopeBody) {
const key = body.error?.details?.field_errors?.[0]?.message_key;
if (key === "auth.challenge.invalid") return "验证码不正确,请检查后重试。";
if (key === "auth.challenge.expired") return "验证码已过期,请重新获取。";
if (key === "auth.challenge.resend_too_soon") return "请等待倒计时结束后重新获取验证码。";
if (key === "auth.account.suspended" || key === "admin.auth.not_allowed") return "无法使用管理员入口,请联系部署维护人员。";
return "管理员登录暂时无法完成,请稍后重试。";
}
export function AdminAuthPage() {
const emailId = useId();
const codeId = useId();
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const [registrationId, setRegistrationId] = useState<string>();
const [countdown, setCountdown] = useState(0);
const [sending, setSending] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string>();
const emailValid = /^[^@\s]+@[^@\s]+$/.test(email);
useEffect(() => {
if (countdown <= 0) return;
const timer = window.setInterval(() => setCountdown((value) => Math.max(0, value - 1)), 1_000);
return () => window.clearInterval(timer);
}, [countdown]);
async function sendCode() {
if (!emailValid || sending || countdown > 0) return;
setSending(true);
setError(undefined);
try {
const response = await fetch("/api/v1/admin-auth/login/send", {
body: JSON.stringify({ email }),
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
method: "POST",
});
const body = await response.json() as ErrorEnvelopeBody & { registration_id?: string };
if (!response.ok || !body.registration_id) {
setError(adminErrorMessage(body));
return;
}
setRegistrationId(body.registration_id);
setCountdown(60);
window.requestAnimationFrame(() => document.getElementById(codeId)?.focus());
} catch {
setError("管理员登录暂时无法完成,请稍后重试。");
} finally {
setSending(false);
}
}
async function completeLogin(event: FormEvent) {
event.preventDefault();
if (!registrationId || !/^[0-9]{6}$/.test(code) || submitting) return;
setSubmitting(true);
setError(undefined);
try {
const response = await fetch("/api/v1/admin-auth/login/complete", {
body: JSON.stringify({ registration_id: registrationId, verification_code: code }),
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", ""),
},
method: "POST",
});
const body = await response.json() as ErrorEnvelopeBody;
if (!response.ok) {
setError(adminErrorMessage(body));
return;
}
window.location.assign("/admin");
} catch {
setError("管理员登录暂时无法完成,请稍后重试。");
} finally {
setSubmitting(false);
}
}
return (
<main className="admin-auth-page">
<div className="admin-auth-accent" aria-hidden="true" />
<header className="admin-auth-header">
<a className="admin-auth-wordmark" href="/" aria-label="Dada 普通用户登录">DADA</a>
</header>
<section className="admin-auth-panel">
<p className="admin-auth-kicker">ADMIN</p>
<h1 id="admin-auth-heading">管理员邮箱验证码登录</h1>
<form onSubmit={completeLogin} noValidate>
<label htmlFor={emailId}>管理员邮箱</label>
<div className="admin-auth-send-row">
<input
id={emailId}
autoComplete="email"
inputMode="email"
onChange={(event) => {
setEmail(event.target.value);
setRegistrationId(undefined);
setCode("");
setCountdown(0);
setError(undefined);
}}
type="email"
value={email}
/>
<button
aria-label="获取验证码"
className="admin-auth-send"
disabled={!emailValid || sending || countdown > 0}
onClick={sendCode}
type="button"
>
{sending ? "发送中" : countdown > 0 ? `${countdown}s` : "获取验证码"}
</button>
</div>
{registrationId ? (
<div className="admin-auth-code-field">
<label htmlFor={codeId}>验证码</label>
<input
id={codeId}
autoComplete="one-time-code"
inputMode="numeric"
maxLength={6}
onChange={(event) => setCode(event.target.value.replace(/\D/g, "").slice(0, 6))}
value={code}
/>
</div>
) : null}
{error ? <p className="admin-auth-error" role="alert">{error}</p> : null}
<button className="admin-auth-submit" disabled={!registrationId || code.length !== 6 || submitting} type="submit">
{submitting ? "登录中" : "登录后台"}
</button>
</form>
<a className="admin-auth-return" href="/">返回普通用户登录</a>
</section>
</main>
);
}