feat: implement docker infrastructure (US-001)
- Add docker-compose.yml with postgres, redis, api, and web services - Add multi-stage Dockerfile for API (Python 3.11) - Add multi-stage Dockerfile for web (Node.js 20 + nginx) - Add Makefile with common development commands - Add .env.example with all required environment variables - Add placeholder pyproject.toml and package.json for builds - Configure health checks for all services - Setup persistent volumes for postgres, redis, and repos - Run services as non-root users
This commit is contained in:
@@ -1,306 +0,0 @@
|
||||
import type { Config, ConfigCreate, Project, ProjectCreate, ProjectUpdate, Repository, RepositoryConnection, RepositoryConnectionCreate, RepositoryCreate, Secret, SecretCreate, ToolDefinition, ToolInstance, User } from '../types/api.ts'
|
||||
import { getToken, clearToken } from '../auth/oidc'
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
let isRefreshing = false
|
||||
let refreshPromise: Promise<string | null> | null = null
|
||||
|
||||
async function refreshToken(): Promise<string | null> {
|
||||
if (isRefreshing && refreshPromise) {
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const token = getToken()
|
||||
if (!token) return null
|
||||
|
||||
const response = await fetch(`${API_URL}/api/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
clearToken()
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.access_token) {
|
||||
localStorage.setItem('access_token', data.access_token)
|
||||
return data.access_token
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
clearToken()
|
||||
return null
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
async function fetchWithAuth(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
const url = `${API_URL}/api/v1${endpoint}`
|
||||
const token = getToken()
|
||||
|
||||
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.status === 401 && token) {
|
||||
const newToken = await refreshToken()
|
||||
if (newToken) {
|
||||
headers['Authorization'] = `Bearer ${newToken}`
|
||||
const retryResponse = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
if (!retryResponse.ok) {
|
||||
const data = await retryResponse.json().catch(() => undefined)
|
||||
throw new ApiError(retryResponse.status, retryResponse.statusText, data)
|
||||
}
|
||||
return retryResponse
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
getConfigs: async (projectId: string): Promise<Config[]> => {
|
||||
const response = await fetchWithAuth(`/configs?scope_type=project&scope_id=${projectId}`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
createConfig: async (data: ConfigCreate): Promise<Config> => {
|
||||
const response = await fetchWithAuth('/configs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
updateConfig: async (id: string, data: { value: unknown }): Promise<Config> => {
|
||||
const response = await fetchWithAuth(`/configs/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteConfig: async (id: string): Promise<void> => {
|
||||
await fetchWithAuth(`/configs/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
getSecrets: async (projectId: string): Promise<Secret[]> => {
|
||||
const response = await fetchWithAuth(`/secrets?scope_type=project&scope_id=${projectId}`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
createSecret: async (data: SecretCreate): Promise<Secret> => {
|
||||
const response = await fetchWithAuth('/secrets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
updateSecret: async (id: string, data: { value: string }): Promise<Secret> => {
|
||||
const response = await fetchWithAuth(`/secrets/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteSecret: async (id: string): Promise<void> => {
|
||||
await fetchWithAuth(`/secrets/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
getRepository: async (projectId: string, repoId: string): Promise<Repository> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/repositories/${repoId}`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
getRepositories: async (projectId: string): Promise<Repository[]> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/repositories`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
createRepository: async (projectId: string, data: RepositoryCreate): Promise<Repository> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/repositories`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteRepository: async (projectId: string, repoId: string): Promise<void> => {
|
||||
await fetchWithAuth(`/projects/${projectId}/repositories/${repoId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
getRepositoryConnections: async (projectId: string): Promise<RepositoryConnection[]> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/repository-connections`)
|
||||
return response.json()
|
||||
},
|
||||
|
||||
createRepositoryConnection: async (projectId: string, data: RepositoryConnectionCreate): Promise<RepositoryConnection> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/repository-connections`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteRepositoryConnection: async (projectId: string, connectionId: string): Promise<void> => {
|
||||
await fetchWithAuth(`/projects/${projectId}/repository-connections/${connectionId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
generateSshKey: async (projectId: string, connectionId: string): Promise<{ public_key: string }> => {
|
||||
const response = await fetchWithAuth(`/projects/${projectId}/repository-connections/${connectionId}/ssh-key`, {
|
||||
method: 'POST',
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user