www_site/components/auth-form.tsx

72 lines
3.8 KiB
TypeScript
Raw Normal View History

"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 [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 {
setMessage("注册成功,请使用新账户登录");
window.setTimeout(() => window.location.assign("/login"), 500);
}
} catch (submitError) {
setError(submitError instanceof Error ? submitError.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>
<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>
);
}