import type { Character, ConfigResponse, MapInfo, MapInfoAdmin, Marker, MeResponse, SettingsResponse, } from '~/types/api' export type { Character, ConfigResponse, MapInfo, MapInfoAdmin, Marker, MeResponse, SettingsResponse } // Singleton: shared by all useMapApi() callers so 401 triggers one global handler (e.g. app.vue) const onApiErrorCallbacks = new Map void>() export function useMapApi() { const config = useRuntimeConfig() const apiBase = config.public.apiBase as string /** Subscribe to API auth errors (401). Returns unsubscribe function. */ function onApiError(cb: () => void): () => void { const id = Symbol() onApiErrorCallbacks.set(id, cb) return () => onApiErrorCallbacks.delete(id) } async function request(path: string, opts?: RequestInit): Promise { const url = path.startsWith('http') ? path : `${apiBase}/${path.replace(/^\//, '')}` const res = await fetch(url, { credentials: 'include', ...opts }) // Only redirect to login on 401 (session invalid); 403 = forbidden (no permission) if (res.status === 401) { onApiErrorCallbacks.forEach((cb) => cb()) throw new Error('Unauthorized') } if (res.status === 403) throw new Error('Forbidden') if (!res.ok) throw new Error(`API ${res.status}`) if (res.headers.get('content-type')?.includes('application/json')) { return res.json() as Promise } return undefined as T } async function getConfig() { return request('config') } async function getCharacters() { return request('v1/characters') } async function getMarkers() { return request('v1/markers') } async function getMaps() { return request>('maps') } // Auth async function login(user: string, pass: string) { const res = await fetch(`${apiBase}/login`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user, pass }), }) if (res.status === 401) { const data = (await res.json().catch(() => ({}))) as { error?: string } throw new Error(data.error || 'Unauthorized') } if (!res.ok) throw new Error(`API ${res.status}`) return res.json() as Promise } /** OAuth login URL for redirect (full page navigation). */ function oauthLoginUrl(provider: string, redirect?: string): string { const url = new URL(`${apiBase}/oauth/${provider}/login`) if (redirect) url.searchParams.set('redirect', redirect) return url.toString() } /** List of configured OAuth providers. */ async function oauthProviders(): Promise { try { const res = await fetch(`${apiBase}/oauth/providers`, { credentials: 'include' }) if (!res.ok) return [] const data = await res.json() return Array.isArray(data) ? data : [] } catch { return [] } } async function logout() { await fetch(`${apiBase}/logout`, { method: 'POST', credentials: 'include' }) } async function me() { return request('me') } /** Public: whether first-time setup (no users) is required. */ async function setupRequired(): Promise<{ setupRequired: boolean }> { const res = await fetch(`${apiBase}/setup`, { credentials: 'include' }) if (!res.ok) throw new Error(`API ${res.status}`) return res.json() as Promise<{ setupRequired: boolean }> } // Profile async function meTokens() { const data = await request<{ tokens: string[] }>('me/tokens', { method: 'POST' }) return data!.tokens } async function mePassword(pass: string) { await request('me/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pass }), }) } // Admin async function adminUsers() { return request('admin/users') } async function adminUserByName(name: string) { return request<{ username: string; auths: string[] }>(`admin/users/${encodeURIComponent(name)}`) } async function adminUserPost(body: { user: string; pass?: string; auths: string[] }) { await request('admin/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) } async function adminUserDelete(name: string) { await request(`admin/users/${encodeURIComponent(name)}`, { method: 'DELETE' }) } async function adminSettings() { return request('admin/settings') } async function adminSettingsPost(body: { prefix?: string; defaultHide?: boolean; title?: string }) { await request('admin/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) } async function adminMaps() { return request('admin/maps') } async function adminMapPost(id: number, body: { name: string; hidden: boolean; priority: boolean }) { await request(`admin/maps/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) } async function adminMapToggleHidden(id: number) { return request(`admin/maps/${id}/toggle-hidden`, { method: 'POST' }) } async function adminWipe() { await request('admin/wipe', { method: 'POST' }) } async function adminRebuildZooms() { await request('admin/rebuildZooms', { method: 'POST' }) } function adminExportUrl() { return `${apiBase}/admin/export` } async function adminMerge(formData: FormData) { const res = await fetch(`${apiBase}/admin/merge`, { method: 'POST', credentials: 'include', body: formData, }) if (res.status === 401 || res.status === 403) { onApiErrorCallbacks.forEach((cb) => cb()) throw new Error('Unauthorized') } if (!res.ok) throw new Error(`API ${res.status}`) } async function adminWipeTile(params: { map: number; x: number; y: number }) { const qs = new URLSearchParams({ map: String(params.map), x: String(params.x), y: String(params.y) }) return request(`admin/wipeTile?${qs}`) } async function adminSetCoords(params: { map: number; fx: number; fy: number; tx: number; ty: number }) { const qs = new URLSearchParams({ map: String(params.map), fx: String(params.fx), fy: String(params.fy), tx: String(params.tx), ty: String(params.ty), }) return request(`admin/setCoords?${qs}`) } async function adminHideMarker(params: { id: number }) { const qs = new URLSearchParams({ id: String(params.id) }) return request(`admin/hideMarker?${qs}`) } return { apiBase, onApiError, getConfig, getCharacters, getMarkers, getMaps, login, logout, me, oauthLoginUrl, oauthProviders, setupRequired, meTokens, mePassword, adminUsers, adminUserByName, adminUserPost, adminUserDelete, adminSettings, adminSettingsPost, adminMaps, adminMapPost, adminMapToggleHidden, adminWipe, adminRebuildZooms, adminExportUrl, adminMerge, adminWipeTile, adminSetCoords, adminHideMarker, } }