v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика

This commit is contained in:
2026-06-16 17:41:06 +03:00
parent 6ab74f01aa
commit 56b5d09a4d
123 changed files with 13572 additions and 21 deletions
+55
View File
@@ -0,0 +1,55 @@
import createClient from "openapi-fetch";
import type { paths } from "./schema";
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<paths>({
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<T> {
data?: T;
error?: unknown;
response: Response;
}
/** Разворачивает ответ openapi-fetch: возвращает данные или бросает ApiError. */
export function unwrap<T>(res: FetchResult<T>): 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;
}