dev_arc_aws/src/lib/api.ts

244 lines
9.3 KiB
TypeScript
Raw Normal View History

const TOKEN_KEY = 'archnest_token'
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
export function setToken(token: string | null) {
if (token) localStorage.setItem(TOKEN_KEY, token)
else localStorage.removeItem(TOKEN_KEY)
}
export class ApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.status = status
}
}
export async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken()
const headers: Record<string, string> = {
...(options.body ? { 'Content-Type': 'application/json' } : {}),
...(options.headers as Record<string, string> | undefined),
}
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`/api${path}`, { ...options, headers })
if (!res.ok) {
let message = res.statusText
try {
const body = await res.json()
message = body.error ?? message
} catch {
// ignore non-JSON error bodies
}
throw new ApiError(res.status, message)
}
if (res.status === 204) return undefined as T
return res.json() as Promise<T>
}
export const api = {
getSetupStatus: () => apiFetch<{ needsSetup: boolean }>('/system/setup-status'),
setup: (username: string, password: string) =>
apiFetch<{ token: string }>('/setup', { method: 'POST', body: JSON.stringify({ username, password }) }),
login: (username: string, password: string) =>
apiFetch<{ token: string }>('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
me: () => apiFetch<{ user: AuthUser }>('/auth/me'),
updateMe: (data: Partial<{ displayName: string | null; email: string | null; avatarDataUrl: string | null }>) =>
apiFetch<{ user: AuthUser }>('/auth/me', { method: 'PUT', body: JSON.stringify(data) }),
listIntegrations: () => apiFetch<{ integrations: Integration[] }>('/integrations'),
createIntegration: (data: { type: string; name: string; config?: Record<string, string>; secrets?: Record<string, string> }) =>
apiFetch<{ integration: Integration }>('/integrations', { method: 'POST', body: JSON.stringify(data) }),
updateIntegration: (id: number, data: Partial<{ name: string; config: Record<string, string>; secrets: Record<string, string> }>) =>
apiFetch<{ integration: Integration }>(`/integrations/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
deleteIntegration: (id: number) => apiFetch<void>(`/integrations/${id}`, { method: 'DELETE' }),
testIntegration: (id: number) => apiFetch<{ ok: boolean; message: string }>(`/integrations/${id}/test`, { method: 'POST' }),
listBookmarks: () => apiFetch<{ bookmarks: Bookmark[] }>('/bookmarks'),
listBookmarkCategories: () => apiFetch<{ categories: BookmarkCategory[] }>('/bookmarks/categories'),
createBookmarkCategory: (data: { name: string; icon?: string; sortOrder?: number }) =>
apiFetch<{ id: number }>('/bookmarks/categories', { method: 'POST', body: JSON.stringify(data) }),
createBookmark: (data: { categoryId?: number | null; title: string; url: string; icon?: string; favorite?: boolean }) =>
apiFetch<{ id: number }>('/bookmarks', { method: 'POST', body: JSON.stringify(data) }),
updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) =>
apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
deleteBookmark: (id: number) => apiFetch<void>(`/bookmarks/${id}`, { method: 'DELETE' }),
listEvents: (limit = 20) => apiFetch<{ events: Event[] }>(`/events?limit=${limit}`),
listResources: () => apiFetch<{ resources: Resource[] }>('/integrations/resources'),
listTunnels: () => apiFetch<{ tunnels: Tunnel[] }>('/tunnels'),
createTunnel: (data: {
name: string
integrationId: number
mode: 'local' | 'remote' | 'dynamic'
sourcePort: number
endpointHost?: string
endpointPort?: number
autoStart?: boolean
maxRetries?: number
retryIntervalMs?: number
}) => apiFetch<{ tunnel: Tunnel }>('/tunnels', { method: 'POST', body: JSON.stringify(data) }),
deleteTunnel: (id: number) => apiFetch<void>(`/tunnels/${id}`, { method: 'DELETE' }),
connectTunnel: (id: number) =>
apiFetch<{ ok: boolean; status: string; error: string | null; retryCount: number }>(`/tunnels/${id}/connect`, { method: 'POST' }),
disconnectTunnel: (id: number) =>
apiFetch<{ ok: boolean; status: string; error: string | null; retryCount: number }>(`/tunnels/${id}/disconnect`, { method: 'POST' }),
listFiles: (integrationId: number, path: string) =>
apiFetch<{ path: string; entries: FileEntry[] }>(`/files/${integrationId}/list?path=${encodeURIComponent(path)}`),
readFile: (integrationId: number, path: string) =>
apiFetch<{ content: string; encoding: 'utf8' | 'base64'; size: number; mode: number }>(
`/files/${integrationId}/content?path=${encodeURIComponent(path)}`,
),
writeFile: (integrationId: number, path: string, content: string, encoding: 'utf8' | 'base64' = 'utf8') =>
apiFetch<{ ok: boolean }>(`/files/${integrationId}/content`, { method: 'PUT', body: JSON.stringify({ path, content, encoding }) }),
mkdir: (integrationId: number, path: string) =>
apiFetch<{ ok: boolean }>(`/files/${integrationId}/mkdir`, { method: 'POST', body: JSON.stringify({ path }) }),
renameFile: (integrationId: number, from: string, to: string) =>
apiFetch<{ ok: boolean }>(`/files/${integrationId}/rename`, { method: 'POST', body: JSON.stringify({ from, to }) }),
deleteFile: (integrationId: number, path: string, isDirectory = false) =>
apiFetch<{ ok: boolean }>(`/files/${integrationId}/delete`, { method: 'POST', body: JSON.stringify({ path, isDirectory }) }),
chmodFile: (integrationId: number, path: string, mode: string) =>
apiFetch<{ ok: boolean }>(`/files/${integrationId}/chmod`, { method: 'POST', body: JSON.stringify({ path, mode }) }),
downloadFileUrl: (integrationId: number, path: string) =>
`/api/files/${integrationId}/download?path=${encodeURIComponent(path)}`,
uploadFile: async (integrationId: number, path: string, file: File) => {
const form = new FormData()
form.append('path', path)
form.append('file', file)
const token = getToken()
const res = await fetch(`/api/files/${integrationId}/upload`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
body: form,
})
if (!res.ok) {
let message = res.statusText
try {
message = (await res.json()).error ?? message
} catch {
// ignore non-JSON error bodies
}
throw new ApiError(res.status, message)
}
return res.json() as Promise<{ ok: boolean; path: string }>
},
listContainers: (integrationId: number) =>
apiFetch<{ containers: Container[] }>(`/docker/${integrationId}/containers`),
containerStats: (integrationId: number, id: string) =>
apiFetch<ContainerStats>(`/docker/${integrationId}/containers/${encodeURIComponent(id)}/stats`),
containerLogs: (integrationId: number, id: string, tail = 200) =>
apiFetch<{ logs: string }>(`/docker/${integrationId}/containers/${encodeURIComponent(id)}/logs?tail=${tail}`),
containerAction: (integrationId: number, id: string, action: 'start' | 'stop' | 'restart' | 'pause' | 'unpause') =>
apiFetch<{ ok: boolean }>(`/docker/${integrationId}/containers/${encodeURIComponent(id)}/${action}`, { method: 'POST' }),
removeContainer: (integrationId: number, id: string, force = false) =>
apiFetch<{ ok: boolean }>(`/docker/${integrationId}/containers/${encodeURIComponent(id)}/remove`, {
method: 'POST',
body: JSON.stringify({ force }),
}),
}
export interface AuthUser {
id: number
username: string
display_name: string | null
email: string | null
avatar_data_url: string | null
}
export interface Integration {
id: number
type: string
name: string
enabled: boolean
status: string
config: Record<string, string>
lastCheckedAt: string | null
createdAt: string
}
export interface Tunnel {
id: number
name: string
integrationId: number
mode: 'local' | 'remote' | 'dynamic'
sourcePort: number
endpointHost: string
endpointPort: number
autoStart: boolean
maxRetries: number
retryIntervalMs: number
createdAt: string
status: 'stopped' | 'connecting' | 'connected' | 'retrying' | 'error'
error: string | null
retryCount: number
}
export interface Bookmark {
id: number
category_id: number | null
title: string
url: string
icon: string | null
favorite: number
status: string
last_checked_at: string | null
created_at: string
}
export interface BookmarkCategory {
id: number
name: string
icon: string | null
sort_order: number
}
export interface Event {
id: number
type: string
title: string
source: string | null
created_at: string
}
export interface FileEntry {
name: string
isDirectory: boolean
isSymlink: boolean
size: number
mode: number
mtime: number
}
export interface Container {
id: string
name: string
image: string
state: string
status: string
ports: { privatePort: number; publicPort?: number; type: string }[]
}
export interface ContainerStats {
cpuPercent: number
memUsage: number
memLimit: number
netRx: number
netTx: number
}
export interface Resource {
name: string
status: 'healthy' | 'warning' | 'critical' | 'unknown'
detail?: string
integration: string
}