/** * Admin 后台管理系统 - 统一 API 客户端 * * 鉴权:通过 POST /api/v1/auth/login 用密码换取 HttpOnly Cookie 会话, * 同源请求自动携带 Cookie,无需手动管理密钥。 * 收到 401 时广播 `profeto:unauthorized` 事件,由 AdminLayout 切回登录页。 */ const API_BASE = '/api/v1' const TIMEOUT_MS = 30_000 /** 会话失效事件名,AdminLayout 监听后弹出登录页 */ export const UNAUTHORIZED_EVENT = 'profeto:unauthorized' export class ApiError extends Error { constructor( message: string, public status: number, public data?: unknown, ) { super(message) this.name = 'ApiError' } } async function request( path: string, options: RequestInit & { timeoutMs?: number } = {}, ): Promise { // 修复: 正确拼接 API_BASE const url = path.startsWith('http') ? path : path.startsWith('/') ? path // 已经是绝对路径(如 /health) : `${API_BASE}${path}` const { timeoutMs = TIMEOUT_MS, ...fetchOptions } = options const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeoutMs) try { const res = await fetch(url, { ...fetchOptions, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...fetchOptions.headers, }, }) if (!res.ok) { let detail: unknown try { detail = await res.json() } catch { detail = await res.text() } let message = detail && typeof detail === 'object' && 'detail' in detail ? String((detail as { detail: unknown }).detail) : `HTTP ${res.status}: ${res.statusText}` if (res.status === 401) { message += '\n登录已过期,请重新登录。' window.dispatchEvent(new CustomEvent(UNAUTHORIZED_EVENT)) } throw new ApiError(message, res.status, detail) } // 修复: 正确判断 204 No Content if (res.status === 204) { return undefined as T } return res.json() as Promise } catch (err) { if (err instanceof ApiError) throw err if (err instanceof DOMException && err.name === 'AbortError') { throw new ApiError('请求超时,请稍后重试', 0) } throw new ApiError( err instanceof Error ? err.message : '网络错误,请检查连接', 0, ) } finally { clearTimeout(timer) } } export const api = { get: (path: string) => request(path), post: (path: string, body?: unknown, opts?: { timeoutMs?: number }) => request(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined, ...opts }), put: (path: string, body?: unknown, opts?: { timeoutMs?: number }) => request(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined, ...opts }), delete: (path: string) => request(path, { method: 'DELETE' }), } // ── 认证 ──────────────────────────────────────────────────────── /** 密码登录,成功后服务端写入 HttpOnly 会话 Cookie */ export function login(password: string): Promise<{ ok: boolean }> { return api.post(`${API_BASE}/auth/login`, { password }) } /** 退出登录,清除会话 Cookie */ export function logout(): Promise<{ ok: boolean }> { return api.post(`${API_BASE}/auth/logout`) } /** 探测当前登录状态 */ export function fetchAuthState(): Promise<{ authenticated: boolean enabled: boolean password_origin?: 'db' | 'env' | 'none' }> { return api.get(`${API_BASE}/auth/me`) } /** 修改管理员密码(成功后所有会话失效,需重新登录) */ export function changePassword(currentPassword: string, newPassword: string): Promise<{ ok: boolean; message: string }> { return api.post(`${API_BASE}/auth/change-password`, { current_password: currentPassword, new_password: newPassword, }) } export { API_BASE }