import { authHeaders, isRemote, resolveUrl } from './source' /** 后端 API 客户端:默认同源请求(开发时由 Vite 代理到后端),Cookie 会话,修改类请求带 CSRF 令牌;指到别的后端时用 Bearer 令牌(见 source.ts)。 */ export class ApiError extends Error { readonly status: number readonly code: string readonly detail: string /** 字段级的校验错误:字段名 → 信息。 */ readonly errors: Record constructor(status: number, code: string, detail: string, errors: Record = {}) { super(detail || code) this.status = status this.code = code this.detail = detail this.errors = errors } } let csrfToken: string | null = null export function setCsrf(token: string | null) { csrfToken = token } export type Query = Record export function withQuery(path: string, query?: Query): string { if (!query) return path const params = new URLSearchParams() for (const [k, v] of Object.entries(query)) { if (v !== undefined && v !== null && v !== '') params.set(k, String(v)) } const text = params.toString() return text ? `${path}?${text}` : path } async function request(method: string, path: string, body?: unknown, init?: RequestInit): Promise { const headers: Record = { Accept: 'application/json', ...authHeaders() } if (body !== undefined) headers['Content-Type'] = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json' if (method !== 'GET' && csrfToken) headers['X-CSRF-Token'] = csrfToken const response = await fetch(resolveUrl(path), { method, headers, credentials: isRemote() ? 'omit' : 'include', body: body === undefined ? undefined : JSON.stringify(body), ...init, }) if (response.status === 204) return undefined as T const text = await response.text() const data = text ? (JSON.parse(text) as unknown) : undefined if (!response.ok) { const problem = (data ?? {}) as { code?: string; detail?: string; title?: string; errors?: Record | null } throw new ApiError(response.status, problem.code ?? 'error', problem.detail ?? problem.title ?? response.statusText, problem.errors ?? {}) } return data as T } /** * 在另一个后端上用用户名和密码登录:不设 Cookie(跨域用不了),把会话令牌放在响应里返回(`return_session`), * 调用方存进浏览器,之后每个请求带上。 */ export async function loginTo(base: string, username: string, password: string): Promise { const response = await fetch(`${base}/api/admin/auth/login`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, credentials: 'omit', body: JSON.stringify({ username, password, return_session: true }), }) const text = await response.text() const data = text ? (JSON.parse(text) as unknown) : undefined if (!response.ok) { const problem = (data ?? {}) as { code?: string; detail?: string; title?: string } throw new ApiError(response.status, problem.code ?? 'error', problem.detail ?? problem.title ?? response.statusText) } const token = (data as { session_token?: string | null } | undefined)?.session_token if (!token) throw new ApiError(502, 'no_session', '这个后端的版本还不支持跨域登录,请先升级后端') return token } export const api = { get: (path: string, query?: Query, init?: RequestInit) => request('GET', withQuery(path, query), undefined, init), post: (path: string, body?: unknown) => request('POST', path, body ?? {}), patch: (path: string, body: unknown) => request('PATCH', path, body), put: (path: string, body: unknown) => request('PUT', path, body), del: (path: string) => request('DELETE', path), }