/
VVRONGG
/
backside
Обзор
Документация
Войти
/
VVRONGG
/
backside
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
admin/src/api.ts
167 строк
5 KB
VVrongg
add kubec deploy
24 май 2026, 20:16
24 май 2026, 20:16
db9ffee
Код
Авторство
О чём код?
// API client for the Backside admin SPA. One place that knows the backend's URL, // auth scheme and response shapes; the rest of the app calls `api.*`. const BASE = ( (import.meta.env.VITE_API_BASE as string | undefined) ?? 'http://localhost:3000' ).replace(/\/$/, ''); const TOKEN_KEY = 'backside_admin_token'; /** Fired when a request gets a 401, so the auth layer can drop to the login screen. */ export const AUTH_EXPIRED_EVENT = 'backside-auth-expired'; export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } export function setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token); } export function clearToken(): void { localStorage.removeItem(TOKEN_KEY); } /** A failed request, carrying the HTTP status and the backend's `{error}` message. */ export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; } } /** Best-effort human-readable message from anything thrown in a try block. */ export function errMsg(e: unknown): string { if (e instanceof ApiError || e instanceof Error) return e.message; return 'Что-то пошло не так'; } interface ReqOpts { method?: string; body?: unknown; } async function request<T>(path: string, opts: ReqOpts = {}): Promise<T> { const headers: Record<string, string> = {}; if (opts.body !== undefined) headers['Content-Type'] = 'application/json'; const token = getToken(); if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${BASE}${path}`, { method: opts.method ?? 'GET', headers, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, }); if (res.status === 401) { clearToken(); window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT)); throw new ApiError(401, 'Сессия истекла — войдите снова.'); } if (!res.ok) { let message = `Ошибка запроса (${res.status})`; try { const body = await res.json(); if (body && typeof body.error === 'string') message = body.error; } catch { // non-JSON error body; keep the generic message } throw new ApiError(res.status, message); } if (res.status === 204) return undefined as T; return (await res.json()) as T; } // ---- Shapes mirrored from the backend DTOs ---- export interface GeoPos { lat: number; lon: number; } export interface ImageRef { id: number; url: string; position: number; } export interface Objective { id: number; name: string; description: string; pos: GeoPos; images: ImageRef[]; created_at: string; updated_at: string; } /** Active objective from the sync feed (deleted ones omit the payload fields). */ export interface ObjectiveSyncItem { id: number; deleted: boolean; updated_at: string; name?: string; description?: string; pos?: GeoPos; images?: ImageRef[]; } interface ObjectivesSyncResponse { server_time: string; objectives: ObjectiveSyncItem[]; } export interface Route { id: number; name: string; description: string | null; points: number[]; created_at: string; updated_at: string; } export interface ObjectiveInput { name: string; description: string; pos: GeoPos; images?: string[]; } export interface RouteInput { name: string; description: string | null; points: number[]; } /** Absolute URL for an image blob, for use in `<img src>`. */ export function imageSrc(ref: ImageRef): string { return `${BASE}${ref.url}`; } export const api = { login: (username: string, password: string) => request<{ token: string }>('/auth/login', { method: 'POST', body: { username, password }, }), // The backend has no plain "list objectives" route; the sync feed with no // cursor returns every row, so we take it and drop the tombstones. listObjectives: async (): Promise<ObjectiveSyncItem[]> => { const r = await request<ObjectivesSyncResponse>('/objectives/sync'); return r.objectives.filter((o) => !o.deleted); }, getObjective: (id: number) => request<Objective>(`/objectives/${id}`), createObjective: (body: ObjectiveInput) => request<Objective>('/objectives', { method: 'POST', body }), updateObjective: (id: number, body: Partial<ObjectiveInput>) => request<Objective>(`/objectives/${id}`, { method: 'PATCH', body }), deleteObjective: (id: number) => request<void>(`/objectives/${id}`, { method: 'DELETE' }), addImages: (id: number, images: string[]) => request<ImageRef[]>(`/objectives/${id}/images`, { method: 'POST', body: { images } }), reorderImages: (id: number, order: number[]) => request<ImageRef[]>(`/objectives/${id}/images`, { method: 'PUT', body: { order } }), deleteImage: (id: number, imageId: number) => request<void>(`/objectives/${id}/images/${imageId}`, { method: 'DELETE' }), listRoutes: () => request<Route[]>('/routes'), getRoute: (id: number) => request<Route>(`/routes/${id}`), createRoute: (body: RouteInput) => request<Route>('/routes', { method: 'POST', body }), updateRoute: (id: number, body: Partial<RouteInput>) => request<Route>(`/routes/${id}`, { method: 'PATCH', body }), deleteRoute: (id: number) => request<void>(`/routes/${id}`, { method: 'DELETE' }), };