@@ -6,158 +6,161 @@ import { CheckCircle, Loader2, AlertCircle } from 'lucide-react';
66
77import { useGitHubLogin , useTokenLogin } from '@/hooks/useAuth' ;
88
9+ const DEFAULT_REDIRECT = '/dashboard' ;
10+ const REDIRECT_STORAGE_KEY = 'auth_redirect' ;
11+
12+ function safeDecode ( value : string | null ) : string | null {
13+ if ( ! value ) return null ;
14+
15+ try {
16+ return decodeURIComponent ( value ) ;
17+ } catch {
18+ return value ;
19+ }
20+ }
21+
22+ function parseOptionalInt ( value : string | null ) : number | undefined {
23+ if ( value == null || value === '' ) return undefined ;
24+
25+ const n = Number ( value ) ;
26+
27+ return Number . isFinite ( n ) ? n : undefined ;
28+ }
29+
30+ type CallbackState = 'loading' | 'success' | 'error' ;
31+
932function CallbackContent ( ) {
1033 const [ status , setStatus ] = useState ( '处理中...' ) ;
11- const [ state , setState ] = useState < 'loading' | 'success' | 'error' > ( 'loading' ) ;
34+ const [ uiState , setUiState ] = useState < CallbackState > ( 'loading' ) ;
1235 const [ mounted , setMounted ] = useState ( false ) ;
13- const [ authProcessed , setAuthProcessed ] = useState ( false ) ; // 添加认证处理标记
36+ const [ processed , setProcessed ] = useState ( false ) ;
1437 const searchParams = useSearchParams ( ) ;
1538 const router = useRouter ( ) ;
1639
1740 const gitHubLoginMutation = useGitHubLogin ( ) ;
1841 const tokenLoginMutation = useTokenLogin ( ) ;
1942
20- // 确保组件在客户端挂载
2143 useEffect ( ( ) => {
2244 setMounted ( true ) ;
2345 } , [ ] ) ;
2446
25- // 获取重定向 URL
2647 const getRedirectUrl = ( ) : string => {
27- // 1. 优先从 state 参数获取(GitHub OAuth 标准)
28- const state = searchParams ?. get ( 'state' ) ;
48+ const stateParam = safeDecode ( searchParams ?. get ( 'state' ) ?? null ) ;
2949
30- if ( state ) {
31- try {
32- return decodeURIComponent ( state ) ;
33- } catch { }
50+ if ( stateParam ) {
51+ return stateParam ;
3452 }
3553
36- // 2. 从 redirect_to 参数获取
37- const redirectTo = searchParams ?. get ( 'redirect_to' ) ;
54+ const redirectTo = safeDecode ( searchParams ?. get ( 'redirect_to' ) ?? null ) ;
3855
3956 if ( redirectTo ) {
40- try {
41- return decodeURIComponent ( redirectTo ) ;
42- } catch { }
57+ return redirectTo ;
4358 }
4459
45- // 3. 从 sessionStorage 获取(仅客户端)
46- if ( mounted && typeof window !== 'undefined' ) {
47- try {
48- const saved = sessionStorage . getItem ( 'auth_redirect' ) ;
60+ if ( typeof window !== 'undefined' ) {
61+ const saved = sessionStorage . getItem ( REDIRECT_STORAGE_KEY ) ;
4962
50- if ( saved ) {
51- sessionStorage . removeItem ( 'auth_redirect' ) ;
63+ if ( saved ) {
64+ sessionStorage . removeItem ( REDIRECT_STORAGE_KEY ) ;
5265
53- return saved ;
54- }
55- } catch { }
66+ return saved ;
67+ }
5668 }
5769
58- // 4. 默认跳转到仪表盘
59- return '/dashboard' ;
70+ return DEFAULT_REDIRECT ;
6071 } ;
6172
6273 useEffect ( ( ) => {
63- if ( ! mounted || authProcessed || ! searchParams ) return ;
64-
65- const processAuth = async ( ) => {
66- setAuthProcessed ( true ) ;
67-
68- try {
69- // 场景1: 直接 Token 登录
70- const token = searchParams . get ( 'token' ) ;
71-
72- if ( token ) {
73- const authData = {
74- token,
75- refresh_token : searchParams . get ( 'refresh_token' ) || undefined ,
76- expires_in : searchParams . get ( 'expires_in' )
77- ? parseInt ( searchParams . get ( 'expires_in' ) ! )
78- : undefined ,
79- refresh_expires_in : searchParams . get ( 'refresh_expires_in' )
80- ? parseInt ( searchParams . get ( 'refresh_expires_in' ) ! )
81- : undefined ,
82- } ;
83-
84- setStatus ( '登录成功,正在跳转...' ) ;
85- setState ( 'success' ) ;
86-
87- tokenLoginMutation . mutate ( {
88- authData,
89- redirectUrl : getRedirectUrl ( ) ,
90- } ) ;
91-
92- return ;
93- }
94-
95- // 场景2: GitHub OAuth 授权码登录
96- const code = searchParams . get ( 'code' ) ;
97-
98- if ( ! code ) {
99- setStatus ( '缺少授权码,请重新登录' ) ;
100- setState ( 'error' ) ;
101-
102- return ;
103- }
104-
105- setStatus ( '正在验证授权...' ) ;
106-
107- gitHubLoginMutation . mutate (
108- {
109- code,
110- redirectUrl : getRedirectUrl ( ) ,
74+ if ( ! mounted || processed ) return ;
75+
76+ setProcessed ( true ) ;
77+
78+ const redirectUrl = getRedirectUrl ( ) ;
79+ const token = searchParams . get ( 'token' ) ;
80+
81+ if ( token ) {
82+ setStatus ( '登录成功,正在跳转...' ) ;
83+ setUiState ( 'success' ) ;
84+ tokenLoginMutation . mutate ( {
85+ authData : {
86+ token,
87+ refresh_token : searchParams . get ( 'refresh_token' ) ?? undefined ,
88+ expires_in : parseOptionalInt ( searchParams . get ( 'expires_in' ) ) ,
89+ refresh_expires_in : parseOptionalInt ( searchParams . get ( 'refresh_expires_in' ) ) ,
90+ } ,
91+ redirectUrl,
92+ } ) ;
93+
94+ return ;
95+ }
96+
97+ const code = searchParams . get ( 'code' ) ;
98+
99+ if ( code ) {
100+ setStatus ( '正在验证授权...' ) ;
101+ gitHubLoginMutation . mutate (
102+ { code, redirectUrl } ,
103+ {
104+ onSuccess : ( ) => {
105+ setStatus ( '登录成功,正在跳转...' ) ;
106+ setUiState ( 'success' ) ;
111107 } ,
112- {
113- onSuccess : ( ) => {
114- setStatus ( '登录成功,正在跳转...' ) ;
115- setState ( 'success' ) ;
116- } ,
117- onError : ( error ) => {
118- const message = error instanceof Error ? error . message : String ( error ) ;
119- setStatus ( `认证失败: ${ message } ` ) ;
120- setState ( 'error' ) ;
121- } ,
108+ onError : ( error ) => {
109+ setStatus ( `认证失败:${ error instanceof Error ? error . message : String ( error ) } ` ) ;
110+ setUiState ( 'error' ) ;
122111 } ,
123- ) ;
124- } catch ( error ) {
125- const message = error instanceof Error ? error . message : '未知错误' ;
126- setStatus ( `登录失败: ${ message } ` ) ;
127- setState ( 'error' ) ;
128- }
129- } ;
112+ } ,
113+ ) ;
114+
115+ return ;
116+ }
117+
118+ setStatus ( '缺少授权信息,请重新登录' ) ;
119+ setUiState ( 'error' ) ;
120+ } , [ mounted , processed , searchParams ] ) ;
130121
131- processAuth ( ) ;
132- } , [ mounted , authProcessed , searchParams ] ) ;
122+ const handleManualRedirect = ( ) => router . push ( getRedirectUrl ( ) ) ;
133123
134124 return (
135125 < div className = "min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-50" >
136126 < div className = "w-full max-w-md p-10 space-y-6 bg-white rounded-2xl shadow-xl border border-gray-100" >
137127 < div className = "text-center" >
138- < h1 className = "text-2xl font-bold text-gray-800 mb-2" > GitHub认证</ h1 >
139-
140- < div className = "flex flex-col items-center justify-center space-y-4 mt-6" >
141- { state === 'loading' && (
128+ < h1 className = "text-2xl font-bold text-gray-800 mb-2" > 登录</ h1 >
129+
130+ < div
131+ className = "flex flex-col items-center justify-center space-y-4 mt-6"
132+ role = "status"
133+ aria-live = "polite"
134+ aria-label = { status }
135+ >
136+ { uiState === 'loading' && (
142137 < div className = "flex flex-col items-center" >
143- < Loader2 className = "h-12 w-12 text-blue-500 animate-spin mb-4" />
138+ < Loader2 className = "h-12 w-12 text-blue-500 animate-spin mb-4" aria-hidden />
144139 < p className = "text-lg font-medium text-gray-700" > { status } </ p >
145140 </ div >
146141 ) }
147142
148- { state === 'success' && (
149- < div className = "flex flex-col items-center" >
150- < CheckCircle className = "h-14 w-14 text-green-500 mb-4" />
143+ { uiState === 'success' && (
144+ < div className = "flex flex-col items-center gap-3 " >
145+ < CheckCircle className = "h-14 w-14 text-green-500 mb-4" aria-hidden />
151146 < p className = "text-lg font-medium text-gray-700" > { status } </ p >
147+ < button
148+ type = "button"
149+ className = "text-sm text-blue-600 hover:text-blue-800 underline focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
150+ onClick = { handleManualRedirect }
151+ >
152+ 若未自动跳转,请点击此处
153+ </ button >
152154 </ div >
153155 ) }
154156
155- { state === 'error' && (
157+ { uiState === 'error' && (
156158 < div className = "flex flex-col items-center" >
157- < AlertCircle className = "h-14 w-14 text-red-500 mb-4" />
159+ < AlertCircle className = "h-14 w-14 text-red-500 mb-4" aria-hidden />
158160 < p className = "text-lg font-medium text-gray-700 text-center mb-4" > { status } </ p >
159161 < button
160- className = "py-2.5 px-6 bg-gray-900 hover:bg-gray-800 text-white rounded-xl transition-all duration-200 cursor-pointer"
162+ type = "button"
163+ className = "py-2.5 px-6 bg-gray-900 hover:bg-gray-800 text-white rounded-xl transition-colors focus:outline-none focus:ring-2 focus:ring-gray-700 focus:ring-offset-2"
161164 onClick = { ( ) => router . push ( '/auth' ) }
162165 >
163166 返回登录
@@ -171,23 +174,23 @@ function CallbackContent() {
171174 ) ;
172175}
173176
174- export default function AuthCallback ( ) {
175- return (
176- < Suspense
177- fallback = {
178- < div className = "min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-50" >
179- < div className = "w-full max-w-md p-10 space-y-6 bg-white rounded-2xl shadow-xl border border-gray-100" >
180- < div className = "text-center" >
181- < h1 className = "text-2xl font-bold text-gray-800 mb-2" > GitHub认证</ h1 >
182- < div className = "flex flex-col items-center justify-center mt-6" >
183- < Loader2 className = "h-12 w-12 text-blue-500 animate-spin mb-4" />
184- < p className = "text-lg font-medium text-gray-700" > 加载中...</ p >
185- </ div >
186- </ div >
187- </ div >
177+ const LoadingFallback = ( ) => (
178+ < div className = "min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-50" >
179+ < div className = "w-full max-w-md p-10 space-y-6 bg-white rounded-2xl shadow-xl border border-gray-100" >
180+ < div className = "text-center" >
181+ < h1 className = "text-2xl font-bold text-gray-800 mb-2" > 登录</ h1 >
182+ < div className = "flex flex-col items-center justify-center mt-6" >
183+ < Loader2 className = "h-12 w-12 text-blue-500 animate-spin mb-4" />
184+ < p className = "text-lg font-medium text-gray-700" > 加载中...</ p >
188185 </ div >
189- }
190- >
186+ </ div >
187+ </ div >
188+ </ div >
189+ ) ;
190+
191+ export default function AuthCallbackPage ( ) {
192+ return (
193+ < Suspense fallback = { < LoadingFallback /> } >
191194 < CallbackContent />
192195 </ Suspense >
193196 ) ;
0 commit comments