import { goto } from '$app/navigation'; import { auth, type Tokens, type User } from './stores/auth.svelte'; import type { Photo, Preferences, Tag } from './types'; export class ApiError extends Error { status: number; code: string; constructor(status: number, code: string, message: string) { super(message); this.status = status; this.code = code; } } interface RequestOptions { method?: string; body?: unknown; form?: FormData; } let refreshing: Promise | null = null; async function raw(path: string, opts: RequestOptions): Promise { const headers = new Headers(); if (auth.access) headers.set('Authorization', `Bearer ${auth.access}`); let body: BodyInit | undefined; if (opts.form) { body = opts.form; } else if (opts.body !== undefined) { headers.set('Content-Type', 'application/json'); body = JSON.stringify(opts.body); } return fetch(`/api${path}`, { method: opts.method ?? 'GET', headers, body }); } async function doRefresh(): Promise { if (!auth.refresh) return false; try { const res = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: auth.refresh }) }); if (!res.ok) return false; const data = await res.json(); auth.setTokens(data.tokens as Tokens); return true; } catch { return false; } } export async function api(path: string, opts: RequestOptions = {}): Promise { let res = await raw(path, opts); if (res.status === 401 && auth.refresh) { if (!refreshing) { refreshing = doRefresh().finally(() => { refreshing = null; }); } const ok = await refreshing; if (ok) res = await raw(path, opts); } if (!res.ok) { let code = 'error'; let message = res.statusText; try { const body = await res.json(); if (body?.error) { code = body.error.code ?? code; message = body.error.message ?? message; } } catch { /* ignore */ } if (res.status === 401) { auth.clear(); goto('/login'); } throw new ApiError(res.status, code, message); } if (res.status === 204) return null as T; const ct = res.headers.get('content-type') ?? ''; if (ct.includes('application/json')) return (await res.json()) as T; return (await res.text()) as unknown as T; } export const authApi = { register: (email: string, password: string, name: string) => api<{ user: User; tokens: Tokens }>('/auth/register', { method: 'POST', body: { email, password, name } }), login: (email: string, password: string) => api<{ user: User; tokens: Tokens }>('/auth/login', { method: 'POST', body: { email, password } }), logout: (refresh_token: string) => api('/auth/logout', { method: 'POST', body: { refresh_token } }), me: () => api<{ user: User; preferences?: Preferences }>('/auth/me'), savePreferences: (preferences: Preferences) => api<{ preferences: Preferences }>('/auth/me/preferences', { method: 'PATCH', body: preferences }) }; export const photosApi = { deleteBatch: (ids: string[]) => api<{ deleted: number }>('/photo-batch/delete', { method: 'POST', body: { ids } }), moveBatch: (ids: string[], albumId: string) => api<{ moved: number }>('/photo-batch/move', { method: 'POST', body: { ids, album_id: albumId } }), copyBatch: (ids: string[], albumId: string) => api<{ photos: Photo[] }>('/photo-batch/copy', { method: 'POST', body: { ids, album_id: albumId } }), trash: () => api<{ photos: Photo[] }>('/trash'), trashCount: () => api<{ count: number }>('/trash/count'), restore: (id: string, albumId?: string) => api<{ photo: Photo }>(`/photos/${id}/restore`, { method: 'POST', body: albumId ? { album_id: albumId } : {} }), permanentDelete: (id: string) => api(`/photos/${id}/permanent`, { method: 'DELETE' }), addTag: (id: string, name: string) => api<{ photo: Photo }>(`/photos/${id}/tags`, { method: 'POST', body: { name } }), removeTag: (id: string, tagId: string) => api<{ photo: Photo }>(`/photos/${id}/tags/${tagId}`, { method: 'DELETE' }) }; export const tagsApi = { list: (q = '') => api<{ tags: Tag[] }>(`/tags${q ? `?q=${encodeURIComponent(q)}` : ''}`) };