/
Mihaham
/
Table-Time
Обзор
Документация
Войти
/
Mihaham
/
Table-Time
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
apps/web/lib/api.ts
220 строк
7 KB
MihahamYT
feat: games catalog, 90%+ test coverage, MkDocs ready for GitVerse
14 июн 2026, 10:45
14 июн 2026, 10:45
03c79f9
Код
Авторство
О чём код?
import { randomUUID } from "./id"; import { getApiBaseUrl } from "./urls"; export function formatApiError(detail: unknown): string { if (typeof detail === "string") return detail; if (Array.isArray(detail)) { return detail .map((item) => { if (typeof item === "object" && item !== null && "msg" in item) { return String((item as { msg: string }).msg); } return String(item); }) .join("; "); } if (typeof detail === "object" && detail !== null) return JSON.stringify(detail); return "Unknown error"; } function clearStoredAuth(): void { if (typeof window === "undefined") return; localStorage.removeItem("tabletime_token"); localStorage.removeItem("tabletime_token_type"); localStorage.removeItem("tabletime_guest_id"); } function storeParticipantId(participantId: string): void { if (typeof window === "undefined") return; localStorage.setItem("tabletime_participant_id", participantId); } export function getStoredParticipantId(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem("tabletime_participant_id"); } function storeGuestTokens(tokens: AuthTokens): void { localStorage.setItem("tabletime_token", tokens.guest_token || tokens.access_token); localStorage.setItem("tabletime_token_type", "guest"); if (tokens.guest_id) localStorage.setItem("tabletime_guest_id", tokens.guest_id); } export interface AuthTokens { access_token: string; refresh_token?: string; guest_token?: string; guest_id?: string; token_type: string; expires_in: number; } export interface Participant { id: string; display_name: string; role: string; presence_web: boolean; presence_telegram: boolean; is_host: boolean; control_channel?: string; } export interface Session { id: string; invite_code: string; status: string; max_players: number; active_game_id?: string; participants: Participant[]; created_at: string; } function getStoredToken(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem("tabletime_token"); } function getAuthHeader(): Record<string, string> { const token = getStoredToken(); if (!token) return {}; const type = localStorage.getItem("tabletime_token_type") || "bearer"; if (type === "guest") return { Authorization: `Guest ${token}` }; return { Authorization: `Bearer ${token}` }; } async function apiFetch<T>(path: string, options: RequestInit = {}, retryOn401 = true): Promise<T> { const res = await fetch(`${getApiBaseUrl()}/api/v1${path}`, { ...options, headers: { "Content-Type": "application/json", ...getAuthHeader(), ...options.headers, }, }); if (res.status === 401 && retryOn401 && path !== "/auth/guest") { const wasGuest = localStorage.getItem("tabletime_token_type") === "guest"; clearStoredAuth(); if (wasGuest) { const guestName = localStorage.getItem("tabletime_guest_name") || "Игрок"; await fetchGuestToken(guestName); return apiFetch<T>(path, options, false); } } if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(formatApiError(err.detail) || `HTTP ${res.status}`); } return res.json(); } async function fetchGuestToken(displayName: string): Promise<AuthTokens> { const res = await fetch(`${getApiBaseUrl()}/api/v1/auth/guest`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ display_name: displayName }), }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(formatApiError(err.detail) || `HTTP ${res.status}`); } const tokens: AuthTokens = await res.json(); storeGuestTokens(tokens); return tokens; } export async function ensureGuest(displayName: string): Promise<AuthTokens> { if (typeof window !== "undefined") { localStorage.setItem("tabletime_guest_name", displayName); } const existing = getStoredToken(); if (existing) return { access_token: existing, token_type: "guest", expires_in: 0 }; return fetchGuestToken(displayName); } export async function createSession(displayName?: string): Promise<Session> { const session = await apiFetch<Session>("/sessions", { method: "POST", body: JSON.stringify({ display_name: displayName, max_players: 4 }), }); const host = session.participants.find((p) => p.is_host); if (host) storeParticipantId(host.id); return session; } export async function joinSession(code: string, displayName?: string): Promise<{ session: Session; participant_id: string }> { const result = await apiFetch<{ session: Session; participant_id: string }>("/sessions/join", { method: "POST", body: JSON.stringify({ invite_code: code, channel: "web", display_name: displayName }), }); storeParticipantId(result.participant_id); return result; } export async function getSession(id: string): Promise<Session> { return apiFetch(`/sessions/${id}`); } export async function startGame(sessionId: string, pluginId: string): Promise<{ game_id: string }> { return apiFetch(`/sessions/${sessionId}/games`, { method: "POST", body: JSON.stringify({ plugin_id: pluginId }), }); } export async function endActiveGame(sessionId: string): Promise<{ game_id: string; status: string }> { return apiFetch(`/sessions/${sessionId}/games/end`, { method: "POST", body: JSON.stringify({}), }); } export async function getGameState(gameId: string) { return apiFetch(`/games/${gameId}/state`); } export interface GameActionResult { state: Record<string, unknown>; view: Record<string, unknown>; status: string; winner_id?: string; } export async function submitAction( gameId: string, actionType: string, payload: Record<string, unknown> = {} ): Promise<GameActionResult> { return apiFetch<GameActionResult>(`/games/${gameId}/actions`, { method: "POST", headers: { "X-Channel": "web" }, body: JSON.stringify({ action_id: randomUUID(), action_type: actionType, payload, }), }); } export async function getCatalog() { return apiFetch<{ plugins: Array<{ plugin_id: string; display_name: string }> }>("/games/catalog"); } export async function login(email: string, password: string): Promise<AuthTokens> { const tokens = await apiFetch<AuthTokens>("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }); localStorage.setItem("tabletime_token", tokens.access_token); localStorage.setItem("tabletime_token_type", "bearer"); return tokens; } export async function register(email: string, password: string, displayName?: string): Promise<AuthTokens> { const tokens = await apiFetch<AuthTokens>("/auth/register", { method: "POST", body: JSON.stringify({ email, password, display_name: displayName }), }); localStorage.setItem("tabletime_token", tokens.access_token); localStorage.setItem("tabletime_token_type", "bearer"); return tokens; }