const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3001'; let authToken: string | null = localStorage.getItem('token-empire-auth-token'); export function setAuthToken(token: string) { authToken = token; localStorage.setItem('token-empire-auth-token', token); } export function getAuthToken() { return authToken; } export function clearAuthToken() { authToken = null; localStorage.removeItem('token-empire-auth-token'); } export interface TokenPayload { sub: string; email: string | null; role: string; username: string | null; mustResetPassword: boolean; } export function decodeTokenPayload(token: string): TokenPayload | null { try { const parts = token.split('.'); if (parts.length !== 3) return null; const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return { sub: payload.sub, email: payload.email ?? null, role: payload.role ?? 'user', username: payload.username ?? null, mustResetPassword: payload.mustResetPassword ?? false, }; } catch { return null; } } export function getTokenPayload(): TokenPayload | null { const token = getAuthToken(); if (!token) return null; return decodeTokenPayload(token); } export function isRegistered(): boolean { const payload = getTokenPayload(); if (!payload) return false; return payload.email != null || payload.role === 'admin'; } export function isAdmin(): boolean { const payload = getTokenPayload(); return payload?.role === 'admin'; } export function needsPasswordReset(): boolean { const payload = getTokenPayload(); return payload?.mustResetPassword === true; } async function request(path: string, options: RequestInit & { timeoutMs?: number } = {}): Promise { const { timeoutMs = 10_000, ...fetchOptions } = options; const headers: Record = { 'Content-Type': 'application/json', ...(fetchOptions.headers as Record), }; if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(`${API_BASE}${path}`, { ...fetchOptions, headers, signal: controller.signal, }); if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.error || `HTTP ${res.status} ${res.statusText}`); } return res.json(); } catch (e) { if (e instanceof DOMException && e.name === 'AbortError') { throw new Error('Request timed out — server may be unreachable'); } if (e instanceof TypeError) { throw new Error('Network error — server may be unreachable'); } throw e; } finally { clearTimeout(timeout); } } export function validateStoredToken(): void { const token = getAuthToken(); if (token && !decodeTokenPayload(token)) { clearAuthToken(); } } export const api = { health: () => request<{ status: string }>('/api/health', { timeoutMs: 5_000 }), auth: { anonymous: () => request<{ userId: string; token: string }>('/api/auth/anonymous', { method: 'POST' }), login: (login: string, password: string) => request<{ userId: string; token: string }>('/api/auth/login', { method: 'POST', body: JSON.stringify({ login, password }), }), register: (email: string, password: string, inviteCode: string) => request<{ userId: string; token: string }>('/api/auth/register', { method: 'POST', body: JSON.stringify({ email, password, inviteCode }), }), changePassword: (newPassword: string, currentPassword?: string) => request<{ success: boolean; token: string }>('/api/auth/change-password', { method: 'POST', body: JSON.stringify({ newPassword, currentPassword }), }), changeUsername: (username: string) => request<{ success: boolean; token: string }>('/api/auth/change-username', { method: 'POST', body: JSON.stringify({ username }), }), changeEmail: (email: string, currentPassword: string) => request<{ success: boolean; token: string }>('/api/auth/change-email', { method: 'POST', body: JSON.stringify({ email, currentPassword }), }), }, config: { get: () => request<{ requireInvite: boolean; userInvitations: number }>('/api/config'), }, invites: { create: () => request<{ code: string }>('/api/invites', { method: 'POST' }), validate: (code: string) => request<{ valid: boolean }>(`/api/invites/validate/${encodeURIComponent(code)}`), list: () => request<{ invitations: Array<{ id: string; code: string; createdBy: { username: string | null; email: string | null }; usedBy: { username: string | null; email: string | null } | null; createdAt: string; expiresAt: string | null; used: boolean; }>; }>('/api/invites'), remaining: () => request<{ remaining: number }>('/api/invites/remaining'), revoke: (id: string) => request<{ deleted: boolean }>(`/api/invites/${id}`, { method: 'DELETE' }), }, saves: { list: () => request<{ saves: Array<{ id: string; companyName: string; era: string; tickCount: number; updatedAt: string }> }>('/api/saves'), get: (id: string) => request<{ save: { id: string; gameData: unknown } }>(`/api/saves/${id}`), put: (data: { companyName: string; saveVersion: number; gameData: unknown; tickCount: number; era: string }) => request<{ id: string }>('/api/saves', { method: 'PUT', body: JSON.stringify(data) }), delete: (id: string) => request<{ deleted: boolean }>(`/api/saves/${id}`, { method: 'DELETE' }), }, leaderboard: { get: (category: string) => request<{ entries: Array<{ companyName: string; score: number; era: string; tickCount: number }> }>(`/api/leaderboard/${category}`), submit: (data: { companyName: string; category: string; score: number; era: string; tickCount: number }) => request<{ entry: unknown }>('/api/leaderboard', { method: 'POST', body: JSON.stringify(data) }), }, };