import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig, } from "axios"; import type { ErrorEnvelope } from "@new-api-key-usage/core"; import { redirectAuth, type AuthWorld } from "./auth-redirect.ts"; const MEMBER_TOKEN_KEY = "member_token"; export class ApiError extends Error { readonly status: number; readonly code: string; readonly fields: Record | undefined; readonly keyId: number | undefined; constructor( status: number, code: string, message: string, extra?: { fields?: Record; key_id?: number }, ) { super(message); this.name = "ApiError"; this.status = status; this.code = code; this.fields = extra?.fields; this.keyId = extra?.key_id; } } function parseErrorEnvelope(data: unknown): ErrorEnvelope | null { if (!data || typeof data !== "object") return null; const env = data as Partial; if (!env.error || typeof env.error !== "object") return null; const { code, message } = env.error; if (typeof code !== "string" || typeof message !== "string") return null; return env as ErrorEnvelope; } function toApiError(error: AxiosError, world: AuthWorld): ApiError { const envelope = parseErrorEnvelope(error.response?.data); if (envelope) { return new ApiError( error.response?.status ?? 0, envelope.error.code, envelope.error.message, { fields: envelope.error.fields, key_id: envelope.error.key_id, }, ); } const status = error.response?.status ?? 0; if (status === 401) { return new ApiError(401, "unauthorized", "凭证无效或已过期"); } if (status === 0 || !error.response) { return new ApiError(0, "network", "网络异常,请稍后重试"); } // 中间人非 JSON 错误体(代理 502 页等)回退通用文案 return new ApiError(status, "internal", "服务暂时不可用,请稍后重试"); } function attachInterceptors(client: AxiosInstance, world: AuthWorld): void { if (world === "member") { client.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = sessionStorage.getItem(MEMBER_TOKEN_KEY); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); } client.interceptors.response.use( (response) => response, (error: AxiosError) => { const apiError = toApiError(error, world); if (apiError.status === 401) { redirectAuth(world, "expired"); } return Promise.reject(apiError); }, ); } function createClient(world: AuthWorld, baseURL: string): AxiosInstance { const client = axios.create({ baseURL, timeout: 30_000, // 管理面会话 cookie 需跨端口(Vite 5173 → API 18456)携带 withCredentials: true, headers: { Accept: "application/json" }, }); attachInterceptors(client, world); return client; } /** 管理面:`/api/admin/*`,会话 cookie */ export const adminHttp = createClient("admin", "/api/admin"); /** 成员视图:`/api/member/*`,Bearer 令牌 */ export const memberHttp = createClient("member", "/api/member");