Initial push
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
Connection, Health, HostKeyInfo, JobSnapshot, PackageInfo, Plan,
|
||||
Preflight, PreviewResponse, SourceResponse, TargetInventory,
|
||||
} from './types'
|
||||
|
||||
// The token, when the server requires one, arrives as a query parameter the
|
||||
// first time and is kept for the tab afterwards.
|
||||
function readToken(): string {
|
||||
const fromUrl = new URLSearchParams(location.search).get('token')
|
||||
if (fromUrl) {
|
||||
sessionStorage.setItem('dm.token', fromUrl)
|
||||
const clean = location.pathname + location.hash
|
||||
history.replaceState(null, '', clean)
|
||||
return fromUrl
|
||||
}
|
||||
return sessionStorage.getItem('dm.token') ?? ''
|
||||
}
|
||||
|
||||
const token = readToken()
|
||||
|
||||
/** ApiError carries the server's message plus anything it attached to it. */
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
body: Record<string, unknown>
|
||||
constructor(status: number, message: string, body: Record<string, unknown> = {}) {
|
||||
super(message)
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
/** True when the target's SSH host key still has to be approved. */
|
||||
get needsTrust(): boolean {
|
||||
return this.body.needsTrust === true
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers: Record<string, string> = { ...(init?.headers as Record<string, string>) }
|
||||
if (token) headers['X-Auth-Token'] = token
|
||||
if (init?.body) headers['Content-Type'] = 'application/json'
|
||||
|
||||
const res = await fetch(path, { ...init, headers })
|
||||
if (res.status === 204) return undefined as T
|
||||
|
||||
const text = await res.text()
|
||||
let body: Record<string, unknown> = {}
|
||||
if (text) {
|
||||
try {
|
||||
body = JSON.parse(text)
|
||||
} catch {
|
||||
if (!res.ok) throw new ApiError(res.status, text.slice(0, 400))
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
const msg = typeof body.error === 'string' ? body.error : `request failed (${res.status})`
|
||||
throw new ApiError(res.status, msg, body)
|
||||
}
|
||||
return body as T
|
||||
}
|
||||
|
||||
const post = <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'POST', body: body === undefined ? undefined : JSON.stringify(body) })
|
||||
|
||||
export const api = {
|
||||
health: () => request<Health>('/api/health'),
|
||||
source: () => request<SourceResponse>('/api/source'),
|
||||
volumeSizes: () => request<{ volumes: Record<string, number> }>('/api/source/sizes'),
|
||||
|
||||
connections: () => request<Connection[]>('/api/connections'),
|
||||
saveConnection: (c: Partial<Connection>) => post<Connection>('/api/connections', c),
|
||||
deleteConnection: (id: string) => request<void>(`/api/connections/${id}`, { method: 'DELETE' }),
|
||||
probe: (id: string) => post<HostKeyInfo>(`/api/connections/${id}/probe`),
|
||||
trust: (id: string, fingerprint: string) => post<{ trusted: boolean }>(`/api/connections/${id}/trust`, { fingerprint }),
|
||||
testConnection: (id: string) => post<Preflight>(`/api/connections/${id}/test`),
|
||||
targetInventory: (id: string) => request<TargetInventory>(`/api/connections/${id}/inventory`),
|
||||
|
||||
preview: (plan: Plan) => post<PreviewResponse>('/api/plan/preview', plan),
|
||||
migrateSSH: (connectionId: string, plan: Plan) => post<JobSnapshot>('/api/migrate/ssh', { connectionId, plan }),
|
||||
buildPackage: (plan: Plan, format: 'tar' | 'dir') => post<JobSnapshot>('/api/migrate/package', { plan, format }),
|
||||
|
||||
jobs: () => request<JobSnapshot[]>('/api/jobs'),
|
||||
job: (id: string) => request<JobSnapshot>(`/api/jobs/${id}`),
|
||||
cancelJob: (id: string) => post<{ canceled: boolean }>(`/api/jobs/${id}/cancel`),
|
||||
deleteJob: (id: string) => request<void>(`/api/jobs/${id}`, { method: 'DELETE' }),
|
||||
|
||||
packages: () => request<PackageInfo[]>('/api/packages'),
|
||||
deletePackage: (name: string) => request<void>(`/api/packages/${encodeURIComponent(name)}`, { method: 'DELETE' }),
|
||||
downloadUrl: (name: string) =>
|
||||
`/api/packages/${encodeURIComponent(name)}/download` + (token ? `?token=${encodeURIComponent(token)}` : ''),
|
||||
|
||||
/** Opens the live progress stream for a job. */
|
||||
jobEvents: (id: string) =>
|
||||
new EventSource(`/api/jobs/${id}/events` + (token ? `?token=${encodeURIComponent(token)}` : '')),
|
||||
}
|
||||
Reference in New Issue
Block a user