143 lines
6.0 KiB
TypeScript
143 lines
6.0 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useEffect, useState } from 'react';
|
|||
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|||
|
|
import { oidcClient } from '@/lib/auth/oidc-client';
|
|||
|
|
import { TokenManager } from '@/lib/auth/token-manager';
|
|||
|
|
import { useAuth } from '@/components/providers/auth-provider';
|
|||
|
|
import { useToken } from '@/components/providers/token-provider';
|
|||
|
|
|
|||
|
|
export default function AuthCallbackPage() {
|
|||
|
|
const router = useRouter();
|
|||
|
|
const searchParams = useSearchParams();
|
|||
|
|
const { loadUser } = useAuth();
|
|||
|
|
const { forceRefresh } = useToken();
|
|||
|
|
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
|||
|
|
const [message, setMessage] = useState('正在处理授权回调...');
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleCallback = async () => {
|
|||
|
|
try {
|
|||
|
|
// 获取 URL 参数
|
|||
|
|
const code = searchParams.get('code');
|
|||
|
|
const state = searchParams.get('state');
|
|||
|
|
const error = searchParams.get('error');
|
|||
|
|
|
|||
|
|
// 检查是否有错误
|
|||
|
|
if (error) {
|
|||
|
|
throw new Error(searchParams.get('error_description') || error);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查必需参数
|
|||
|
|
if (!code || !state) {
|
|||
|
|
throw new Error('授权回调参数不完整');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证状态值(防CSRF)
|
|||
|
|
const savedState = sessionStorage.getItem('oauth_state');
|
|||
|
|
const savedRedirectUri = sessionStorage.getItem('oauth_redirect_uri');
|
|||
|
|
|
|||
|
|
if (state !== savedState) {
|
|||
|
|
throw new Error('状态验证失败,可能存在安全风险');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!savedRedirectUri) {
|
|||
|
|
throw new Error('未找到保存的回调地址');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setMessage('正在交换授权码...');
|
|||
|
|
|
|||
|
|
// 使用授权码获取令牌
|
|||
|
|
const tokenResponse = await oidcClient.handleAuthorizationCallback(
|
|||
|
|
code,
|
|||
|
|
savedRedirectUri,
|
|||
|
|
state
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
setMessage('正在保存令牌...');
|
|||
|
|
|
|||
|
|
// 保存令牌
|
|||
|
|
TokenManager.saveTokens(tokenResponse);
|
|||
|
|
|
|||
|
|
setMessage('正在更新认证状态...');
|
|||
|
|
|
|||
|
|
// 同步令牌状态和认证状态
|
|||
|
|
await forceRefresh();
|
|||
|
|
await loadUser();
|
|||
|
|
|
|||
|
|
setMessage('登录成功,正在跳转...');
|
|||
|
|
setStatus('success');
|
|||
|
|
|
|||
|
|
// 清理存储的状态
|
|||
|
|
sessionStorage.removeItem('oauth_state');
|
|||
|
|
sessionStorage.removeItem('oauth_redirect_uri');
|
|||
|
|
|
|||
|
|
// 延迟跳转,让用户看到成功信息
|
|||
|
|
setTimeout(() => {
|
|||
|
|
// 获取保存的返回URL,如果没有则跳转到dashboard
|
|||
|
|
const savedReturnUrl = sessionStorage.getItem('oauth_return_url') || '/dashboard';
|
|||
|
|
sessionStorage.removeItem('oauth_return_url'); // 清理
|
|||
|
|
router.push(savedReturnUrl);
|
|||
|
|
}, 1500);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('授权回调失败:', error);
|
|||
|
|
setStatus('error');
|
|||
|
|
setMessage(error instanceof Error ? error.message : '授权失败');
|
|||
|
|
|
|||
|
|
// 清理存储的状态
|
|||
|
|
sessionStorage.removeItem('oauth_state');
|
|||
|
|
sessionStorage.removeItem('oauth_redirect_uri');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
handleCallback();
|
|||
|
|
}, [searchParams, router, loadUser, forceRefresh]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="flex items-center justify-center">
|
|||
|
|
<div className="max-w-md w-full space-y-8">
|
|||
|
|
<div className="text-center">
|
|||
|
|
{status === 'loading' && (
|
|||
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500 mx-auto mb-4"></div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{status === 'success' && (
|
|||
|
|
<div className="rounded-full h-12 w-12 bg-green-100 mx-auto mb-4 flex items-center justify-center">
|
|||
|
|
<svg className="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{status === 'error' && (
|
|||
|
|
<div className="rounded-full h-12 w-12 bg-red-100 mx-auto mb-4 flex items-center justify-center">
|
|||
|
|
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
|||
|
|
{status === 'loading' && '处理中'}
|
|||
|
|
{status === 'success' && '登录成功'}
|
|||
|
|
{status === 'error' && '登录失败'}
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
<p className="mt-2 text-center text-sm text-gray-600">
|
|||
|
|
{message}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{status === 'error' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => router.push('/auth/login')}
|
|||
|
|
className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|||
|
|
>
|
|||
|
|
返回重试
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|