/* ===================================================================== admin-auth.js — gemeinsame Session-Logik fürs interne Team-Login (dasselbe Owner-/Rollen-System wie schon fürs frühere Bewerbungs- Postfach). WO DIE SITZUNG LIEGT — geändert am 27.08.2026 Bis dahin lag die Sitzung (Token + Rolle/Rechte) in localStorage, weil der Zugangscode auf Wunsch vom 04.08.2026 nur "einmal am Tag" nötig sein sollte. Dogi hat das am 27.08.2026 bewusst wieder zurückgenommen ("jedes mal"), und zwar in der Ausprägung: Code beim ÖFFNEN der Seite/App — Neuladen im selben Tab soll nicht rauswerfen, eine Untätigkeits- Abmeldung will er ausdrücklich nicht. Genau das leistet sessionStorage: es gehört zum einzelnen Tab bzw. zum Fenster der installierten App, übersteht ein Neuladen (F5, Formular- Absenden, Bild-Upload), wird aber beim Schließen des Tabs/der App vom Browser selbst geleert. Ein zweiter Tab startet mit leerem Speicher und verlangt deshalb ebenfalls den Code. localStorage könnte das nicht — es überlebt absichtlich das Schließen des Browsers. Der 24-Stunden-Ablauf (SESSION_MAX_AGE_MS) bleibt als Rückfallsicherung für den Fall, dass ein Tab tagelang offen liegen bleibt; der Server erzwingt dieselbe Grenze noch einmal eigenständig (server-internal/ lib/auth.js). Sicherheit hängt also nicht am Browser allein. Ursprünglich für postfach.html/zugaenge.html gebaut (Bewerbungs- Verwaltung) — die wurden am 01.08.2026 auf Dogis Wunsch entfernt ("nicht mehr gebraucht"). Diese Datei selbst ist aber ein reines, seitenneutrales Login-Werkzeug (kein Bewerbungscode) und wurde am 04.08.2026 reaktiviert für verwaltung.html (Supporter-Abo-Administration) — nutzt exakt denselben, weiterhin voll funktionsfähigen Backend-Login (POST /auth/login), nur eine neue Seite drumherum. ===================================================================== */ const SESSION_KEY = "dogfather_admin_session"; const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000; /* Altlast aufräumen: Wer vor dem 27.08.2026 eingeloggt war, hat noch eine Sitzung samt Token in localStorage liegen. Die wird jetzt nicht mehr gelesen — sie soll aber auch nicht monatelang im Browser herumliegen, deshalb wird sie beim ersten Laden dieser Datei einmalig entfernt. */ try { localStorage.removeItem(SESSION_KEY); } catch { /* z.B. Speicher gesperrt */ } function getSession() { try { const session = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "null"); if (!session) return null; if (!session.loggedInAt || Date.now() - session.loggedInAt > SESSION_MAX_AGE_MS) { clearSession(); // Rückfallsicherung: Tag-alter Tab -> neuer Code nötig return null; } return session; } catch { return null; } } function setSession(data) { sessionStorage.setItem(SESSION_KEY, JSON.stringify({ ...data, loggedInAt: Date.now() })); } function clearSession() { sessionStorage.removeItem(SESSION_KEY); } /** Prüft, ob die aktuelle Session (Owner oder Rolle+Overrides) ein Recht hat. */ function sessionHasPermission(session, key) { if (!session) return false; if (session.isOwner) return true; const overrides = session.overrides || {}; if (Object.prototype.hasOwnProperty.call(overrides, key)) return !!overrides[key]; return Array.isArray(session.permissions) && session.permissions.includes(key); } async function apiCall(apiBaseUrl, path, body, { auth = true } = {}) { const session = getSession(); const headers = { "Content-Type": "application/json" }; if (auth && session?.token) headers["Authorization"] = `Bearer ${session.token}`; const res = await fetch(`${apiBaseUrl}${path}`, { method: "POST", headers, body: JSON.stringify(body || {}), }); let data; try { data = await res.json(); } catch { data = { ok: false, error: "Ungültige Antwort vom Server." }; } if (res.status === 401 && auth) { clearSession(); // Session ist abgelaufen/ungültig -> zurück zum Login zwingen } return { httpOk: res.ok, ...data }; } async function doLogin(apiBaseUrl, code) { const res = await apiCall(apiBaseUrl, "/auth/login", { code }, { auth: false }); if (res.ok) { setSession({ token: res.token, isOwner: res.isOwner, name: res.name, username: res.username, roleName: res.roleName, permissions: res.permissions || [], overrides: res.overrides || {}, }); } return res; } async function doLogout(apiBaseUrl) { await apiCall(apiBaseUrl, "/auth/logout", {}); clearSession(); } // Für Einbindung ohne Modul-Bundler global verfügbar machen. if (typeof window !== "undefined") { window.AdminAuth = { getSession, setSession, clearSession, sessionHasPermission, apiCall, doLogin, doLogout }; }