feat: add LoginCallback page and route for OAuth redirect handling

This commit is contained in:
2026-04-03 13:17:53 +08:00
parent 9316795e4f
commit a7027c8aaa
2 changed files with 69 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import { ThemeProvider } from './lib/theme';
import { I18nProvider } from './lib/i18n';
import Login from './pages/Login';
import Register from './pages/Register';
import LoginCallback from './pages/LoginCallback';
import Layout from './pages/Layout';
import Projects from './pages/Projects';
import ProjectDetail from './pages/ProjectDetail';
@@ -22,6 +23,7 @@ export default function App() {
<Route path="/" element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/login/callback" element={<LoginCallback />} />
<Route path="/dashboard" element={<Layout />}>
<Route index element={<Projects />} />
<Route path="projects/:id" element={<ProjectDetail />} />

View File

@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { useAuth } from '../lib/auth';
import { useI18n } from '../lib/i18n';
export default function LoginCallback() {
const [searchParams] = useSearchParams();
const [error, setError] = useState('');
const { loginWithTokens } = useAuth();
const navigate = useNavigate();
const { t } = useI18n();
useEffect(() => {
const accessToken = searchParams.get('accessToken');
const refreshToken = searchParams.get('refreshToken');
const errorParam = searchParams.get('error');
if (errorParam) {
setError(errorParam);
return;
}
if (!accessToken || !refreshToken) {
setError('Missing authentication tokens');
return;
}
// Clear tokens from URL immediately
window.history.replaceState({}, '', '/login/callback');
loginWithTokens(accessToken, refreshToken)
.then(() => navigate('/dashboard', { replace: true }))
.catch((err) => setError(err instanceof Error ? err.message : 'Authentication failed'));
}, []);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-bg-primary">
<div className="text-center max-w-sm mx-4">
<div className="w-12 h-12 rounded-full bg-danger-muted mx-auto flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-danger" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<circle cx="12" cy="12" r="10" />
<path d="M15 9l-6 6m0-6l6 6" />
</svg>
</div>
<h1 className="text-lg font-semibold text-text-primary mb-2">{t('auth.callback.error')}</h1>
<p className="text-[13px] text-text-muted mb-6">{error}</p>
<Link to="/login" className="btn-primary inline-block px-6">
{t('auth.callback.retry')}
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-bg-primary">
<div className="text-center">
<svg className="w-8 h-8 animate-spin text-accent mx-auto mb-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<p className="text-[13px] text-text-muted">{t('auth.callback.loading')}</p>
</div>
</div>
);
}