feat(FN-005): implement frontend foundation with auth and routing
- Add OIDC authentication with PKCE flow - Create dashboard shell with sidebar and header - Implement project management UI (list, create, detail) - Add tool spawn page with tool/project selection - Create tool instance detail page with status and controls - Set up React Router with route guards - Add Zustand auth store and API client with types
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { getPKCE, clearPKCE, storeToken } from '../auth/oidc'
|
||||
import { api } from '../api/client'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const OIDC_ISSUER = import.meta.env.VITE_OIDC_ISSUER
|
||||
const CLIENT_ID = import.meta.env.VITE_OIDC_CLIENT_ID
|
||||
const REDIRECT_URI = import.meta.env.VITE_OIDC_REDIRECT_URI
|
||||
|
||||
export default function CallbackPage() {
|
||||
const navigate = useNavigate()
|
||||
const { setUser, setError } = useAuthStore()
|
||||
const [status, setStatus] = useState('Processing authentication...')
|
||||
|
||||
useEffect(() => {
|
||||
const handleCallback = async () => {
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
const error = urlParams.get('error')
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Authentication error: ${error}`)
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No authorization code received')
|
||||
}
|
||||
|
||||
const verifier = getPKCE()
|
||||
if (!verifier) {
|
||||
throw new Error('PKCE verifier not found')
|
||||
}
|
||||
|
||||
setStatus('Exchanging code for token...')
|
||||
|
||||
const tokenResponse = await fetch(`${OIDC_ISSUER}/token/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: CLIENT_ID,
|
||||
code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
code_verifier: verifier,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Token exchange failed')
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json()
|
||||
storeToken(tokenData.access_token)
|
||||
clearPKCE()
|
||||
|
||||
setStatus('Fetching user information...')
|
||||
const user = await api.getCurrentUser()
|
||||
setUser(user)
|
||||
|
||||
navigate('/')
|
||||
} catch (error) {
|
||||
console.error('Callback error:', error)
|
||||
setError(error instanceof Error ? error : new Error('Authentication failed'))
|
||||
setStatus('Authentication failed')
|
||||
setTimeout(() => navigate('/login'), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
handleCallback()
|
||||
}, [navigate, setUser, setError])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">{status}</h1>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user