Erste Produktivversion
This commit is contained in:
85
web/html/js/api.js
Normal file
85
web/html/js/api.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// Kommunikation mit der API. Einziger Ort, an dem fetch() aufgerufen wird.
|
||||
"use strict";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(status, detail) {
|
||||
super(detail);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export class OfflineError extends Error {
|
||||
constructor() {
|
||||
super("Keine Verbindung zum Server.");
|
||||
this.name = "OfflineError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Das CSRF-Token steht in einem lesbaren Cookie. Das Session-Cookie ist
|
||||
* HttpOnly und für JavaScript unsichtbar - genau so soll es sein. */
|
||||
function csrfToken() {
|
||||
const hit = document.cookie
|
||||
.split(";")
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith("ea_csrf="));
|
||||
return hit ? decodeURIComponent(hit.slice("ea_csrf=".length)) : "";
|
||||
}
|
||||
|
||||
/** Übersetzt die Fehlerform von FastAPI in einen Satz für Menschen.
|
||||
* Bei Validierungsfehlern ist "detail" eine Liste, keine Zeichenkette. */
|
||||
function describe(status, data) {
|
||||
const detail = data && data.detail;
|
||||
if (typeof detail === "string") return detail;
|
||||
if (Array.isArray(detail) && detail.length) {
|
||||
const first = detail[0];
|
||||
const field = Array.isArray(first.loc) ? first.loc[first.loc.length - 1] : "";
|
||||
return field ? `Ungültige Eingabe bei "${field}": ${first.msg}` : first.msg;
|
||||
}
|
||||
if (status === 401) return "Nicht angemeldet.";
|
||||
if (status === 429) return "Zu viele Versuche. Bitte einen Moment warten.";
|
||||
if (status >= 500) return "Der Server hat einen Fehler gemeldet.";
|
||||
return `Unerwarteter Fehler (HTTP ${status}).`;
|
||||
}
|
||||
|
||||
export async function api(path, { method = "GET", body } = {}) {
|
||||
const headers = {};
|
||||
if (body !== undefined) headers["Content-Type"] = "application/json";
|
||||
if (method !== "GET" && method !== "HEAD") headers["X-CSRF-Token"] = csrfToken();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(path, {
|
||||
method,
|
||||
headers,
|
||||
credentials: "same-origin",
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
} catch {
|
||||
// Netzwerkfehler von HTTP-Fehlern trennen: In Phase 4 entscheidet
|
||||
// genau das, ob eine Änderung in die Outbox wandert.
|
||||
throw new OfflineError();
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let data = null;
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
`Unerwartete Antwort vom Server (HTTP ${response.status}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) throw new ApiError(response.status, describe(response.status, data));
|
||||
return data;
|
||||
}
|
||||
|
||||
export const get = (path) => api(path);
|
||||
export const post = (path, body) => api(path, { method: "POST", body });
|
||||
export const patch = (path, body) => api(path, { method: "PATCH", body });
|
||||
export const put = (path, body) => api(path, { method: "PUT", body });
|
||||
export const del = (path) => api(path, { method: "DELETE" });
|
||||
Reference in New Issue
Block a user