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:
2026-05-14 17:28:15 +02:00
parent 6aea953734
commit 6f18dd24a5
27 changed files with 1469 additions and 43 deletions
+30
View File
@@ -0,0 +1,30 @@
import { useEffect } from 'react'
import { useAuthStore } from '../stores/auth'
import { api } from '../api/client'
export function AuthProvider({ children }: { children: React.ReactNode }) {
const { setUser, setError, setState } = useAuthStore()
useEffect(() => {
const initAuth = async () => {
try {
const token = localStorage.getItem('access_token')
if (!token) {
setState('unauthenticated')
return
}
const user = await api.getCurrentUser()
setUser(user)
} catch (error) {
console.error('Auth initialization failed:', error)
setError(error instanceof Error ? error : new Error('Auth failed'))
localStorage.removeItem('access_token')
}
}
initAuth()
}, [setUser, setError, setState])
return <>{children}</>
}
+47
View File
@@ -0,0 +1,47 @@
// PKCE utilities for OIDC flow
function generateRandomString(length: number): string {
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let text = ''
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(verifier)
const digest = await crypto.subtle.digest('SHA-256', data)
const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)))
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}
export async function createPKCE(): Promise<{ verifier: string; challenge: string }> {
const verifier = generateRandomString(128)
const challenge = await generateCodeChallenge(verifier)
return { verifier, challenge }
}
export function storePKCE(verifier: string): void {
sessionStorage.setItem('pkce_verifier', verifier)
}
export function getPKCE(): string | null {
return sessionStorage.getItem('pkce_verifier')
}
export function clearPKCE(): void {
sessionStorage.removeItem('pkce_verifier')
}
export function storeToken(token: string): void {
localStorage.setItem('access_token', token)
}
export function getToken(): string | null {
return localStorage.getItem('access_token')
}
export function clearToken(): void {
localStorage.removeItem('access_token')
}