www_site/components/auth-form.tsx

113 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { FormEvent, useEffect, useState } from "react";
import { PreviewNotice } from "@/components/ui";
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api/v1";
export function AuthForm({ mode }: { mode: "login" | "register" }) {
const isLogin = mode === "login";
const [csrf, setCsrf] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmation, setConfirmation] = useState("");
const [verificationId, setVerificationId] = useState("");
const [verificationCode, setVerificationCode] = useState("");
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
useEffect(() => {
fetch(`${apiBase}/auth/csrf`, { credentials: "include" })
.then((response) => response.json())
.then((body) => setCsrf(body.csrf_token ?? ""))
.catch(() => setError("暂时无法连接认证服务"));
}, []);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setMessage("");
if (!isLogin && password !== confirmation) {
setError("两次输入的密码不一致");
return;
}
setBusy(true);
try {
const response = await fetch(`${apiBase}/auth/${isLogin ? "login" : "register"}`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", ...(csrf ? { "X-CSRF-Token": csrf } : {}) },
body: JSON.stringify({ email, password }),
});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.error?.message ?? "请求失败,请稍后重试");
if (isLogin) {
window.location.assign("/account");
} else {
setVerificationId(body.challenge_id ?? "");
setMessage("注册成功,验证码已发送到你的邮箱");
}
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "请求失败,请稍后重试");
} finally {
setBusy(false);
}
}
async function confirmVerification(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setBusy(true);
try {
const response = await fetch(`${apiBase}/auth/verification/confirm-registration`, {
method: "POST", credentials: "include", headers: { "Content-Type": "application/json", ...(csrf ? { "X-CSRF-Token": csrf } : {}) },
body: JSON.stringify({ challenge_id: verificationId, code: verificationCode }),
});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.error?.message ?? "验证码错误,请重试");
setMessage("邮箱验证成功,请登录");
window.setTimeout(() => window.location.assign("/login"), 500);
} catch (confirmError) {
setError(confirmError instanceof Error ? confirmError.message : "验证失败,请重试");
} finally { setBusy(false); }
}
async function resendVerification() {
setError("");
setBusy(true);
try {
const response = await fetch(`${apiBase}/auth/verification/resend`, {
method: "POST", credentials: "include", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ challenge_id: verificationId }),
});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.error?.message ?? "验证码发送失败,请稍后重试");
setMessage("新的验证码已发送");
} catch (resendError) {
setError(resendError instanceof Error ? resendError.message : "验证码发送失败,请稍后重试");
} finally { setBusy(false); }
}
return (
<div className="w-full max-w-md rounded-2xl border border-line bg-panel/70 p-6 sm:p-8">
<PreviewNotice></PreviewNotice>
{verificationId ? <form className="mt-8 space-y-5" onSubmit={confirmVerification}>
<div><label className="field-label" htmlFor="verification-code"></label><input className="field-input" id="verification-code" inputMode="numeric" maxLength={6} onChange={(event) => setVerificationCode(event.target.value)} placeholder="输入 6 位验证码" required value={verificationCode} /></div>
{error ? <p className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm leading-6 text-danger" role="alert">{error}</p> : null}
<button className="button-primary w-full" disabled={busy || !csrf} type="submit">{busy ? "验证中…" : "验证邮箱"}</button>
<button className="w-full text-sm text-muted hover:text-copy" disabled={busy} onClick={resendVerification} type="button"></button>
</form> : <form className="mt-8 space-y-5" onSubmit={submit}>
<div><label className="field-label" htmlFor="email"></label><input className="field-input" id="email" onChange={(event) => setEmail(event.target.value)} placeholder="name@example.com" required type="email" value={email} /></div>
<div><label className="field-label" htmlFor="password"></label><input className="field-input" id="password" minLength={8} onChange={(event) => setPassword(event.target.value)} placeholder="至少 8 位字符" required type="password" value={password} /></div>
{!isLogin ? <div><label className="field-label" htmlFor="password-confirm"></label><input className="field-input" id="password-confirm" minLength={8} onChange={(event) => setConfirmation(event.target.value)} placeholder="再次输入密码" required type="password" value={confirmation} /></div> : null}
{error ? <p className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm leading-6 text-danger" role="alert">{error}</p> : null}
{message ? <p className="rounded-lg border border-success/30 bg-success/10 px-3 py-2 text-sm leading-6 text-success" role="status">{message}</p> : null}
<button className="button-primary w-full" disabled={busy || !csrf} type="submit">{busy ? "处理中…" : isLogin ? "登录" : "注册"}</button>
</form>}
<p className="mt-6 text-center text-sm text-muted">{isLogin ? "还没有账户?" : "已有账户?"}{" "}<Link className="text-cyan hover:text-copy" href={isLogin ? "/register" : "/login"}>{isLogin ? "去注册" : "去登录"}</Link></p>
</div>
);
}