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,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}</>
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
Reference in New Issue
Block a user