import createClient from "openapi-fetch"; import type { paths } from "./schema"; export function readCookie(name: string): string | null { const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : null; } // Пути в схеме уже содержат /api; baseUrl — origin (пусто в dev/prod). export const api = createClient({ baseUrl: import.meta.env.VITE_API_BASE_URL || "", credentials: "include", }); // CSRF double-submit: дублируем cookie csrf_token в заголовок на всех запросах. api.use({ onRequest({ request }) { const csrf = readCookie("csrf_token"); if (csrf) request.headers.set("X-CSRF-Token", csrf); return request; }, }); export class ApiError extends Error { code: string; status: number; details?: unknown; constructor(message: string, code: string, status: number, details?: unknown) { super(message); this.code = code; this.status = status; this.details = details; } } interface FetchResult { data?: T; error?: unknown; response: Response; } /** Разворачивает ответ openapi-fetch: возвращает данные или бросает ApiError. */ export function unwrap(res: FetchResult): T { if (res.error || !res.response.ok) { const env = (res.error as { error?: { code?: string; message?: string; details?: unknown } }) ?.error; throw new ApiError( env?.message || "Ошибка запроса", env?.code || "ERROR", res.response.status, env?.details, ); } return res.data as T; } /** * Повторы для проб сессии (`/users/me`, `/admin/me`). «Не авторизован» — это * только ответ сервера (401/403, обрабатывается в самих хуках); обрыв связи * ответом не является, и без повторов гварды приняли бы его за разлогин и * увели на страницу входа. */ export const authProbeRetry = (count: number, err: unknown): boolean => !(err instanceof ApiError) && count < 2;