forked from datawhale/whale-town-end
131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
import { getToken, clearAuth } from './adminAuth';
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message);
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
function parseFilenameFromContentDisposition(contentDisposition: string | null): string | null {
|
|
if (!contentDisposition) return null;
|
|
|
|
// Prefer RFC 5987 filename*=UTF-8''...
|
|
const filenameStarMatch = contentDisposition.match(/filename\*=(?:UTF-8''|utf-8'')([^;]+)/);
|
|
if (filenameStarMatch?.[1]) {
|
|
try {
|
|
return decodeURIComponent(filenameStarMatch[1].trim().replace(/^"|"$/g, ''));
|
|
} catch {
|
|
return filenameStarMatch[1].trim().replace(/^"|"$/g, '');
|
|
}
|
|
}
|
|
|
|
const filenameMatch = contentDisposition.match(/filename=([^;]+)/);
|
|
if (filenameMatch?.[1]) {
|
|
return filenameMatch[1].trim().replace(/^"|"$/g, '');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const token = getToken();
|
|
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
...init,
|
|
headers: {
|
|
...headers,
|
|
...(init?.headers || {}),
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (res.status === 401) {
|
|
clearAuth();
|
|
}
|
|
|
|
const data = (await res.json().catch(() => ({}))) as any;
|
|
|
|
if (!res.ok) {
|
|
throw new ApiError(data?.message || `请求失败: ${res.status}`, res.status);
|
|
}
|
|
|
|
return data as T;
|
|
}
|
|
|
|
async function requestDownload(path: string, init?: RequestInit): Promise<{ blob: Blob; filename: string }>
|
|
{
|
|
const token = getToken();
|
|
|
|
const headers: Record<string, string> = {
|
|
...(init?.headers as any),
|
|
};
|
|
|
|
// Do NOT force Content-Type for downloads (GET binary)
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
...init,
|
|
headers,
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (res.status === 401) {
|
|
clearAuth();
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
// Try to extract message from JSON-ish body
|
|
let message = `请求失败: ${res.status}`;
|
|
try {
|
|
const maybeJson = JSON.parse(text || '{}');
|
|
message = maybeJson?.message || message;
|
|
} catch {
|
|
// ignore
|
|
}
|
|
throw new ApiError(message, res.status);
|
|
}
|
|
|
|
const filename =
|
|
parseFilenameFromContentDisposition(res.headers.get('content-disposition')) || 'logs.tar.gz';
|
|
const blob = await res.blob();
|
|
return { blob, filename };
|
|
}
|
|
|
|
export const api = {
|
|
adminLogin: (identifier: string, password: string) =>
|
|
request<any>('/admin/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ identifier, password }),
|
|
}),
|
|
|
|
listUsers: (limit = 100, offset = 0) =>
|
|
request<any>(`/admin/users?limit=${encodeURIComponent(limit)}&offset=${encodeURIComponent(offset)}`),
|
|
|
|
resetUserPassword: (userId: string, newPassword: string) =>
|
|
request<any>(`/admin/users/${encodeURIComponent(userId)}/reset-password`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ new_password: newPassword }),
|
|
}),
|
|
|
|
getRuntimeLogs: (lines = 200) =>
|
|
request<any>(`/admin/logs/runtime?lines=${encodeURIComponent(lines)}`),
|
|
|
|
downloadLogsArchive: () => requestDownload('/admin/logs/archive'),
|
|
};
|