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]>
This commit is contained in:
2026-08-27 13:28:14 +02:00
co-authored by Claude Opus 5
parent 0465a7a14a
commit 7960b212c8
4 changed files with 226 additions and 20 deletions
+30 -12
View File
@@ -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. */
+170
View File
@@ -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);
})();
+13
View File
@@ -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;
}
+13 -8
View File
@@ -1289,7 +1289,11 @@
<script src="assets/js/data-manager.js?v=20260828b"></script>
<script src="assets/js/data-stimmen-avatare.js?v=20260828b"></script>
<script src="assets/js/main.js?v=20260828b"></script>
<script src="assets/js/admin-auth.js?v=20260828b"></script>
<!-- Versionsnummer bewusst hochgesetzt (27.08.2026): Caddy liefert Assets mit
max-age=14400, ein Browser hätte die alte Anmelde-Logik sonst bis zu vier
Stunden weiterbenutzt und Dogi wäre trotz Umstellung weiter ohne Code
drin gewesen. Neue Adresse = sofort neue Datei. -->
<script src="assets/js/admin-auth.js?v=20260827-jedesmal"></script>
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
@@ -3068,13 +3072,14 @@
document.getElementById("liste-ansicht").style.display = "";
});
// Nutzer-Wunsch 04.08.2026 (aktualisiert): der Zugangscode soll nur
// EINMAL PRO TAG eingegeben werden müssen, nicht bei jedem Besuch (das
// war die ursprüngliche, jetzt bewusst gelockerte Regel). admin-auth.js
// speichert die Sitzung dafür seit heute in localStorage mit eigenem
// 24-Std-Ablauf, der Server erzwingt zusätzlich dasselbe Zeitlimit
// (lib/auth.js) — läuft die Sitzung ab, verlangt pruefeSession() ganz
// automatisch wieder den Gate-Bildschirm.
// Nutzer-Wunsch 27.08.2026: der Zugangscode wird wieder bei JEDEM ÖFFNEN
// der Seite/App verlangt (die Lockerung "nur einmal am Tag" vom 04.08.2026
// ist damit aufgehoben). Umgesetzt in admin-auth.js über sessionStorage:
// gehört zum Tab/App-Fenster, übersteht ein Neuladen (F5, Upload), ist
// beim nächsten Öffnen aber weg. Eine Abmeldung bei Untätigkeit wurde
// ausdrücklich NICHT gewünscht. Der Server erzwingt zusätzlich sein
// eigenes 24-Std-Limit (server-internal/lib/auth.js) — fehlt oder taugt
// die Sitzung nicht, zeigt pruefeSession() automatisch wieder das Gate.
pruefeSession();
})();
</script>