- Updated API documentation for the `rebuildZooms` endpoint to clarify its long execution time and response behavior. - Modified MapView component to manage tile cache invalidation after rebuilding zoom levels, ensuring fresh tile display. - Introduced a new composable for handling tile cache invalidation state after admin actions. - Enhanced character icon creation to reflect ownership status with distinct colors. - Improved loading state handling in various components for better user experience during data fetching.
260 lines
7.6 KiB
TypeScript
260 lines
7.6 KiB
TypeScript
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<symbol, () => 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<T>(path: string, opts?: RequestInit): Promise<T> {
|
|
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<T>
|
|
}
|
|
return undefined as T
|
|
}
|
|
|
|
async function getConfig() {
|
|
return request<ConfigResponse>('config')
|
|
}
|
|
|
|
async function getCharacters() {
|
|
return request<Character[]>('v1/characters')
|
|
}
|
|
|
|
async function getMarkers() {
|
|
return request<Marker[]>('v1/markers')
|
|
}
|
|
|
|
async function getMaps() {
|
|
return request<Record<string, MapInfo>>('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<MeResponse>
|
|
}
|
|
|
|
/** 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<string[]> {
|
|
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<MeResponse>('me')
|
|
}
|
|
|
|
async function meUpdate(body: { email?: string }) {
|
|
await request('me', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
/** 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<string[]>('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<SettingsResponse>('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<MapInfoAdmin[]>('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<MapInfoAdmin>(`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,
|
|
meUpdate,
|
|
oauthLoginUrl,
|
|
oauthProviders,
|
|
setupRequired,
|
|
meTokens,
|
|
mePassword,
|
|
adminUsers,
|
|
adminUserByName,
|
|
adminUserPost,
|
|
adminUserDelete,
|
|
adminSettings,
|
|
adminSettingsPost,
|
|
adminMaps,
|
|
adminMapPost,
|
|
adminMapToggleHidden,
|
|
adminWipe,
|
|
adminRebuildZooms,
|
|
adminExportUrl,
|
|
adminMerge,
|
|
adminWipeTile,
|
|
adminSetCoords,
|
|
adminHideMarker,
|
|
}
|
|
}
|