mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 15:06:37 +08:00
199 lines
6.9 KiB
TypeScript
199 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { toast } from 'sonner';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import { LoginForm } from '@/components/auth/login-form';
|
|
import { AuthShell } from '@/components/auth/auth-shell';
|
|
import { Check } from 'lucide-react';
|
|
|
|
import { AuthService } from '@/lib/services/auth';
|
|
import { useAuth } from '@/components/providers/auth-provider';
|
|
import { safeRedirectTarget } from '@/lib/utils';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
/**
|
|
* 登录页面组件
|
|
* 显示登录表单和登录按钮
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <LoginPage />
|
|
* ```
|
|
* @returns {React.ReactNode} 登录页面组件
|
|
*/
|
|
export function LoginPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const { user, loading, setUser } = useAuth();
|
|
const t = useTranslations('auth.login');
|
|
const [showOTP, setShowOTP] = useState(false);
|
|
|
|
/* 处理OAuth回调 */
|
|
const isOAuthCallback = !!(
|
|
searchParams.get('state') && searchParams.get('code')
|
|
);
|
|
const [isProcessingCallback, setIsProcessingCallback] =
|
|
useState(isOAuthCallback);
|
|
const isCheckingSession = !isOAuthCallback && loading;
|
|
|
|
const [loginSuccess, setLoginSuccess] = useState(false);
|
|
const redirectedRef = useRef(false);
|
|
const callbackProcessedRef = useRef(false);
|
|
|
|
const resolveRedirectTarget = useCallback(() => {
|
|
const callbackUrl = searchParams.get('callbackUrl');
|
|
const storedRedirect = sessionStorage.getItem('redirect_after_login');
|
|
const target = callbackUrl || storedRedirect || '/';
|
|
|
|
if (storedRedirect) {
|
|
sessionStorage.removeItem('redirect_after_login');
|
|
}
|
|
|
|
return safeRedirectTarget(target);
|
|
}, [searchParams]);
|
|
|
|
const resolveRedirectTargetRef = useRef(resolveRedirectTarget);
|
|
useEffect(() => {
|
|
resolveRedirectTargetRef.current = resolveRedirectTarget;
|
|
}, [resolveRedirectTarget]);
|
|
|
|
/* 登录页兜底:已登录用户直接跳转 */
|
|
useEffect(() => {
|
|
const state = searchParams.get('state');
|
|
const code = searchParams.get('code');
|
|
|
|
if ((state && code) || loading || !user) {
|
|
return;
|
|
}
|
|
|
|
if (!redirectedRef.current) {
|
|
redirectedRef.current = true;
|
|
router.replace(resolveRedirectTargetRef.current());
|
|
}
|
|
}, [loading, router, searchParams, user]);
|
|
|
|
/* 回调逻辑 */
|
|
useEffect(() => {
|
|
const handleOAuthCallback = async () => {
|
|
const state = searchParams.get('state');
|
|
const code = searchParams.get('code');
|
|
|
|
if (state && code) {
|
|
if (callbackProcessedRef.current) return;
|
|
callbackProcessedRef.current = true;
|
|
|
|
setIsProcessingCallback(true);
|
|
try {
|
|
const result = await AuthService.handleCallback({ state, code });
|
|
if (result.status === 'need_bind') {
|
|
toast.info(t('needBind'));
|
|
setIsProcessingCallback(false);
|
|
router.replace('/login');
|
|
return;
|
|
}
|
|
if (result.user) {
|
|
setUser(result.user);
|
|
}
|
|
setLoginSuccess(true);
|
|
toast.success(
|
|
result.status === 'bound' ? t('bindSuccess') : t('success'),
|
|
);
|
|
|
|
setTimeout(() => {
|
|
if (!redirectedRef.current) {
|
|
redirectedRef.current = true;
|
|
router.replace(resolveRedirectTargetRef.current());
|
|
}
|
|
}, 1500);
|
|
} catch (error) {
|
|
console.error('OAuth callback error:', error);
|
|
toast.error(error instanceof Error ? error.message : t('failed'));
|
|
setIsProcessingCallback(false);
|
|
router.replace('/login');
|
|
}
|
|
}
|
|
};
|
|
handleOAuthCallback();
|
|
}, [router, searchParams, setUser, t]);
|
|
|
|
return (
|
|
<AuthShell wide={showOTP}>
|
|
<div className='w-full'>
|
|
<AnimatePresence mode='wait'>
|
|
{isProcessingCallback || isCheckingSession ? (
|
|
<motion.div
|
|
key={isProcessingCallback ? 'processing' : 'session-check'}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className='w-full'
|
|
>
|
|
{isCheckingSession ? (
|
|
<div className='flex flex-col items-center justify-center gap-4 py-16'>
|
|
<div className='relative'>
|
|
<Spinner className='size-8' />
|
|
</div>
|
|
<div className='flex flex-col gap-2 text-center'>
|
|
<h3 className='font-semibold tracking-tight text-foreground'>
|
|
{t('checkingSession')}
|
|
</h3>
|
|
<p className='text-xs text-muted-foreground'>
|
|
{t('checkingSessionDesc')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : loginSuccess ? (
|
|
<div className='flex flex-col items-center justify-center gap-4 py-16'>
|
|
<motion.div
|
|
initial={{ scale: 0.5, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
className='flex size-8 items-center justify-center rounded-full bg-primary/10 text-primary ring-1 ring-primary/20'
|
|
>
|
|
<Check className='size-6' strokeWidth={3} />
|
|
</motion.div>
|
|
<div className='flex flex-col gap-2 text-center'>
|
|
<h3 className='font-semibold tracking-tight text-foreground'>
|
|
{t('success')}
|
|
</h3>
|
|
<p className='text-xs text-muted-foreground'>
|
|
{t('redirecting')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className='flex flex-col items-center justify-center gap-4 py-16'>
|
|
<div className='relative'>
|
|
<Spinner className='size-8' />
|
|
</div>
|
|
<div className='flex flex-col gap-2 text-center'>
|
|
<h3 className='font-semibold tracking-tight text-foreground'>
|
|
{t('verifyingCredentials')}
|
|
</h3>
|
|
<p className='text-xs text-muted-foreground'>
|
|
{t('establishingSession')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key='login-form-wrapper'
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.4 }}
|
|
className='w-full'
|
|
>
|
|
<LoginForm onOTPStateChange={setShowOTP} />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</AuthShell>
|
|
);
|
|
}
|