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:
2026-05-16 17:44:39 +00:00
parent 212d072417
commit e7819bfc82
246 changed files with 3625 additions and 17311 deletions
-306
View File
@@ -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()
},
}