'use client'; import {zodResolver} from '@hookform/resolvers/zod'; import {useMutation, useQuery} from '@tanstack/react-query'; import Link from 'next/link'; import {useRouter, useSearchParams} from 'next/navigation'; import {useEffect, useRef, useState} from 'react'; import {useForm} from 'react-hook-form'; import {z} from 'zod'; import {InlineMessage} from '@/components/feedback/inline-message'; import {useAuth} from '@/components/providers/auth-provider'; import {AppCard} from '@/components/ui/app-card'; import {getOAuthAuthorizeUrl, login} from '@/features/auth/api/auth'; import {getPublicStatus} from '@/features/auth/api/public'; import {AuthButton, AuthFormField, AuthInput, SecondaryButton,} from '@/features/auth/components/auth-form-primitives'; import {PublicAuthGuard} from '@/features/auth/components/public-auth-guard'; declare module 'react' { /* eslint-disable-next-line @typescript-eslint/no-namespace */ namespace JSX { interface IntrinsicElements { 'cap-widget': React.DetailedHTMLProps< React.HTMLAttributes & { ref?: React.RefObject | React.Ref; onsolve?: (e: CustomEvent<{ token: string }>) => void; 'data-cap-api-endpoint'?: string; }, HTMLElement >; } } } const TEXT = { usernameRequired: '\u8bf7\u8f93\u5165\u7528\u6237\u540d', passwordRequired: '\u8bf7\u8f93\u5165\u5bc6\u7801', loginFailed: '\u767b\u5f55\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002', oauthUnavailable: '第三方登录当前不可用。', title: '\u7528\u6237\u767b\u5f55', username: '\u7528\u6237\u540d', password: '\u5bc6\u7801', loginPending: '\u767b\u5f55\u4e2d...', login: '\u767b\u5f55', forgotPassword: '\u5fd8\u8bb0\u5bc6\u7801\uff1f', register: '\u6ce8\u518c', }; const loginSchema = z.object({ username: z.string().min(1, TEXT.usernameRequired), password: z.string().min(1, TEXT.passwordRequired), }); type LoginFormValues = z.infer; export function LoginForm() { const router = useRouter(); const searchParams = useSearchParams(); const { setUser } = useAuth(); const [errorMessage, setErrorMessage] = useState(''); const [capToken, setCapToken] = useState(''); const capWidgetRef = useRef void }>(null); const redirect = searchParams?.get('redirect') || '/'; const form = useForm({ resolver: zodResolver(loginSchema), defaultValues: { username: '', password: '', }, }); const statusQuery = useQuery({ queryKey: ['public-status'], queryFn: getPublicStatus, }); useEffect(() => { if (statusQuery.data?.cap_login_enabled) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/cap-widget'; script.type = 'module'; script.async = true; document.head.appendChild(script); return () => { document.head.removeChild(script); }; } }, [statusQuery.data?.cap_login_enabled]); const loginMutation = useMutation({ mutationFn: login, onSuccess: (user) => { setUser(user); router.replace(redirect); }, onError: (error: Error) => { setErrorMessage(error.message || TEXT.loginFailed); setCapToken(''); if (capWidgetRef.current && typeof capWidgetRef.current.reset === 'function') { capWidgetRef.current.reset(); } }, }); const oauthMutation = useMutation({ mutationFn: getOAuthAuthorizeUrl, onSuccess: (result) => { window.location.href = result.authorize_url; }, onError: (error: Error) => { setErrorMessage(error.message || TEXT.oauthUnavailable); }, }); const handleSubmit = form.handleSubmit((values) => { setErrorMessage(''); loginMutation.mutate({ ...values, cap_token: capToken || undefined, }); }); const handleOAuthLogin = (sourceName: string) => { setErrorMessage(''); oauthMutation.mutate(sourceName); }; return (
{form.formState.errors.username ? ( {form.formState.errors.username.message} ) : null} {form.formState.errors.password ? ( {form.formState.errors.password.message} ) : null} {statusQuery.data?.cap_login_enabled ? (
) => { setCapToken(e.detail.token); }} />
) : null} {errorMessage ? ( ) : null}
{loginMutation.isPending ? TEXT.loginPending : TEXT.login}
{(statusQuery.data?.auth_sources ?? []).length > 0 ? (
第三方账号登录
{(statusQuery.data?.auth_sources ?? []).map((source) => ( handleOAuthLogin(source.name)} className="min-w-36" disabled={oauthMutation.isPending} > {source.display_name || source.name} 登录 ))}
) : null}
{TEXT.forgotPassword}
); }