'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; 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 { Field, FieldGroup, FieldLabel } from '@/components/ui/field'; import { AuthHeading } from '@/components/auth/auth-shell'; import { CapWidget } from '@/components/auth/cap-widget'; import { AuthService } from '@/lib/services/auth'; import { ConfigService } from '@/lib/services/config'; import type { RegisterRequest } from '@/lib/services/auth/types'; import { safeRedirectTarget } from '@/lib/utils'; function getRedirectTarget(searchParams: ReturnType) { const callbackUrl = searchParams.get('callbackUrl'); const storedRedirect = typeof window === 'undefined' ? null : sessionStorage.getItem('redirect_after_login'); return safeRedirectTarget(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: () => ConfigService.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, ); const capEnabled = configBool( publicConfigQuery.data?.cap_login_enabled, false, ); const capAutoSolve = configBool(publicConfigQuery.data?.cap_auto_solve, true); const [capScope, setCapScope] = useState<'send_email_code' | 'register'>( 'send_email_code', ); // 监听 emailRegisterEnabled 改变初始 scope useEffect(() => { setCapScope(emailRegisterEnabled ? 'send_email_code' : 'register'); }, [emailRegisterEnabled]); const capTokenRef = useRef(null); const [capReady, setCapReady] = useState(false); const [capError, setCapError] = useState(false); const [capResetKey, setCapResetKey] = useState(0); const handleCapToken = (token: string) => { capTokenRef.current = token; setCapReady(true); setCapError(false); }; const handleCapError = () => { capTokenRef.current = null; setCapReady(false); setCapError(true); }; // 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) => { const headers: Record = {}; if (capEnabled && capTokenRef.current) { headers['X-Cap-Token'] = capTokenRef.current; capTokenRef.current = null; setCapReady(false); } return AuthService.register( req, Object.keys(headers).length ? headers : undefined, ); }, onSuccess: (user) => { setUser(user); router.replace(redirectTarget); toast.success('注册并登录成功'); }, onError: (error: Error) => { setErrorMessage(error.message || '注册失败,请重试'); if (capEnabled) { capTokenRef.current = null; setCapReady(false); setCapResetKey((key) => key + 1); } }, }); const sendRegisterCodeMutation = useMutation({ mutationFn: (targetEmail: string) => { const headers: Record = {}; if (capEnabled && capTokenRef.current) { headers['X-Cap-Token'] = capTokenRef.current; capTokenRef.current = null; setCapReady(false); } return AuthService.sendEmailCode( targetEmail, 'register', Object.keys(headers).length ? headers : undefined, ); }, onSuccess: () => { setRegisterCooldown(60); toast.success('验证码已发送至您的邮箱,请查收'); if (capEnabled) { setCapScope('register'); capTokenRef.current = null; setCapReady(false); setCapResetKey((key) => key + 1); } }, onError: (error: Error) => { toast.error(error.message || '发送验证码失败,请重试'); if (capEnabled) { capTokenRef.current = null; setCapReady(false); setCapResetKey((key) => key + 1); } }, }); const registerDisabled = registerMutation.isPending || (capEnabled && capScope === 'register' && capAutoSolve && !capReady && !capError); const sendCodeDisabled = registerCooldown > 0 || sendRegisterCodeMutation.isPending || (capEnabled && capScope === 'send_email_code' && capAutoSolve && !capReady && !capError); const handleSendRegisterCode = () => { const trimmedEmail = email.trim(); if (!trimmedEmail) { toast.error('请先输入邮箱地址'); return; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { toast.error('请输入有效的邮箱地址'); return; } if (capEnabled && capScope === 'send_email_code' && !capReady) { toast.error( capAutoSolve ? '人机验证尚未完成,请稍候…' : '请先点击「开始验证」完成人机验证', ); return; } sendRegisterCodeMutation.mutate(trimmedEmail); }; const handleRegister = () => { setErrorMessage(''); if (!username.trim() || !password) { toast.error('用户名和密码不能为空'); return; } if (password.length < 8) { toast.error('密码长度不能少于 8 位'); return; } const trimmedEmail = email.trim(); if (!trimmedEmail) { toast.error('邮箱不能为空'); return; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { toast.error('请输入有效的邮箱地址'); return; } if (emailRegisterEnabled && !code.trim()) { toast.error('验证码不能为空'); return; } if (capEnabled && capScope === 'register' && !capReady) { toast.error( capAutoSolve ? '人机验证尚未完成,请稍候…' : '请先点击「开始验证」完成人机验证', ); return; } registerMutation.mutate({ username: username.trim(), password, nickname: nickname.trim() || undefined, email: trimmedEmail, code: code.trim() || undefined, }); }; if (publicConfigQuery.isPending) { return (
); } if (!registrationEnabled) { return null; } return (
用户名 * setUsername(e.target.value)} placeholder='请输入用户名' autoComplete='username' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> 昵称 (可选) setNickname(e.target.value)} placeholder='请输入昵称' autoComplete='nickname' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> 电子邮箱 * setEmail(e.target.value)} placeholder='请输入电子邮箱' autoComplete='email' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> 密码 * setPassword(e.target.value)} type='password' placeholder='请输入密码(至少 8 位)' autoComplete='new-password' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> {emailRegisterEnabled && ( 邮箱验证码 *
setCode(e.target.value)} placeholder='请输入 6 位邮箱验证码' maxLength={6} className='h-10 flex-1 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} />
)}
{capEnabled && ( )} {errorMessage ? (
{errorMessage}
) : null}
已经有账号?{' '} 返回登录
); }