From 63e3ade7f45eef97e44860ac8df393e20e3fa9eb Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 12 Jun 2026 10:27:03 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=B3=A8=E5=86=8C=E5=88=86?= =?UTF-8?q?=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(auth)/register/page.tsx | 10 + frontend/components/auth/login-form.tsx | 340 +++++++-------------- frontend/components/auth/register-form.tsx | 247 +++++++++++++++ frontend/components/auth/register-page.tsx | 25 ++ 4 files changed, 386 insertions(+), 236 deletions(-) create mode 100644 frontend/app/(auth)/register/page.tsx create mode 100644 frontend/components/auth/register-form.tsx create mode 100644 frontend/components/auth/register-page.tsx diff --git a/frontend/app/(auth)/register/page.tsx b/frontend/app/(auth)/register/page.tsx new file mode 100644 index 00000000..91ab280b --- /dev/null +++ b/frontend/app/(auth)/register/page.tsx @@ -0,0 +1,10 @@ +import {Suspense} from "react" +import {RegisterPage} from "@/components/auth/register-page" + +export default function Page() { + return ( + + + + ) +} diff --git a/frontend/components/auth/login-form.tsx b/frontend/components/auth/login-form.tsx index f62dde42..031ff983 100644 --- a/frontend/components/auth/login-form.tsx +++ b/frontend/components/auth/login-form.tsx @@ -3,19 +3,19 @@ import {useEffect, useMemo, useRef, useState} from "react" import {useMutation, useQuery} from "@tanstack/react-query" import {useRouter, useSearchParams} from "next/navigation" -import {KeyRound, UserPlus} from "lucide-react" +import {KeyRound} from "lucide-react" import {toast} from "sonner" +import Link from "next/link" import {useAuth} from "@/components/providers/auth-provider" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" import {Separator} from "@/components/ui/separator" import {Spinner} from "@/components/ui/spinner" -import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" import {Card, CardContent} from "@/components/ui/card" import {CapWidget} from "@/components/auth/cap-widget" import services from "@/lib/services" -import type {LoginRequest, RegisterRequest} from "@/lib/services/auth/types" +import type {LoginRequest} from "@/lib/services/auth/types" function getRedirectTarget(searchParams: ReturnType) { const callbackUrl = searchParams.get("callbackUrl") @@ -45,16 +45,12 @@ export function LoginForm() { const router = useRouter() const searchParams = useSearchParams() const { setUser } = useAuth() - const [mode, setMode] = useState<"login" | "register">("login") const [username, setUsername] = useState("") const [password, setPassword] = useState("") - const [nickname, setNickname] = useState("") - const [email, setEmail] = useState("") const [code, setCode] = useState("") const [showLoginCodeInput, setShowLoginCodeInput] = useState(false) const [maskedEmail, setMaskedEmail] = useState("") const [loginCooldown, setLoginCooldown] = useState(0) - const [registerCooldown, setRegisterCooldown] = useState(0) const [errorMessage, setErrorMessage] = useState("") useEffect(() => { @@ -64,13 +60,6 @@ export function LoginForm() { } }, [loginCooldown]) - useEffect(() => { - if (registerCooldown > 0) { - const timer = setTimeout(() => setRegisterCooldown(registerCooldown - 1), 1000) - return () => clearTimeout(timer) - } - }, [registerCooldown]) - // Cap token management — ref to hold latest token without triggering re-render const capTokenRef = useRef(null) const [capReady, setCapReady] = useState(false) @@ -110,6 +99,7 @@ export function LoginForm() { onSuccess: (user) => { setUser(user) router.replace(redirectTarget) + toast.success("登录成功") }, onError: (error: Error) => { const errorMsg = error.message || "" @@ -127,7 +117,7 @@ export function LoginForm() { return } - toast.error(errorMsg || "登录失败,请重试") + setErrorMessage(errorMsg || "登录失败,请重试") if (capEnabled) { capTokenRef.current = null setCapReady(false) @@ -136,48 +126,12 @@ export function LoginForm() { }, }) - const registerMutation = useMutation({ - mutationFn: (req: RegisterRequest) => services.auth.register(req), - onSuccess: (user) => { - setUser(user) - router.replace(redirectTarget) - }, - onError: (error: Error) => { - setErrorMessage(error.message || "注册失败,请重试") - }, - }) - - const sendRegisterCodeMutation = useMutation({ - mutationFn: (targetEmail: string) => services.auth.sendEmailCode(targetEmail, "register"), - onSuccess: () => { - setRegisterCooldown(60) - toast.success("验证码已发送至您的邮箱,请查收") - }, - onError: (error: Error) => { - toast.error(error.message || "发送验证码失败,请重试") - }, - }) - - const handleSendRegisterCode = () => { - const trimmedEmail = email.trim() - if (!trimmedEmail) { - toast.error("请先输入邮箱地址") - return - } - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(trimmedEmail)) { - toast.error("请输入有效的邮箱地址") - return - } - sendRegisterCodeMutation.mutate(trimmedEmail) - } - const handlePasswordLogin = () => { setErrorMessage("") const trimmedUsername = username.trim() if (!trimmedUsername || !password) { toast.error("账号或密码未填写完整", { - description: "请先输入账号和密码后再登录", + description: "请先输入账号 and 密码后再登录", }) return } @@ -204,23 +158,6 @@ export function LoginForm() { }) } - const handleRegister = () => { - setErrorMessage("") - if (emailRegisterEnabled) { - if (!email.trim() || !code.trim()) { - toast.error("邮箱和验证码不能为空") - return - } - } - registerMutation.mutate({ - username: username.trim(), - password, - nickname: nickname.trim() || undefined, - email: email.trim() || undefined, - code: code.trim() || undefined, - }) - } - const handleOAuthLogin = async (sourceName: string) => { try { setErrorMessage("") @@ -249,20 +186,23 @@ export function LoginForm() { configBool(publicConfigQuery.data?.password_register_enabled, true) const passwordLoginEnabled = configBool(publicConfigQuery.data?.password_login_enabled, true) - const emailRegisterEnabled = configBool(publicConfigQuery.data?.email_register_verification_enabled, false) - const authSources = authSourcesQuery.data ?? [] - // Login button disabled when: - // - password login is off, OR - // - login mutation is pending, OR - // - cap is enabled AND auto-solve mode AND not yet solved (and not in error state) - // When autoStart=false (manual mode), idle means the user hasn't clicked yet — don't block. const loginDisabled = !passwordLoginEnabled || loginMutation.isPending || (capEnabled && capAutoSolve && !capReady && !capError) + if (publicConfigQuery.isPending) { + return ( + + + + + + ) + } + return ( @@ -272,122 +212,30 @@ export function LoginForm() { - setMode(value as "login" | "register")}> - - 登录 - - 注册 - - +
+
+
+ setUsername(e.target.value)} + placeholder="用户名" + autoComplete="username" + onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} + /> + setPassword(e.target.value)} + type="password" + placeholder="密码" + autoComplete="current-password" + onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} + /> - -
-
- setUsername(e.target.value)} - placeholder="用户名" - autoComplete="username" - onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} - /> - setPassword(e.target.value)} - type="password" - placeholder="密码" - autoComplete="current-password" - onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} - /> - - {showLoginCodeInput && ( -
-

- 已向您的安全邮箱 {maskedEmail} 发送了登录验证码。 -

-
- setCode(e.target.value)} - placeholder="6 位邮箱验证码" - maxLength={6} - className="flex-1" - onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} - /> - -
-
- )} -
- - {/* Cap 人机验证 */} - {capEnabled && ( - - )} - - -
-
- - -
-
- setUsername(e.target.value)} - placeholder="用户名" - autoComplete="username" - /> - setNickname(e.target.value)} - placeholder="昵称(可选)" - autoComplete="nickname" - /> - setPassword(e.target.value)} - type="password" - placeholder="密码(至少 8 位)" - autoComplete="new-password" - /> - setEmail(e.target.value)} - placeholder={emailRegisterEnabled ? "电子邮箱" : "电子邮箱(可选)"} - autoComplete="email" - /> - {emailRegisterEnabled && ( + {showLoginCodeInput && ( +
+

+ 已向您的安全邮箱 {maskedEmail} 发送了登录验证码。 +

e.key === "Enter" && handlePasswordLogin()} />
- )} -
- - {errorMessage ? ( -
- {errorMessage}
- ) : null} - - + )}
- - - + {/* Cap 人机验证 */} + {capEnabled && ( + + )} + + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} + + +
+
{authSources.length > 0 ? ( - authSources.map((source) => ( -
+ <> + +
- + {authSources.map((source) => ( + + ))}
- )) + ) : null} + {registrationEnabled && ( +
+ Don't have an account?{" "} + + Sign up + +
+ )} ) diff --git a/frontend/components/auth/register-form.tsx b/frontend/components/auth/register-form.tsx new file mode 100644 index 00000000..87d84daa --- /dev/null +++ b/frontend/components/auth/register-form.tsx @@ -0,0 +1,247 @@ +"use client" + +import {useEffect, useMemo, useState} from "react" +import {useMutation, useQuery} from "@tanstack/react-query" +import {useRouter, useSearchParams} from "next/navigation" +import {UserPlus} from "lucide-react" +import {toast} from "sonner" +import Link from "next/link" + +import {useAuth} from "@/components/providers/auth-provider" +import {Button} from "@/components/ui/button" +import {Input} from "@/components/ui/input" +import {Spinner} from "@/components/ui/spinner" +import {Card, CardContent} from "@/components/ui/card" +import services from "@/lib/services" +import type {RegisterRequest} from "@/lib/services/auth/types" + +function getRedirectTarget(searchParams: ReturnType) { + const callbackUrl = searchParams.get("callbackUrl") + const storedRedirect = sessionStorage.getItem("redirect_after_login") + return callbackUrl || storedRedirect || "/home" +} + +function configBool(value: string | undefined, fallback: boolean) { + if (value === undefined) return fallback + return value === "true" +} + +export function RegisterForm() { + const router = useRouter() + const searchParams = useSearchParams() + const { setUser } = useAuth() + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [nickname, setNickname] = useState("") + const [email, setEmail] = useState("") + const [code, setCode] = useState("") + const [registerCooldown, setRegisterCooldown] = useState(0) + const [errorMessage, setErrorMessage] = useState("") + + useEffect(() => { + if (registerCooldown > 0) { + const timer = setTimeout(() => setRegisterCooldown(registerCooldown - 1), 1000) + return () => clearTimeout(timer) + } + }, [registerCooldown]) + + const publicConfigQuery = useQuery({ + queryKey: ["public-config"], + queryFn: () => services.config.getPublicConfig(), + }) + + const redirectTarget = useMemo( + () => getRedirectTarget(searchParams), + [searchParams], + ) + + const registrationEnabled = + configBool(publicConfigQuery.data?.registration_enabled, true) && + configBool(publicConfigQuery.data?.password_register_enabled, true) + + const emailRegisterEnabled = configBool(publicConfigQuery.data?.email_register_verification_enabled, false) + + // Redirect to login if registration is closed + useEffect(() => { + if (publicConfigQuery.isSuccess && !registrationEnabled) { + toast.error("系统注册功能已关闭") + router.replace("/login") + } + }, [publicConfigQuery.isSuccess, registrationEnabled, router]) + + const registerMutation = useMutation({ + mutationFn: (req: RegisterRequest) => services.auth.register(req), + onSuccess: (user) => { + setUser(user) + router.replace(redirectTarget) + toast.success("注册并登录成功") + }, + onError: (error: Error) => { + setErrorMessage(error.message || "注册失败,请重试") + }, + }) + + const sendRegisterCodeMutation = useMutation({ + mutationFn: (targetEmail: string) => services.auth.sendEmailCode(targetEmail, "register"), + onSuccess: () => { + setRegisterCooldown(60) + toast.success("验证码已发送至您的邮箱,请查收") + }, + onError: (error: Error) => { + toast.error(error.message || "发送验证码失败,请重试") + }, + }) + + const handleSendRegisterCode = () => { + const trimmedEmail = email.trim() + if (!trimmedEmail) { + toast.error("请先输入邮箱地址") + return + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(trimmedEmail)) { + toast.error("请输入有效的邮箱地址") + return + } + sendRegisterCodeMutation.mutate(trimmedEmail) + } + + const handleRegister = () => { + setErrorMessage("") + if (!username.trim() || !password) { + toast.error("用户名和密码不能为空") + return + } + if (password.length < 8) { + toast.error("密码长度不能少于 8 位") + return + } + if (emailRegisterEnabled) { + if (!email.trim() || !code.trim()) { + toast.error("邮箱和验证码不能为空") + return + } + } + registerMutation.mutate({ + username: username.trim(), + password, + nickname: nickname.trim() || undefined, + email: email.trim() || undefined, + code: code.trim() || undefined, + }) + } + + if (publicConfigQuery.isPending) { + return ( + + + + + + ) + } + + if (!registrationEnabled) { + return null + } + + return ( + + +
+

+ 创建账号 +

+
+ +
+
+ setUsername(e.target.value)} + placeholder="用户名" + autoComplete="username" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + setNickname(e.target.value)} + placeholder="昵称(可选)" + autoComplete="nickname" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + setPassword(e.target.value)} + type="password" + placeholder="密码(至少 8 位)" + autoComplete="new-password" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + setEmail(e.target.value)} + placeholder={emailRegisterEnabled ? "电子邮箱" : "电子邮箱(可选)"} + autoComplete="email" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + {emailRegisterEnabled && ( +
+ setCode(e.target.value)} + placeholder="6 位邮箱验证码" + maxLength={6} + className="flex-1" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + +
+ )} +
+ + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} + + +
+ +
+ Already have an account?{" "} + + Sign in + +
+
+
+ ) +} diff --git a/frontend/components/auth/register-page.tsx b/frontend/components/auth/register-page.tsx new file mode 100644 index 00000000..cba9b374 --- /dev/null +++ b/frontend/components/auth/register-page.tsx @@ -0,0 +1,25 @@ +"use client" + +import {motion} from "motion/react" +import {RegisterForm} from "@/components/auth/register-form" +import {AuroraBackground} from "@/components/ui/aurora-background" + +export function RegisterPage() { + return ( + + + + + + ) +}