72 lines
3.8 KiB
TypeScript
72 lines
3.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|