Initial commit: DSH Web Manager
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number, message: string) { super(message); }
|
||||
}
|
||||
export interface User { id: string; username: string; admin: boolean; quota_bytes: number; created_at?: string }
|
||||
export interface Session { user: User; csrf_token: string }
|
||||
export interface Instance { id: string; name: string; status: string; image_id: string; version: string; created_at: string }
|
||||
export interface Image { id: string; image_id?: string; version: string; tag?: string; status?: string; usage_count?: number; recipe_rev?: string; platform?: string; created_at?: string }
|
||||
export interface Build { id: string; version: string; status: string; image_id?: string; logs?: string; error?: string; created_at: string }
|
||||
export interface Entry { name: string; path?: string; is_dir: boolean; size: number; mod_time: string }
|
||||
export interface Usage { used_bytes: number; quota_bytes: number }
|
||||
export interface Backup { id: string; created_at: string; size: number }
|
||||
export interface Audit { id: string; action: string; target: string; detail: string; user_id: string; created_at: string }
|
||||
export function createApi(fetcher: typeof fetch = fetch) {
|
||||
let csrf = '';
|
||||
let refreshing: Promise<Session> | undefined;
|
||||
const csrfCookie = () => typeof document === 'undefined' ? '' : document.cookie.split('; ').find(c => c.startsWith('dsh_csrf='))?.slice(9) || '';
|
||||
async function request<T>(path: string, method = 'GET', data?: unknown, retried = false): Promise<T> {
|
||||
const headers = new Headers();
|
||||
if (method !== 'GET' && (csrf || csrfCookie())) headers.set('X-CSRF-Token', csrf || csrfCookie());
|
||||
const multipart = data instanceof FormData;
|
||||
if (data !== undefined && !multipart) headers.set('Content-Type', 'application/json');
|
||||
let response: Response;
|
||||
try { response = await fetcher('/api' + path, { method, credentials: 'same-origin', headers, body: data === undefined ? undefined : multipart ? data : JSON.stringify(data) }); }
|
||||
catch { throw new ApiError(0, '无法连接服务器,请检查网络或后端服务。'); }
|
||||
if (response.status === 401 && !retried && path !== '/login' && path !== '/refresh' && (csrf || csrfCookie())) {
|
||||
try {
|
||||
refreshing ??= request<Session>('/refresh', 'POST', undefined, true).finally(() => { refreshing = undefined; });
|
||||
const session = await refreshing; csrf = session.csrf_token;
|
||||
return request<T>(path, method, data, true);
|
||||
} catch { /* The original response determines session expiration handling. */ }
|
||||
}
|
||||
const text = await response.text();
|
||||
let payload: unknown;
|
||||
try { payload = text ? JSON.parse(text) : undefined; } catch { payload = undefined; }
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && path !== '/login' && path !== '/me' && path !== '/refresh' && typeof window !== 'undefined') window.dispatchEvent(new Event('session-expired'));
|
||||
throw new ApiError(response.status, (payload as {error?: string})?.error || (response.status === 401 ? '登录已失效,请重新登录。' : '请求失败(HTTP ' + response.status + ')'));
|
||||
}
|
||||
if (text && payload === undefined) throw new ApiError(response.status, '服务器返回了非 JSON 响应,请检查 API 代理配置。');
|
||||
return payload as T;
|
||||
}
|
||||
return {
|
||||
request,
|
||||
async me() { const session = await request<Session>('/me'); csrf = session.csrf_token; return session; },
|
||||
async login(username: string, password: string) { const session = await request<Session>('/login', 'POST', {username,password}); csrf = session.csrf_token; return session; },
|
||||
async logout() { await request('/logout', 'POST'); csrf = ''; },
|
||||
instances: () => request<Instance[]>('/instances'),
|
||||
images: () => request<Image[]>('/images'),
|
||||
builds: () => request<Build[]>('/builds'),
|
||||
usage: (instance?: string) => request<Usage>('/ws/usage' + (instance ? '?instance=' + encodeURIComponent(instance) : '')),
|
||||
tree: (path: string, instance?: string) => request<Entry[]>('/ws/tree?path=' + encodeURIComponent(path) + (instance ? '&instance=' + encodeURIComponent(instance) : '')),
|
||||
download: (path: string, instance?: string) => '/api/ws/download?path=' + encodeURIComponent(path) + (instance ? '&instance=' + encodeURIComponent(instance) : ''),
|
||||
mkdir: (path: string, instance?: string) => request('/ws/mkdir' + (instance ? '?instance=' + encodeURIComponent(instance) : ''), 'POST', {path}),
|
||||
rename: (path: string, newPath: string, instance?: string) => request('/ws/rename' + (instance ? '?instance=' + encodeURIComponent(instance) : ''), 'POST', {path, new_path: newPath}),
|
||||
delete: (path: string, instance?: string) => request('/ws/delete?path=' + encodeURIComponent(path) + (instance ? '&instance=' + encodeURIComponent(instance) : ''), 'DELETE'),
|
||||
async upload(path: string, file: File, instance?: string) { const form = new FormData(); form.set('path', path); form.set('file', file); return request('/ws/upload' + (instance ? '?instance=' + encodeURIComponent(instance) : ''), 'POST', form); },
|
||||
};
|
||||
}
|
||||
export const api = createApi();
|
||||
export const instancePath = (id: string) => '/instances/' + encodeURIComponent(id);
|
||||
Reference in New Issue
Block a user