'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 { useTranslations } from 'next-intl'; 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 t = useTranslations('auth.register'); const tCommon = useTranslations('common'); 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(t('closed')); 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(t('success')); }, onError: (error: Error) => { setErrorMessage(error.message || t('failed')); 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(t('codeSent')); if (capEnabled) { setCapScope('register'); capTokenRef.current = null; setCapReady(false); setCapResetKey((key) => key + 1); } }, onError: (error: Error) => { toast.error(error.message || t('sendCodeFailed')); 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(t('emailRequired')); return; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { toast.error(t('emailInvalid')); return; } if (capEnabled && capScope === 'send_email_code' && !capReady) { toast.error(capAutoSolve ? t('capPending') : t('capRequired')); return; } sendRegisterCodeMutation.mutate(trimmedEmail); }; const handleRegister = () => { setErrorMessage(''); if (!username.trim() || !password) { toast.error(t('usernamePasswordRequired')); return; } if (password.length < 8) { toast.error(t('passwordTooShort')); return; } const trimmedEmail = email.trim(); if (!trimmedEmail) { toast.error(t('emailEmpty')); return; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { toast.error(t('emailInvalid')); return; } if (emailRegisterEnabled && !code.trim()) { toast.error(t('codeRequired')); return; } if (capEnabled && capScope === 'register' && !capReady) { toast.error(capAutoSolve ? t('capPending') : t('capRequired')); 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 (
{t('username')} * setUsername(e.target.value)} placeholder={t('usernamePlaceholder')} autoComplete='username' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> {t('nickname')} {tCommon('optional')} setNickname(e.target.value)} placeholder={t('nicknamePlaceholder')} autoComplete='nickname' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> {t('email')} * setEmail(e.target.value)} placeholder={t('emailPlaceholder')} autoComplete='email' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> {t('password')} * setPassword(e.target.value)} type='password' placeholder={t('passwordPlaceholder')} autoComplete='new-password' className='h-10 text-sm [@media(max-height:700px)]:h-9' onKeyDown={(e) => e.key === 'Enter' && handleRegister()} /> {emailRegisterEnabled && ( {t('emailCode')} *
setCode(e.target.value)} placeholder={t('emailCodePlaceholder')} 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}
{t('hasAccount')}{' '} {t('backToLogin')}
); }