Skip to content

Commit cf957ae

Browse files
authored
feat(auth): 登录回调先跳转再拉用户,优化 callback 页与 useAuth (#317)
- useAuth: handleAuthSuccess 先安排跳转再后台 getCurrentUser,避免卡在「正在跳转」 - useAuth: 增加 client-side 未生效时的整页跳转兜底,移除所有 console - auth/callback: 优化回调页逻辑与文案,抽取常量与工具函数,改进无障碍 - types: AuthResponse 恢复简洁注释 Made-with: Cursor
1 parent 00ec75c commit cf957ae

File tree

3 files changed

+236
-264
lines changed

3 files changed

+236
-264
lines changed

apps/DocFlow/src/app/auth/callback/page.tsx

Lines changed: 123 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -6,158 +6,161 @@ import { CheckCircle, Loader2, AlertCircle } from 'lucide-react';
66

77
import { 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+
932
function 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

Comments
 (0)