Files
CC-Manager/web/src/lib/api.ts
T
Harlekin 724f68644b Umlaute statt Umschreibungen
Oberflaeche, Fehlermeldungen und Kommentare schrieben durchgehend ae, oe, ue
und ss. Dafuer gab es keinen Grund - die Anwendung ist deutschsprachig und
laeuft durchgehend in UTF-8.

Betroffen waren 55 Dateien, darunter alle sichtbaren Texte: Guthaben-Zaehler,
Import, Nutzerverwaltung, Passwortwechsel und die Zwei-Faktor-Einrichtung.

Der Name einer Migration bleibt im README-Beispiel bewusst bei ASCII, weil
daraus ein Verzeichnisname wird.
2026-09-18 16:57:39 +02:00

103 lines
2.6 KiB
TypeScript

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<T>(path: string, options: RequestOptions = {}): Promise<T> {
const method = options.method ?? 'GET';
const headers: Record<string, string> = {};
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<T>(path: string, file: File): Promise<T> {
const form = new FormData();
form.append('file', file);
const headers: Record<string, string> = {};
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 };
}
}