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,144 @@
|
||||
import type { Project, ProjectCreate, ProjectUpdate, ToolDefinition, ToolInstance, User } from '../types/api.ts'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public statusText: string,
|
||||
public data?: unknown
|
||||
) {
|
||||
super(`API Error ${status}: ${statusText}`)
|
||||
this.name = 'ApiError'
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthToken(): string | null {
|
||||
return localStorage.getItem('access_token')
|
||||
}
|
||||
|
||||
async function fetchWithAuth(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
const url = `${API_URL}/api/v1${endpoint}`
|
||||
const token = getAuthToken()
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...((options.headers as Record<string, string>) || {}),
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`[API] ${options.method || 'GET'} ${url}`)
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`[API] ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => undefined)
|
||||
throw new ApiError(response.status, response.statusText, data)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Auth
|
||||
getCurrentUser: async (): Promise<User> => {
|
||||
const response = await fetchWithAuth('/users/me')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Projects
|
||||
getProjects: async (): Promise<Project[]> => {
|
||||
const response = await fetchWithAuth('/projects')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
getProject: async (id: string): Promise<Project> => {
|
||||
const response = await fetchWithAuth(`/projects/${id}`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
createProject: async (data: ProjectCreate): Promise<Project> => {
|
||||
const response = await fetchWithAuth('/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
updateProject: async (id: string, data: ProjectUpdate): Promise<Project> => {
|
||||
const response = await fetchWithAuth(`/projects/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteProject: async (id: string): Promise<void> => {
|
||||
await fetchWithAuth(`/projects/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
getToolInstances: async (projectId: string): Promise<ToolInstance[]> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
getToolInstance: async (projectId: string, instanceId: string): Promise<ToolInstance> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
createToolInstance: async (projectId: string, data: { tool_definition_id: string; name: string }): Promise<ToolInstance> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteToolInstance: async (projectId: string, instanceId: string): Promise<void> => {
|
||||
await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
stopToolInstance: async (projectId: string, instanceId: string): Promise<ToolInstance> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}/stop`, {
|
||||
method: 'POST',
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
startToolInstance: async (projectId: string, instanceId: string): Promise<ToolInstance> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}/start`, {
|
||||
method: 'POST',
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
getToolInstanceStatus: async (projectId: string, instanceId: string): Promise<{ status: string; subdomain: string; container_id: string }> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}/status`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
getToolRegistry: async (): Promise<ToolDefinition[]> => {
|
||||
const response = await fetchWithAuth('/tools')
|
||||
return response.json()
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user