256 lines
8.7 KiB
TypeScript
256 lines
8.7 KiB
TypeScript
"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?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",
|
||
}
|
||
);
|
||
|
||
// biome-ignore lint/correctness/useExhaustiveDependencies: router is a stable ref
|
||
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>
|
||
);
|
||
}
|