www_site/components/auth-form.tsx

72 lines
3.9 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 [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>
);
}