feat: add LoginCallback page and route for OAuth redirect handling
This commit is contained in:
@@ -5,6 +5,7 @@ import { ThemeProvider } from './lib/theme';
|
|||||||
import { I18nProvider } from './lib/i18n';
|
import { I18nProvider } from './lib/i18n';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Register from './pages/Register';
|
import Register from './pages/Register';
|
||||||
|
import LoginCallback from './pages/LoginCallback';
|
||||||
import Layout from './pages/Layout';
|
import Layout from './pages/Layout';
|
||||||
import Projects from './pages/Projects';
|
import Projects from './pages/Projects';
|
||||||
import ProjectDetail from './pages/ProjectDetail';
|
import ProjectDetail from './pages/ProjectDetail';
|
||||||
@@ -22,6 +23,7 @@ export default function App() {
|
|||||||
<Route path="/" element={<LandingPage />} />
|
<Route path="/" element={<LandingPage />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/login/callback" element={<LoginCallback />} />
|
||||||
<Route path="/dashboard" element={<Layout />}>
|
<Route path="/dashboard" element={<Layout />}>
|
||||||
<Route index element={<Projects />} />
|
<Route index element={<Projects />} />
|
||||||
<Route path="projects/:id" element={<ProjectDetail />} />
|
<Route path="projects/:id" element={<ProjectDetail />} />
|
||||||
|
|||||||
67
packages/web/src/pages/LoginCallback.tsx
Normal file
67
packages/web/src/pages/LoginCallback.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user