diff --git a/assets/js/admin-auth.js b/assets/js/admin-auth.js
index 2ade1235..15530bd8 100644
--- a/assets/js/admin-auth.js
+++ b/assets/js/admin-auth.js
@@ -3,14 +3,26 @@
(dasselbe Owner-/Rollen-System wie schon fürs frühere Bewerbungs-
Postfach).
- Session (Token + Rolle/Rechte) liegt in localStorage (Nutzer-Wunsch
- 04.08.2026: "den Zugangscode auf der Verwaltungsseite nur einmal am Tag
- eingeben müssen") — localStorage übersteht anders als sessionStorage
- auch das Schließen des Tabs/Browsers, sonst wäre "einmal am Tag" nicht
- möglich. Eigener 24-Stunden-Ablauf hier im Client (siehe SESSION_MAX_AGE_MS)
- zusätzlich zum server-seitigen Session-Timeout (lib/auth.js, ebenfalls
- 24 Std) — beide zusammen sorgen dafür, dass wirklich niemand länger als
- einen Tag mit einem alten Code eingeloggt bleiben kann.
+ 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
@@ -24,12 +36,18 @@
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(localStorage.getItem(SESSION_KEY) || "null");
+ 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(); // älter als 24 Std -> als abgelaufen behandeln, neuer Code nötig
+ clearSession(); // Rückfallsicherung: Tag-alter Tab -> neuer Code nötig
return null;
}
return session;
@@ -39,11 +57,11 @@ function getSession() {
}
function setSession(data) {
- localStorage.setItem(SESSION_KEY, JSON.stringify({ ...data, loggedInAt: Date.now() }));
+ sessionStorage.setItem(SESSION_KEY, JSON.stringify({ ...data, loggedInAt: Date.now() }));
}
function clearSession() {
- localStorage.removeItem(SESSION_KEY);
+ sessionStorage.removeItem(SESSION_KEY);
}
/** Prüft, ob die aktuelle Session (Owner oder Rolle+Overrides) ein Recht hat. */
diff --git a/pruef-verwaltung-anmeldung.mjs b/pruef-verwaltung-anmeldung.mjs
new file mode 100644
index 00000000..6828567c
--- /dev/null
+++ b/pruef-verwaltung-anmeldung.mjs
@@ -0,0 +1,170 @@
+/* =====================================================================
+ Verwaltung: Zugangscode bei JEDEM Öffnen (Nutzer-Wunsch 27.08.2026)
+
+ Geprüft wird die Regel, die Dogi ausgewählt hat:
+ - Code beim Öffnen der Seite/App
+ - Neuladen im selben Tab wirft NICHT raus
+ - jeder neue Tab / jeder App-Start verlangt wieder den Code
+ - keine Abmeldung bei Untätigkeit (ausdrücklich nicht gewünscht)
+
+ Zusätzlich: die alte, tagelang gültige Sitzung aus localStorage darf
+ nicht mehr greifen und wird beim ersten Laden entfernt — sonst wäre
+ Dogi nach dem Update weiterhin ohne Code drin.
+
+ Der Login-Endpunkt liegt auf postfach.dogfather-universe.com; er wird
+ hier abgefangen und beantwortet (inkl. CORS-Vorabfrage), damit kein
+ echter Zugangscode nötig ist.
+ ===================================================================== */
+import { chromium } from "playwright";
+import http from "http";
+import fs from "fs";
+import path from "path";
+
+const TYP = {
+ ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
+ ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp",
+ ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg",
+ ".webmanifest": "application/manifest+json",
+};
+
+const srv = http.createServer((q, r) => {
+ const p = path.join(process.cwd(), decodeURIComponent(q.url.split("?")[0]));
+ if (fs.existsSync(p) && fs.statSync(p).isFile()) {
+ r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" });
+ return r.end(fs.readFileSync(p));
+ }
+ r.writeHead(404);
+ r.end("");
+});
+
+let ok = 0, fehl = 0;
+const funde = [];
+const pruefe = (name, gut, info) => {
+ if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
+ else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); }
+};
+
+const CORS = {
+ "access-control-allow-origin": "*",
+ "access-control-allow-headers": "content-type,authorization",
+ "access-control-allow-methods": "POST,OPTIONS",
+};
+
+const URL_SEITE = "http://127.0.0.1:4426/verwaltung.html";
+const RICHTIGER_CODE = "GEHEIM-TEST";
+
+/** Fängt alle Aufrufe ans Postfach-Backend ab. */
+async function backendAttrappe(ziel) {
+ await ziel.route("https://postfach.dogfather-universe.com/**", async (route) => {
+ const req = route.request();
+ if (req.method() === "OPTIONS") return route.fulfill({ status: 204, headers: CORS, body: "" });
+ const pfad = new URL(req.url()).pathname;
+ const daten = (() => {
+ if (pfad === "/auth/login") {
+ const code = (req.postDataJSON() || {}).code;
+ return code === RICHTIGER_CODE
+ ? { ok: true, token: "test-token", isOwner: true, name: "Dogi", permissions: [] }
+ : { ok: false, error: "Zugangscode falsch." };
+ }
+ if (pfad === "/admin/supporter/list") return { ok: true, supporters: [] };
+ if (pfad === "/admin/overview") return { ok: true, premiums: [], payments: [] };
+ if (pfad === "/admin/events/list") return { ok: true, events: [null, null] };
+ if (pfad === "/admin/polls/list") return { ok: true, polls: [] };
+ if (pfad === "/admin/team/list") return { ok: true, members: [] };
+ if (pfad === "/admin/testimonials/list") return { ok: true, testimonials: [] };
+ if (pfad === "/admin/special-event") return { ok: true, event: null };
+ return { ok: true };
+ })();
+ await route.fulfill({ status: 200, headers: { ...CORS, "content-type": "application/json" }, body: JSON.stringify(daten) });
+ });
+}
+
+const lage = (seite) => seite.evaluate(() => ({
+ gateSichtbar: getComputedStyle(document.getElementById("gate-container")).display !== "none",
+ verwaltungSichtbar: getComputedStyle(document.getElementById("supp-root")).display !== "none",
+ imTab: !!sessionStorage.getItem("dogfather_admin_session"),
+ imBrowser: !!localStorage.getItem("dogfather_admin_session"),
+}));
+
+(async () => {
+ await new Promise((r) => srv.listen(4426, "127.0.0.1", r));
+ const browser = await chromium.launch();
+ const kontext = await browser.newContext();
+ await backendAttrappe(kontext);
+
+ /* ---------- 1. Frisch geöffnet ---------- */
+ console.log("\n== Frisch geöffnet ==");
+ const tab = await kontext.newPage();
+ await tab.goto(URL_SEITE);
+ await tab.waitForTimeout(400);
+ let l = await lage(tab);
+ pruefe("Zugangswand wird verlangt", l.gateSichtbar && !l.verwaltungSichtbar, JSON.stringify(l));
+
+ /* ---------- 2. Alte Tages-Sitzung aus localStorage ---------- */
+ console.log("\n== Alte 24-Std-Sitzung aus localStorage ==");
+ await tab.evaluate(() => localStorage.setItem("dogfather_admin_session", JSON.stringify({
+ token: "alt-token", isOwner: true, name: "Dogi", permissions: [], loggedInAt: Date.now(),
+ })));
+ await tab.reload();
+ await tab.waitForTimeout(400);
+ l = await lage(tab);
+ pruefe("Alte Sitzung öffnet NICHT mehr", l.gateSichtbar && !l.verwaltungSichtbar, JSON.stringify(l));
+ pruefe("Alte Sitzung wird aus dem Browser entfernt", !l.imBrowser, "localStorage leer: " + !l.imBrowser);
+
+ /* ---------- 3. Falscher Code ---------- */
+ console.log("\n== Falscher Code ==");
+ await tab.fill("#code-input-dogi", "FALSCH");
+ await tab.press("#code-input-dogi", "Enter");
+ await tab.waitForTimeout(500);
+ l = await lage(tab);
+ pruefe("Falscher Code kommt nicht rein", l.gateSichtbar && !l.verwaltungSichtbar, JSON.stringify(l));
+
+ /* ---------- 4. Richtiger Code ---------- */
+ console.log("\n== Richtiger Code ==");
+ await tab.fill("#code-input-dogi", RICHTIGER_CODE);
+ await tab.press("#code-input-dogi", "Enter");
+ await tab.waitForTimeout(800);
+ l = await lage(tab);
+ pruefe("Verwaltung öffnet sich", !l.gateSichtbar && l.verwaltungSichtbar, JSON.stringify(l));
+ pruefe("Sitzung liegt im Tab (sessionStorage)", l.imTab, "sessionStorage gesetzt");
+ pruefe("Sitzung liegt NICHT dauerhaft im Browser", !l.imBrowser, "localStorage leer");
+
+ /* ---------- 5. Neuladen im selben Tab ---------- */
+ console.log("\n== Neuladen im selben Tab (F5) ==");
+ await tab.reload();
+ await tab.waitForTimeout(600);
+ l = await lage(tab);
+ pruefe("Neuladen wirft nicht raus", !l.gateSichtbar && l.verwaltungSichtbar, JSON.stringify(l));
+
+ /* ---------- 6. Neuer Tab ---------- */
+ console.log("\n== Neuer Tab / neuer App-Start ==");
+ const tab2 = await kontext.newPage();
+ await tab2.goto(URL_SEITE);
+ await tab2.waitForTimeout(400);
+ const l2 = await lage(tab2);
+ pruefe("Neuer Tab verlangt den Code", l2.gateSichtbar && !l2.verwaltungSichtbar, JSON.stringify(l2));
+ await tab2.close();
+
+ /* ---------- 7. Abmelden ---------- */
+ console.log("\n== Abmelden ==");
+ await tab.click("#logout-btn");
+ await tab.waitForTimeout(600);
+ l = await lage(tab);
+ pruefe("Abmelden zeigt wieder die Zugangswand", l.gateSichtbar && !l.verwaltungSichtbar, JSON.stringify(l));
+ pruefe("Abmelden räumt die Sitzung weg", !l.imTab && !l.imBrowser, "beide Speicher leer");
+
+ /* ---------- 8. Kein Rauswurf beim Herumliegen ---------- */
+ console.log("\n== Untätigkeit (soll NICHT abmelden) ==");
+ await tab.fill("#code-input-dogi", RICHTIGER_CODE);
+ await tab.press("#code-input-dogi", "Enter");
+ await tab.waitForTimeout(800);
+ await tab.waitForTimeout(5000); // 5 Sekunden unberührt liegen lassen
+ l = await lage(tab);
+ pruefe("Nach Untätigkeit weiterhin eingeloggt", !l.gateSichtbar && l.verwaltungSichtbar, JSON.stringify(l));
+
+ await browser.close();
+ srv.close();
+ console.log("\n===== " + ok + " ok, " + fehl + " Fehler =====");
+ if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); }
+ process.exit(fehl ? 1 : 0);
+})();
diff --git a/pruef-verwaltung-kacheln.mjs b/pruef-verwaltung-kacheln.mjs
index 01e5f33c..79fd044a 100644
--- a/pruef-verwaltung-kacheln.mjs
+++ b/pruef-verwaltung-kacheln.mjs
@@ -123,9 +123,22 @@ async function laden(browser, viewport, dateiname) {
await seite.route("**/assets/js/admin-auth.js*", (route) =>
route.fulfill({ status: 200, contentType: "text/javascript", body: ATTRAPPE })
);
+ /* Die Seite holt die Website-Texte mit einem eigenen fetch() direkt vom
+ Live-Backend — das ginge im Test wirklich ins Netz und verzögerte je
+ nach Laufzeit alles, was danach kommt (die Supporter-Liste wird erst
+ danach gefüllt). Deshalb hier abfangen: der Test soll messen, was die
+ Seite tut, nicht wie schnell der Server gerade antwortet. */
+ await seite.route("https://postfach.dogfather-universe.com/**", (route) =>
+ route.fulfill({
+ status: 200,
+ headers: { "access-control-allow-origin": "*", "content-type": "application/json" },
+ body: JSON.stringify({ ok: true, texte: {} }),
+ })
+ );
seite.on("pageerror", (e) => pruefe("Kein JS-Fehler (" + dateiname + ")", false, (e.stack || e.message).split("\n").slice(0, 4).join(" | ")));
await seite.goto("http://127.0.0.1:4422/verwaltung.html");
await seite.waitForSelector("[data-testimonial-id]", { timeout: 8000 });
+ await seite.waitForSelector("#supporter-tbody tr[data-id]", { timeout: 8000 });
await seite.waitForTimeout(400);
return seite;
}
diff --git a/verwaltung.html b/verwaltung.html
index 0e0090b0..a0e3bd67 100644
--- a/verwaltung.html
+++ b/verwaltung.html
@@ -1289,7 +1289,11 @@
-
+
+