SSOCallback.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import React, { useEffect, useState } from 'react';
  2. import { useNavigate, useSearchParams } from 'react-router-dom';
  3. import { authService } from '../services/authService';
  4. import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
  5. const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8010';
  6. /**
  7. * SSO 回调页面
  8. *
  9. * 支持两种模式:
  10. * 1. OAuth2 授权码模式:统一认证平台跳转携带 code 参数
  11. * URL: /sso-callback?code=xxx
  12. * 2. 旧模式兼容:携带 sso_token 参数(直接验证 token)
  13. * URL: /sso-callback?sso_token=xxx&redirect=/models
  14. */
  15. const SSOCallback: React.FC = () => {
  16. const navigate = useNavigate();
  17. const [searchParams] = useSearchParams();
  18. const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
  19. const [message, setMessage] = useState('正在验证身份...');
  20. useEffect(() => {
  21. const code = searchParams.get('code');
  22. const ssoToken = searchParams.get('sso_token');
  23. let redirect = searchParams.get('redirect') || '/';
  24. // 安全归一化
  25. const forbidden = ['/login', '/register', '/sso-callback'];
  26. if (!redirect || forbidden.includes(redirect)) {
  27. redirect = '/';
  28. }
  29. if (code) {
  30. // OAuth2 授权码模式:用 code 换取本地 token
  31. handleOAuth2Code(code, redirect);
  32. } else if (ssoToken) {
  33. // 旧模式兼容:直接用 sso_token 登录
  34. handleSsoToken(ssoToken, redirect);
  35. } else {
  36. setStatus('error');
  37. setMessage('缺少认证参数,请从统一认证平台重新登录');
  38. setTimeout(() => navigate('/login'), 3000);
  39. }
  40. }, []);
  41. const handleOAuth2Code = async (code: string, redirect: string) => {
  42. try {
  43. const response = await fetch(`${API_BASE}/api/oauth/exchange-code`, {
  44. method: 'POST',
  45. headers: { 'Content-Type': 'application/json' },
  46. body: JSON.stringify({ code })
  47. });
  48. const result = await response.json();
  49. if (result.code === 200 && result.data?.token) {
  50. // 保存 token 和用户信息
  51. const { token, user } = result.data;
  52. authService.setToken(token, user);
  53. setStatus('success');
  54. setMessage('登录成功,正在跳转...');
  55. setTimeout(() => navigate(redirect, { replace: true }), 1000);
  56. } else {
  57. setStatus('error');
  58. setMessage(result.detail || result.message || 'SSO 登录失败');
  59. setTimeout(() => navigate('/login'), 3000);
  60. }
  61. } catch (err) {
  62. setStatus('error');
  63. setMessage(err instanceof Error ? err.message : 'SSO 登录失败,请稍后重试');
  64. setTimeout(() => navigate('/login'), 3000);
  65. }
  66. };
  67. const handleSsoToken = async (ssoToken: string, redirect: string) => {
  68. try {
  69. const response = await authService.ssoLogin(ssoToken);
  70. if (response.code === 200) {
  71. setStatus('success');
  72. setMessage('登录成功,正在跳转...');
  73. setTimeout(() => navigate(redirect, { replace: true }), 1000);
  74. } else {
  75. setStatus('error');
  76. setMessage(response.message || 'SSO 登录失败');
  77. setTimeout(() => navigate('/login'), 3000);
  78. }
  79. } catch (err) {
  80. setStatus('error');
  81. setMessage(err instanceof Error ? err.message : 'SSO 登录失败,请稍后重试');
  82. setTimeout(() => navigate('/login'), 3000);
  83. }
  84. };
  85. return (
  86. <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-indigo-50 px-4">
  87. <div className="w-full max-w-md">
  88. <div className="bg-white rounded-2xl shadow-2xl border border-gray-100 p-12 text-center">
  89. {status === 'loading' && (
  90. <>
  91. <Loader2 className="w-16 h-16 text-blue-600 animate-spin mx-auto mb-6" />
  92. <h2 className="text-xl font-bold text-gray-900 mb-2">正在验证身份</h2>
  93. <p className="text-sm text-gray-500">{message}</p>
  94. </>
  95. )}
  96. {status === 'success' && (
  97. <>
  98. <CheckCircle2 className="w-16 h-16 text-green-500 mx-auto mb-6" />
  99. <h2 className="text-xl font-bold text-gray-900 mb-2">登录成功</h2>
  100. <p className="text-sm text-gray-500">{message}</p>
  101. </>
  102. )}
  103. {status === 'error' && (
  104. <>
  105. <AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-6" />
  106. <h2 className="text-xl font-bold text-gray-900 mb-2">登录失败</h2>
  107. <p className="text-sm text-gray-500 mb-4">{message}</p>
  108. <p className="text-xs text-gray-400">3秒后自动跳转到登录页面</p>
  109. </>
  110. )}
  111. </div>
  112. </div>
  113. </div>
  114. );
  115. };
  116. export default SSOCallback;