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 @@ - + +