Files
dogfather-universe/assets/js/admin-auth.js
T
DogFatherGitandClaude Opus 5 7960b212c8 Verwaltung: Zugangscode wieder bei jedem Oeffnen noetig
Dogi hat die Lockerung vom 04.08.2026 ("Code nur einmal am Tag") heute
zurueckgenommen. Gewaehlte Auspraegung: Code beim OEFFNEN der Seite/App,
Neuladen im selben Tab wirft nicht raus, keine Abmeldung bei Untaetigkeit.

- admin-auth.js legt die Sitzung jetzt in sessionStorage statt in
  localStorage: gehoert zum Tab/App-Fenster, uebersteht F5 und Uploads,
  ist beim naechsten Oeffnen weg. Zweiter Tab = eigener Code.
- Alte localStorage-Sitzung wird beim ersten Laden einmalig entfernt --
  sonst waere Dogi trotz Umstellung mit dem alten Token weiter drin und
  ein Token laege monatelang im Browser herum.
- Versionsnummer des Skripts hochgesetzt: Caddy liefert Assets mit
  max-age=14400, der Browser haette die alte Anmelde-Logik sonst bis zu
  vier Stunden weiterbenutzt.
- Der 24-Std-Ablauf bleibt als Rueckfallsicherung fuer tagelang offene
  Tabs; der Server erzwingt dieselbe Grenze weiterhin selbst.
- Neuer Test pruef-verwaltung-anmeldung.mjs (12 Pruefungen: Gate beim
  Oeffnen, alte Sitzung wirkungslos, falscher/richtiger Code, F5 bleibt
  drin, neuer Tab verlangt Code, Abmelden raeumt auf, Untaetigkeit meldet
  NICHT ab).
- pruef-verwaltung-kacheln.mjs: echten Aufruf ans Live-Backend abgefangen
  und auf die Supporter-Zeilen gewartet -- der Test hatte dadurch
  gelegentlich falsch gemeldet.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 13:28:14 +02:00

123 lines
4.7 KiB
JavaScript

/* =====================================================================
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 };
}