Inital Commit

This commit is contained in:
2026-03-22 19:26:35 +01:00
commit 705329d3c2
17 changed files with 5538 additions and 0 deletions

114
public/js/api.js Normal file
View File

@@ -0,0 +1,114 @@
async function parseError(res, text) {
try {
const j = JSON.parse(text);
if (j.message) {
const base = Array.isArray(j.message) ? j.message.join(', ') : j.message;
const parts = [base];
if (j.detail && String(j.detail).trim()) {
parts.push(String(j.detail).trim());
}
if (j.hint && String(j.hint).trim()) {
parts.push(String(j.hint).trim());
}
return parts.join(' — ');
}
} catch {
/* ignore */
}
return text || res.statusText;
}
function redirectToLogin() {
if (
!location.hash.startsWith('#/login') &&
!location.hash.startsWith('#/bootstrap')
) {
location.hash = '#/login';
}
}
/** Geschützte REST-API liegt unter /api (Root-URLs bleiben für die SPA frei). */
function apiUrl(path) {
if (path.startsWith('/auth/')) return path;
return `/api${path.startsWith('/') ? path : `/${path}`}`;
}
function onUnauthorized(path) {
if (path.startsWith('/auth/')) return;
redirectToLogin();
}
/** Wird bei 401 geworfen: Aufrufer sollen keine Fehlerseite rendern. */
export function isAuthRedirectError(e) {
return Boolean(e && (e.authRedirect === true || e.name === 'AuthRedirect'));
}
function authRedirectError() {
const err = new Error('SESSION');
err.name = 'AuthRedirect';
err.authRedirect = true;
return err;
}
function parseJsonBody(text) {
if (!text) return null;
const trimmed = text.trim();
if (trimmed.startsWith('<')) {
throw new Error(
'Server lieferte HTML statt JSON (API-URL/Proxy prüfen oder Server neu starten).',
);
}
try {
return JSON.parse(text);
} catch (e) {
throw new Error(
e && String(e.message || e).includes('JSON')
? 'Ungültige Server-Antwort (kein JSON).'
: e.message || 'Ungültige Server-Antwort',
);
}
}
async function apiRequest(method, path, body) {
const url = apiUrl(path);
const opt = { method, credentials: 'include', headers: {} };
if (body !== undefined) {
opt.headers['Content-Type'] = 'application/json';
opt.body = JSON.stringify(body);
}
const res = await fetch(url, opt);
const text = await res.text();
if (res.status === 401) {
if (path.startsWith('/auth/')) {
throw new Error((await parseError(res, text)) || 'Anmeldung fehlgeschlagen');
}
onUnauthorized(path);
throw authRedirectError();
}
if (!res.ok) throw new Error(await parseError(res, text));
return parseJsonBody(text);
}
export async function apiGet(path) {
return apiRequest('GET', path);
}
export async function apiPost(path, body) {
return apiRequest('POST', path, body);
}
export async function apiPut(path, body) {
return apiRequest('PUT', path, body);
}
export async function apiDelete(path) {
return apiRequest('DELETE', path);
}
/** Öffentlich: keine Session nötig, kein Redirect bei 401 */
export async function authFetchStatus() {
const res = await fetch('/auth/status', { credentials: 'include' });
const text = await res.text();
if (!res.ok) throw new Error(await parseError(res, text));
return parseJsonBody(text);
}