2026-05-26 19:32:19 +08:00

255 lines
8.6 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 { useRouter } from "next/navigation";
import { useActionState, useEffect, useRef, useState } from "react";
import { AuthForm } from "@/components/auth-form";
import { SubmitButton } from "@/components/submit-button";
import { toast } from "@/components/toast";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type RegisterActionState, register } from "../actions";
export default function Page() {
const router = useRouter();
const [email, setEmail] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
const [siteName, setSiteName] = useState("AI Chatbot");
const [siteLogo, setSiteLogo] = useState<string | null>(null);
const [isEmailVerificationEnabled, setIsEmailVerificationEnabled] = useState(false);
const [emailVerificationExpiresMinutes, setEmailVerificationExpiresMinutes] = useState(10);
const [isSendingCode, setIsSendingCode] = useState(false);
const [codeCooldown, setCodeCooldown] = useState(0);
const [isCheckingExistingSession, setIsCheckingExistingSession] = useState(true);
const redirectTimerRef = useRef<number | null>(null);
useEffect(() => {
// 获取认证设置并检查是否开放注册
fetch("/api/auth-settings")
.then((res) => res.json())
.then((data) => {
if (data.isRegistrationEnabled === false) {
toast({ type: "error", description: "当前系统未开放注册" });
router.push("/login");
}
setIsEmailVerificationEnabled(data.isEmailVerificationEnabled === true);
const minutes = Number(data.emailVerificationExpiresMinutes) || 10;
setEmailVerificationExpiresMinutes(Math.min(Math.max(minutes, 3), 30));
})
.catch(console.error);
// 获取网站设置(添加时间戳防止缓存)
fetch(`/api/site-settings?scope=branding&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
if (data.siteName) {
setSiteName(data.siteName);
}
if (data.siteLogo) {
setSiteLogo(data.siteLogo);
}
})
.catch(console.error);
fetch("/api/auth/validate-session", {
cache: "no-store",
credentials: "include",
})
.then((res) => res.json())
.then((data) => {
if (data?.valid === true && data?.userType !== "guest") {
window.location.replace("/");
return;
}
setIsCheckingExistingSession(false);
})
.catch(() => {
setIsCheckingExistingSession(false);
});
}, []);
useEffect(() => {
if (codeCooldown <= 0) {
return;
}
const timer = window.setInterval(() => {
setCodeCooldown((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
return () => window.clearInterval(timer);
}, [codeCooldown]);
const [state, formAction] = useActionState<RegisterActionState, FormData>(
register,
{
status: "idle",
}
);
useEffect(() => {
if (redirectTimerRef.current) {
window.clearTimeout(redirectTimerRef.current);
redirectTimerRef.current = null;
}
if (state.status === "user_exists") {
setIsSuccessful(false);
toast({ type: "error", description: "账号已存在" });
} else if (state.status === "password_mismatch") {
setIsSuccessful(false);
toast({ type: "error", description: "两次输入的密码不一致" });
} else if (state.status === "verification_code_required") {
setIsSuccessful(false);
toast({ type: "error", description: "请先输入邮箱验证码" });
} else if (state.status === "verification_code_invalid") {
setIsSuccessful(false);
toast({ type: "error", description: "邮箱验证码不正确" });
} else if (state.status === "verification_code_expired") {
setIsSuccessful(false);
toast({ type: "error", description: "邮箱验证码已过期,请重新发送" });
} else if (state.status === "failed") {
setIsSuccessful(false);
toast({ type: "error", description: "创建账号失败" });
} else if (state.status === "invalid_data") {
setIsSuccessful(false);
toast({
type: "error",
description: "提交的信息校验失败",
});
} else if (state.status === "success") {
toast({ type: "success", description: "账号创建成功" });
setIsSuccessful(true);
redirectTimerRef.current = window.setTimeout(() => {
window.location.replace("/");
}, 300);
}
}, [state.status]);
useEffect(() => {
return () => {
if (redirectTimerRef.current) {
window.clearTimeout(redirectTimerRef.current);
}
};
}, []);
const handleSubmit = (formData: FormData) => {
setEmail(formData.get("email") as string);
formAction(formData);
};
if (isCheckingExistingSession) {
return (
<div className="flex min-h-screen items-center justify-center text-muted-foreground text-sm">
正在检查登录状态...
</div>
);
}
const handleSendCode = async () => {
const normalizedEmail = email.trim().toLowerCase();
if (!normalizedEmail) {
toast({ type: "error", description: "请先输入邮箱地址" });
return;
}
if (codeCooldown > 0 || isSendingCode) {
return;
}
setIsSendingCode(true);
try {
const response = await fetch("/api/auth/email/send-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: normalizedEmail }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data?.error || "验证码发送失败");
}
const cooldownSeconds = Number(data?.cooldownSeconds) || 60;
setCodeCooldown(cooldownSeconds);
toast({
type: "success",
description: `验证码已发送,请在 ${emailVerificationExpiresMinutes} 分钟内完成验证`,
});
} catch (error) {
toast({
type: "error",
description: error instanceof Error ? error.message : "验证码发送失败",
});
} finally {
setIsSendingCode(false);
}
};
return (
<div className="flex h-dvh w-screen items-start justify-center bg-background pt-12 md:items-center md:pt-0">
<div className="flex w-full max-w-md flex-col gap-12 overflow-hidden rounded-2xl">
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
{siteLogo ? (
<img src={siteLogo} alt={siteName} className="h-12 w-12 mb-2 object-contain" />
) : null}
<h3 className="font-semibold text-xl dark:text-zinc-50">注册 {siteName}</h3>
<p className="text-gray-500 text-sm dark:text-zinc-400">
使用邮箱与密码创建新账号
</p>
</div>
<AuthForm
action={handleSubmit}
defaultEmail={email}
onEmailChange={setEmail}
showConfirmPassword
>
{isEmailVerificationEnabled ? (
<div className="flex flex-col gap-2">
<Label
className="font-normal text-zinc-600 dark:text-zinc-400"
htmlFor="verificationCode"
>
邮箱验证码
</Label>
<div className="flex items-center gap-2">
<Input
className="bg-muted text-md md:text-sm"
id="verificationCode"
name="verificationCode"
placeholder="请输入 6 位验证码"
required
type="text"
/>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={isSendingCode || codeCooldown > 0}
className="shrink-0"
>
{isSendingCode
? "发送中..."
: codeCooldown > 0
? `${codeCooldown}s`
: "发送验证码"}
</Button>
</div>
</div>
) : null}
<SubmitButton isSuccessful={isSuccessful}>注册</SubmitButton>
<p className="mt-4 text-center text-gray-600 text-sm dark:text-zinc-400">
已经有账号?
<Link
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
href="/login"
>
立即登录
</Link>
。
</p>
</AuthForm>
</div>
</div>
);
}