diff --git a/pruef-personen.png b/pruef-personen.png new file mode 100644 index 00000000..2f6ef233 Binary files /dev/null and b/pruef-personen.png differ diff --git a/server/pruef-personen-liste.mjs b/server/pruef-personen-liste.mjs new file mode 100644 index 00000000..5b3250ff --- /dev/null +++ b/server/pruef-personen-liste.mjs @@ -0,0 +1,180 @@ +/* =================================================================== + Pruefung: In der Personenliste steht standardmaessig nur DogFather, + alle anderen kommen auf Knopfdruck dazu. + + Der wichtigste Punkt ist der letzte: Verstecken ist eine ANSICHT, kein + Recht. Wer eingeklappt ist, hat unveraendert Zugang -- der Server darf + davon gar nichts mitbekommen. + =================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-personen-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4187"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4187"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 180_000).unref?.(); + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +function anlegen(name, rolle, code, aktiv = 1) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,?,?)") + .run(name, rolle, hash, salt, 32768, aktiv, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +/* Genau die Aufstellung aus dem Bild vom 31.08.2026. */ +anlegen("Dogfather", "admin", "CODE-DOGI-0001"); +anlegen("VanVan", "admin", "CODE-VANV-0001"); +anlegen("BananaStift", "scout", "CODE-BANA-0001"); +anlegen("Patrick", "scout", "CODE-PATR-0001"); +anlegen("SpongBobSchwammKopf", "creator", "CODE-SPON-0001"); +anlegen("Tili", "creator", "CODE-TILI-0001"); +anlegen("BanaStift", "manager", "CODE-BSTF-0001", 0); // gesperrt +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function seiteFuer(kontext) { + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(600); + return seite; +} + +const lesen = (seite) => seite.evaluate(() => ({ + namen: [...document.querySelectorAll(".person__name")].map((n) => n.textContent.replace(" (du)", "")), + knopfDa: !document.getElementById("rest-schalter")?.hidden, + knopfText: document.getElementById("rest-schalter-text")?.textContent, + auf: document.getElementById("rest-schalter")?.getAttribute("aria-expanded"), + ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, +})); + +const kontext = await browser.newContext({ viewport: { width: 1440, height: 1100 } }); +const konsole = []; +const seite = await seiteFuer(kontext); +seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); +seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message)); + +console.log("\n1) Standardmäßig nur DogFather"); +{ + const r = await lesen(seite); + console.log(" sichtbar:", r.namen.join(", ")); + console.log(" Knopf: ", r.knopfText); + ok(r.namen.length === 2, `zwei Personen sichtbar (${r.namen.length})`); + ok(r.namen.includes("Dogfather") && r.namen.includes("VanVan"), + `Dogfather und VanVan (${r.namen.join(", ")})`); + ok(!r.namen.includes("Tili"), "kein Creator"); + ok(!r.namen.includes("Patrick"), "kein Scout"); + ok(r.knopfDa, "der Knopf ist da"); + /* Die ZAHL gehoert auf den Knopf -- "Alle zeigen" sagt nicht, wie + viele man gerade nicht sieht. */ + ok(/5 weitere zeigen/.test(r.knopfText || ""), `mit der Anzahl (${r.knopfText})`); + ok(/1 gesperrt/.test(r.knopfText || ""), "und dem Hinweis auf die gesperrte Person"); + ok(r.auf === "false", `aria-expanded=false (${r.auf})`); +} + +console.log("\n2) Auf Knopfdruck kommen alle dazu"); +{ + await seite.click("#rest-schalter"); + await seite.waitForTimeout(250); + const r = await lesen(seite); + console.log(" sichtbar:", r.namen.join(", ")); + ok(r.namen.length === 7, `alle sieben (${r.namen.length})`); + ok(r.namen.includes("BanaStift"), "auch die gesperrte Person"); + ok(/Nur DogFather zeigen/.test(r.knopfText || ""), `Knopf bietet das Zuklappen an (${r.knopfText})`); + ok(r.auf === "true", `aria-expanded=true (${r.auf})`); + ok(r.ueber <= 0, `nichts steht über (${r.ueber}px)`); +} + +console.log("\n3) Der Zustand überlebt das Neuladen"); +{ + /* Ein Knopf, den man nach jedem Laden neu druecken muss, ist keine + Einstellung, sondern eine Zumutung. */ + await seite.reload({ waitUntil: "networkidle" }); + await seite.waitForTimeout(600); + const r = await lesen(seite); + ok(r.namen.length === 7, `nach dem Neuladen weiterhin alle (${r.namen.length})`); + + await seite.click("#rest-schalter"); + await seite.waitForTimeout(250); + await seite.reload({ waitUntil: "networkidle" }); + await seite.waitForTimeout(600); + const z = await lesen(seite); + ok(z.namen.length === 2, `und zugeklappt bleibt zugeklappt (${z.namen.length})`); +} + +console.log("\n4) Verstecken ist eine Ansicht, kein Recht"); +{ + /* Der springende Punkt. Wer eingeklappt ist, muss unveraendert + arbeiten koennen -- sonst waere aus einer Anzeigeeinstellung + versehentlich eine Sperre geworden. */ + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "creator", code: "CODE-TILI-0001" }), redirect: "manual" }); + ok(a.ok, `Tili kann sich anmelden, obwohl sie ausgeblendet ist (${a.status})`); + + const keks = (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; "); + const ich = await (await fetch(BASIS + "/workspace/api/ich", { headers: { cookie: keks } })).json(); + ok(ich.name === "Tili", `und ist ganz normal angemeldet (${ich.name})`); + + /* Und der Server liefert weiterhin ALLE -- das Kuerzen passiert nur + in der Anzeige. Waere es serverseitig, wuerde die naechste + Auswertung stillschweigend Personen uebersehen. */ + const dogiA = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), redirect: "manual" }); + const dogiKeks = (dogiA.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; "); + const { personen } = await (await fetch(BASIS + "/workspace/api/verwaltung/personen", + { headers: { cookie: dogiKeks } })).json(); + ok(personen.length === 7, `der Server liefert weiterhin alle sieben (${personen.length})`); +} + +ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`); +await seite.click("#rest-schalter"); +await seite.waitForTimeout(300); +await seite.screenshot({ path: "pruef-personen.png", fullPage: true }); +await kontext.close(); + +console.log("\n5) Auf dem Handy genauso"); +{ + const k = await browser.newContext({ viewport: { width: 390, height: 844 } }); + const s = await seiteFuer(k); + const r = await lesen(s); + ok(r.knopfDa, "der Knopf ist auch dort da"); + ok(r.ueber <= 0, `nichts steht über (${r.ueber}px)`); + await k.close(); +} + +await browser.close(); +console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler === 0 ? 0 : 1); diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index e8c638ca..605794ad 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -164,3 +164,8 @@ Scouts waere eine feste Schalterreihe unpraktisch. Sie bekommt aber genug Breite, damit kein Name abgeschnitten wird. */ .betreuung__wahl { min-width: 190px; } + +/* Die beiden Knöpfe oben rechts. Der Aufklapper steht LINKS vom + "Person anlegen" – die Hauptsache gehört ganz nach außen, sonst + springt der Blick beim Suchen daran vorbei. */ +.kopf-knoepfe { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 273d044b..8884fd20 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -216,6 +216,61 @@ return b; } + /* ---------- Wer steht in der Liste --------------------------------------- + + Wunsch Filipe, 31.08.2026: "ich will da nur mich und vanvan sehen, + ich will dass ich einen knopf habe wenn ich die anderen sehen will + oder nicht." + + Also: DogFather immer, alle anderen auf Knopfdruck. Drei Dinge sind + dabei wichtig: + + 1. Das ist eine ANSICHT, kein Recht. Wer eingeklappt ist, ist nicht + weg -- er wird nur nicht gezeigt. Verwechselt man das, hält man + irgendwann jemanden für gelöscht, der noch vollen Zugang hat. + Deshalb steht auf dem Knopf IMMER, wie viele gerade fehlen. + + 2. Der Zustand bleibt erhalten. Ein Knopf, den man nach jedem Laden + neu drücken muss, ist keine Einstellung, sondern eine Zumutung. + + 3. Gesperrte Personen tauchen trotzdem auf, sobald aufgeklappt ist -- + eine gesperrte Person zu verstecken wäre genau die Zeile, die man + sehen müsste. + -------------------------------------------------------------------------- */ + + const SPEICHER = 'dfw_personen_alle'; + let alle = []; + let alleZeigen = false; + try { alleZeigen = localStorage.getItem(SPEICHER) === 'ja'; } catch { /* egal */ } + + function listeZeigen() { + const ziel = $('liste'); + ziel.textContent = ''; + + const oben = alle.filter((p) => p.rolle === 'admin'); + const rest = alle.filter((p) => p.rolle !== 'admin'); + for (const p of (alleZeigen ? alle : oben)) ziel.append(zeile(p)); + + const schalter = $('rest-schalter'); + schalter.hidden = rest.length === 0; + if (!rest.length) return; + + /* Die Zahl gehört auf den Knopf, nicht nur der Zustand. "Alle + zeigen" sagt nicht, wie viele man gerade nicht sieht. */ + const gesperrt = rest.filter((p) => !p.aktiv).length; + $('rest-schalter-text').textContent = alleZeigen + ? 'Nur DogFather zeigen' + : `${rest.length} weitere zeigen` + + (gesperrt ? ` · ${gesperrt} gesperrt` : ''); + schalter.setAttribute('aria-expanded', alleZeigen ? 'true' : 'false'); + } + + $('rest-schalter').addEventListener('click', () => { + alleZeigen = !alleZeigen; + try { localStorage.setItem(SPEICHER, alleZeigen ? 'ja' : 'nein'); } catch { /* egal */ } + listeZeigen(); + }); + async function laden() { const ziel = $('liste'); try { @@ -223,9 +278,10 @@ if (!a.ok) { melde('Personen konnten nicht geladen werden.'); return; } const { personen, betreuer } = await a.json(); betreuerListe = betreuer || []; + alle = personen; ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); - for (const p of personen) ziel.append(zeile(p)); + listeZeigen(); await protokollLaden(); } catch { /* umgeleitet */ } } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index ba08b45e..8be15cf4 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -134,8 +134,8 @@ - - - + + + diff --git a/workspace/automation.html b/workspace/automation.html index 790423e3..f90ed2c0 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -127,8 +127,8 @@ - - - + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 1f09ae56..3055c944 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -98,8 +98,8 @@ - - - + + + diff --git a/workspace/calls.html b/workspace/calls.html index 94c8db5f..32e310d9 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -49,9 +49,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index e892dd5c..5675162f 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -153,8 +153,8 @@ - - - + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 4ffa0e8c..56e61548 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -88,8 +88,8 @@ - - - + + + diff --git a/workspace/index.html b/workspace/index.html index d5754445..1fd075f5 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 6d66884e..c153380d 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -94,8 +94,8 @@ - - - + + + diff --git a/workspace/personen.html b/workspace/personen.html index 16e5353c..2de6b1e3 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -43,7 +43,19 @@

Zugänge

Personen & Zugangscodes

- +
+ + + +