const CSRF_COOKIE = 'ccm_csrf'; const CSRF_HEADER = 'x-csrf-token'; export class ApiError extends Error { constructor( readonly status: number, readonly code: string, message: string, ) { super(message); this.name = 'ApiError'; } } function readCookie(name: string): string | undefined { const match = document.cookie .split('; ') .find((entry) => entry.startsWith(`${name}=`)); return match ? decodeURIComponent(match.slice(name.length + 1)) : undefined; } interface RequestOptions { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; body?: unknown; signal?: AbortSignal; } export async function api(path: string, options: RequestOptions = {}): Promise { const method = options.method ?? 'GET'; const headers: Record = {}; if (options.body !== undefined) { headers['content-type'] = 'application/json'; } if (method !== 'GET') { const token = readCookie(CSRF_COOKIE); if (token) headers[CSRF_HEADER] = token; } const response = await fetch(path, { method, headers, credentials: 'same-origin', body: options.body === undefined ? undefined : JSON.stringify(options.body), ...(options.signal ? { signal: options.signal } : {}), }); if (response.status === 204) return undefined as T; const text = await response.text(); const payload = text ? safeParse(text) : null; if (!response.ok) { const error = payload as { error?: string; message?: string } | null; throw new ApiError( response.status, error?.error ?? 'UNKNOWN', error?.message ?? `Fehler ${response.status}`, ); } return payload as T; } /** Datei-Upload läuft ohne JSON-Header, aber mit demselben CSRF-Schutz. */ export async function uploadFile(path: string, file: File): Promise { const form = new FormData(); form.append('file', file); const headers: Record = {}; const token = readCookie(CSRF_COOKIE); if (token) headers[CSRF_HEADER] = token; const response = await fetch(path, { method: 'POST', headers, credentials: 'same-origin', body: form, }); const text = await response.text(); const payload = text ? safeParse(text) : null; if (!response.ok) { const error = payload as { error?: string; message?: string } | null; throw new ApiError( response.status, error?.error ?? 'UNKNOWN', error?.message ?? `Fehler ${response.status}`, ); } return payload as T; } function safeParse(text: string): unknown { try { return JSON.parse(text); } catch { return { message: text }; } }