| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import React, { useEffect, useState } from 'react';
- import { useNavigate, useSearchParams } from 'react-router-dom';
- import { authService } from '../services/authService';
- import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
- const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8010';
- /**
- * SSO 回调页面
- *
- * 支持两种模式:
- * 1. OAuth2 授权码模式:统一认证平台跳转携带 code 参数
- * URL: /sso-callback?code=xxx
- * 2. 旧模式兼容:携带 sso_token 参数(直接验证 token)
- * URL: /sso-callback?sso_token=xxx&redirect=/models
- */
- const SSOCallback: React.FC = () => {
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
- const [message, setMessage] = useState('正在验证身份...');
- useEffect(() => {
- const code = searchParams.get('code');
- const ssoToken = searchParams.get('sso_token');
- let redirect = searchParams.get('redirect') || '/';
- // 安全归一化
- const forbidden = ['/login', '/register', '/sso-callback'];
- if (!redirect || forbidden.includes(redirect)) {
- redirect = '/';
- }
- if (code) {
- // OAuth2 授权码模式:用 code 换取本地 token
- handleOAuth2Code(code, redirect);
- } else if (ssoToken) {
- // 旧模式兼容:直接用 sso_token 登录
- handleSsoToken(ssoToken, redirect);
- } else {
- setStatus('error');
- setMessage('缺少认证参数,请从统一认证平台重新登录');
- setTimeout(() => navigate('/login'), 3000);
- }
- }, []);
- const handleOAuth2Code = async (code: string, redirect: string) => {
- try {
- const response = await fetch(`${API_BASE}/api/oauth/exchange-code`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ code })
- });
- const result = await response.json();
- if (result.code === 200 && result.data?.token) {
- // 保存 token 和用户信息
- const { token, user } = result.data;
- authService.setToken(token, user);
- setStatus('success');
- setMessage('登录成功,正在跳转...');
- setTimeout(() => navigate(redirect, { replace: true }), 1000);
- } else {
- setStatus('error');
- setMessage(result.detail || result.message || 'SSO 登录失败');
- setTimeout(() => navigate('/login'), 3000);
- }
- } catch (err) {
- setStatus('error');
- setMessage(err instanceof Error ? err.message : 'SSO 登录失败,请稍后重试');
- setTimeout(() => navigate('/login'), 3000);
- }
- };
- const handleSsoToken = async (ssoToken: string, redirect: string) => {
- try {
- const response = await authService.ssoLogin(ssoToken);
- if (response.code === 200) {
- setStatus('success');
- setMessage('登录成功,正在跳转...');
- setTimeout(() => navigate(redirect, { replace: true }), 1000);
- } else {
- setStatus('error');
- setMessage(response.message || 'SSO 登录失败');
- setTimeout(() => navigate('/login'), 3000);
- }
- } catch (err) {
- setStatus('error');
- setMessage(err instanceof Error ? err.message : 'SSO 登录失败,请稍后重试');
- setTimeout(() => navigate('/login'), 3000);
- }
- };
- return (
- <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-indigo-50 px-4">
- <div className="w-full max-w-md">
- <div className="bg-white rounded-2xl shadow-2xl border border-gray-100 p-12 text-center">
- {status === 'loading' && (
- <>
- <Loader2 className="w-16 h-16 text-blue-600 animate-spin mx-auto mb-6" />
- <h2 className="text-xl font-bold text-gray-900 mb-2">正在验证身份</h2>
- <p className="text-sm text-gray-500">{message}</p>
- </>
- )}
- {status === 'success' && (
- <>
- <CheckCircle2 className="w-16 h-16 text-green-500 mx-auto mb-6" />
- <h2 className="text-xl font-bold text-gray-900 mb-2">登录成功</h2>
- <p className="text-sm text-gray-500">{message}</p>
- </>
- )}
- {status === 'error' && (
- <>
- <AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-6" />
- <h2 className="text-xl font-bold text-gray-900 mb-2">登录失败</h2>
- <p className="text-sm text-gray-500 mb-4">{message}</p>
- <p className="text-xs text-gray-400">3秒后自动跳转到登录页面</p>
- </>
- )}
- </div>
- </div>
- </div>
- );
- };
- export default SSOCallback;
|