diff --git a/src/lib/admin/api.ts b/src/lib/admin/api.ts new file mode 100644 index 0000000..5424034 --- /dev/null +++ b/src/lib/admin/api.ts @@ -0,0 +1,93 @@ +import { goto } from '$app/navigation' + +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' + +export interface RequestOptions { + method?: HttpMethod + body?: TBody + signal?: AbortSignal + headers?: Record +} + +export interface ApiError extends Error { + status: number + details?: unknown +} + +function getAuthHeader() { + if (typeof localStorage === 'undefined') return {} + const auth = localStorage.getItem('admin_auth') + return auth ? { Authorization: `Basic ${auth}` } : {} +} + +async function handleResponse(res: Response) { + if (res.status === 401) { + // Redirect to login for unauthorized requests + try { + goto('/admin/login') + } catch {} + } + + const contentType = res.headers.get('content-type') || '' + const isJson = contentType.includes('application/json') + const data = isJson ? await res.json().catch(() => undefined) : undefined + + if (!res.ok) { + const err: ApiError = Object.assign(new Error('Request failed'), { + status: res.status, + details: data + }) + throw err + } + return data +} + +export async function request( + url: string, + opts: RequestOptions = {} +): Promise { + const { method = 'GET', body, signal, headers } = opts + + const isFormData = typeof FormData !== 'undefined' && body instanceof FormData + const mergedHeaders: Record = { + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...getAuthHeader(), + ...(headers || {}) + } + + const res = await fetch(url, { + method, + headers: mergedHeaders, + body: body ? (isFormData ? (body as any) : JSON.stringify(body)) : undefined, + signal + }) + + return handleResponse(res) as Promise +} + +export const api = { + get: (url: string, opts: Omit = {}) => + request(url, { ...opts, method: 'GET' }), + post: (url: string, body: B, opts: Omit, 'method' | 'body'> = {}) => + request(url, { ...opts, method: 'POST', body }), + put: (url: string, body: B, opts: Omit, 'method' | 'body'> = {}) => + request(url, { ...opts, method: 'PUT', body }), + patch: (url: string, body: B, opts: Omit, 'method' | 'body'> = {}) => + request(url, { ...opts, method: 'PATCH', body }), + delete: (url: string, opts: Omit = {}) => + request(url, { ...opts, method: 'DELETE' }) +} + +export function createAbortable() { + let controller: AbortController | null = null + return { + nextSignal() { + if (controller) controller.abort() + controller = new AbortController() + return controller.signal + }, + abort() { + if (controller) controller.abort() + } + } +}