From 052881df84b27ccf1b00fa1e5c4cdaf08b1fc531 Mon Sep 17 00:00:00 2001 From: Tobias Wilken Date: Tue, 23 Dec 2025 07:29:46 +0100 Subject: [PATCH] feat: implement OAuth login flow via core API - Update useAuth to fetch OAuth URL from /api/auth/url and redirect - Add AuthCallback page to handle GitHub OAuth callback - Update proxy to set httpOnly cookie on /api/auth/callback response Works with core's new /api/auth/url and /api/auth/callback endpoints. --- server/proxy.js | 4 +- src/App.jsx | 2 + src/hooks/useAuth.js | 17 ++++++- src/pages/AuthCallback/index.jsx | 86 ++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/pages/AuthCallback/index.jsx diff --git a/server/proxy.js b/server/proxy.js index a22e51c..be7891f 100644 --- a/server/proxy.js +++ b/server/proxy.js @@ -56,8 +56,8 @@ router.all('/{*path}', async (req, res) => { } }); - // Handle login route - set httpOnly cookie - if (req.url === '/auth/login' && response.ok) { + // Handle auth callback - set httpOnly cookie + if (req.url === '/auth/callback' && response.ok) { const data = await response.json(); if (data.sessionId) { res.cookie('sessionId', data.sessionId, { diff --git a/src/App.jsx b/src/App.jsx index e6e62b0..2240fec 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,6 +6,7 @@ import { DashboardPage } from './pages/DashboardPage'; import { RepositoryPage } from './pages/RepositoryPage'; import { PullRequestPage } from './pages/PullRequestPage'; import { AdminPage } from './pages/AdminPage'; +import { AuthCallback } from './pages/AuthCallback'; function App() { return ( @@ -13,6 +14,7 @@ function App() { }> } /> + } /> { - window.location.href = '/login'; + const login = useCallback(async () => { + try { + const callbackUrl = `${window.location.origin}/auth/callback`; + const response = await fetch( + `/api/auth/url?redirect_uri=${encodeURIComponent(callbackUrl)}` + ); + if (response.ok) { + const data = await response.json(); + window.location.href = data.url; + } else { + console.error('Failed to get OAuth URL:', response.status); + } + } catch (error) { + console.error('Login failed:', error); + } }, []); return { user, authenticated, loading, login, logout, checkAuth }; diff --git a/src/pages/AuthCallback/index.jsx b/src/pages/AuthCallback/index.jsx new file mode 100644 index 0000000..617f326 --- /dev/null +++ b/src/pages/AuthCallback/index.jsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 50vh; + text-align: center; +`; + +const Message = styled.p` + font-size: 1.2rem; + color: var(--color-text); +`; + +const ErrorMessage = styled.p` + font-size: 1.2rem; + color: #dc3545; +`; + +export function AuthCallback() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [error, setError] = useState(null); + + useEffect(() => { + async function handleCallback() { + const code = searchParams.get('code'); + const oauthError = searchParams.get('error'); + + if (oauthError) { + setError('Authentication was denied'); + return; + } + + if (!code) { + setError('No authorization code received'); + return; + } + + try { + const callbackUrl = `${window.location.origin}/auth/callback`; + const response = await fetch('/api/auth/callback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + code, + redirect_uri: callbackUrl, + }), + }); + + if (response.ok) { + navigate('/dashboard'); + } else { + const data = await response.json().catch(() => ({})); + setError(data.error || 'Authentication failed'); + } + } catch (err) { + console.error('Auth callback error:', err); + setError('Authentication failed'); + } + } + + handleCallback(); + }, [searchParams, navigate]); + + if (error) { + return ( + + {error} + Return to home + + ); + } + + return ( + + Completing authentication... + + ); +}