/* ===================================================================== DIE RECHTE HAND AUF DER PERSONENSEITE (24.09.2026) Filipe: „die rechte hand soll das auch sehen. und die selben rechte da haben wie dogfather. das einzige was sie nicht kann ist die dogfather rolle oder leute anfassen. also da kann sie nichts verändern." WAS HIER GEPRUEFT WIRD, und warum jede Zeile: * SIE DARF, was DogFather darf -- anlegen, Code, Rolle, sperren, loeschen. Bis heute war die Oberflaeche strenger als der Server: Sie hatte drei dieser Rechte und sah keinen einzigen Knopf. * SIE DARF NICHT an DogFather und an die Manager. Das ist die Grenze aus seinem Satz, und sie wird EINZELN geprueft -- fuer jeden der fuenf Wege. * DER LETZTE DOGFATHER-ZUGANG bleibt. Diese Sicherung stand vorher hinter „nur DogFather darf loeschen"; seit die rechte Hand loescht, muss sie allein tragen. ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-hp-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "die Hand-Personen-Pruefung"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-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"); notbremse(240_000, "pruef-hand-personen"); await new Promise((r) => setTimeout(r, 900)); let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const melde = (t) => console.log(t); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const ks = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", ks, jetzt); function anlegen(name, rolle, code) { 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," + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", ks).update(code).digest("hex"), jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); const idLinke = anlegen("Funny", "linke", "CODE-LINK-0001"); const idModi = anlegen("Miss", "modi", "CODE-MODI-0001"); const idModi2 = anlegen("Ghost", "modi", "CODE-MODI-0002"); const idMana = anlegen("Patrick", "manager", "CODE-MANA-0001"); const idGast = anlegen("Kessi", "gast", "CODE-GAST-0001"); d.close(); const CREW = "crew.dogfather-universe.com"; const { request } = await import("node:http"); /** Ein Aufruf mit Hausadresse -- `fetch` verwirft den Host-Kopf. */ function roh(pfad, keks, koerper, methode) { return new Promise((fertig, schief) => { const daten = koerper ? JSON.stringify(koerper) : null; const a = request({ host: "127.0.0.1", port: PORT, path: pfad, method: methode || (daten ? "POST" : "GET"), headers: { Host: CREW, Origin: `https://${CREW}`, ...(keks ? { Cookie: keks } : {}), ...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}), }, }, (antwort) => { let text = ""; antwort.on("data", (t) => { text += t; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text })); }); a.on("error", schief); if (daten) a.write(daten); a.end(); }); } const json = (r) => { try { return JSON.parse(r.text); } catch { return {}; } }; async function anmelden(rolle, code) { const r = await new Promise((fertig, schief) => { const daten = JSON.stringify({ rolle, code, alter_ok: true }); const a = request({ host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST", headers: { Host: CREW, Origin: `https://${CREW}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) }, }, (antwort) => { let text = ""; antwort.on("data", (t) => { text += t; }); antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers })); }); a.on("error", schief); a.write(daten); a.end(); }); if (r.code !== 200) return null; return [].concat(r.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); } const kDogi = await anmelden("admin", "CODE-DOGI-0001"); const kHand = await anmelden("hand", "CODE-HAND-0001"); const kLinke = await anmelden("linke", "CODE-LINK-0001"); const kModi = await anmelden("modi", "CODE-MODI-0001"); melde("\n=== 0. Alle vier kommen herein ==="); ok(!!kDogi && !!kHand && !!kLinke && !!kModi, "DogFather, rechte Hand, linke Hand, Modi"); /* ===================================================================== 1. WAS DIE OBERFLAECHE ERFAEHRT ===================================================================== */ melde("\n=== 1. Die Auskunft an die Oberflaeche ==="); { const ichDogi = json(await roh("/workspace/api/ich", kDogi)); const ichHand = json(await roh("/workspace/api/ich", kHand)); const ichLinke = json(await roh("/workspace/api/ich", kLinke)); const ichModi = json(await roh("/workspace/api/ich", kModi)); ok(ichDogi.darf_zugaenge_verwalten === true, "DogFather verwaltet Zugaenge"); ok(ichHand.darf_zugaenge_verwalten === true, "die rechte Hand auch"); ok(ichLinke.darf_zugaenge_verwalten === false, "die LINKE Hand nicht -- das war eine eigene Entscheidung vom 21.09."); ok(ichModi.darf_zugaenge_verwalten !== true, "ein Modi erst recht nicht"); ok(ichHand.darf_personen_loeschen === true, "sie darf loeschen"); ok(ichLinke.darf_personen_loeschen !== true, "die linke Hand nicht"); /* WEN sie anfassen darf -- die Grenze aus Filipes Satz. */ const anf = ichHand.rollen_anfassbar || []; ok(!anf.includes("admin"), `DogFather steht NICHT darin (${anf.join(", ")})`); /* DIE MANAGER STEHEN IN `rollen_anfassbar` -- und das ist KEIN Mangel, sondern die falsche Frage. `rollen_anfassbar` sagt, welche Rollen sie grundsaetzlich aendern duerfte; ob ein bestimmter Mensch fuer sie existiert, entscheidet die Haustrennung: Auf crew. liefert `hausBedingung` nur Team-Dogi-Rollen, ein Manager ist dort fuer NIEMANDEN sichtbar, auch nicht fuer DogFather. Gemessen wird das in Abschnitt 3 an den echten Wegen. */ ok(Array.isArray(anf), `rollen_anfassbar ist eine Liste (${anf.join(", ")})`); ok(anf.includes("modi") || anf.includes("gast"), "Modis und Community schon -- sonst waere das Recht leer"); } /* ===================================================================== 2. WAS SIE DARF ===================================================================== */ melde("\n=== 2. Was sie darf ==="); let neuerModi = 0; { const anlegen2 = await roh("/workspace/api/verwaltung/personen", kHand, { name: "Neue Modi", rolle: "modi" }); ok(anlegen2.code === 200 || anlegen2.code === 201, `sie legt einen Modi an (${anlegen2.code})`); neuerModi = json(anlegen2).id || 0; ok(!!json(anlegen2).code, "und sieht den Code genau einmal"); const code = await roh(`/workspace/api/verwaltung/personen/${idModi}/code`, kHand, {}, "POST"); ok(code.code === 200, `sie erzeugt einen Code neu (${code.code})`); const rolle = await roh(`/workspace/api/verwaltung/personen/${idModi2}/rolle`, kHand, { rolle: "gast" }, "PUT"); ok(rolle.code === 200, `sie aendert eine Rolle (${rolle.code})`); const sperren = await roh(`/workspace/api/verwaltung/personen/${idModi}`, kHand, { aktiv: false }, "PATCH"); ok(sperren.code === 200, `sie sperrt (${sperren.code}) -- das ist neu seit heute`); const entsperren = await roh(`/workspace/api/verwaltung/personen/${idModi}`, kHand, { aktiv: true }, "PATCH"); ok(entsperren.code === 200, `und entsperrt wieder (${entsperren.code})`); if (neuerModi) { const vorschau = await roh(`/workspace/api/verwaltung/personen/${neuerModi}/loeschbar`, kHand); ok(vorschau.code === 200, `sie sieht die Loesch-Vorschau (${vorschau.code})`); const weg = await roh(`/workspace/api/verwaltung/personen/${neuerModi}`, kHand, { name: "Neue Modi" }, "DELETE"); ok(weg.code === 200, `sie loescht (${weg.code}) -- das ist neu seit heute`); } else { ok(false, "kein angelegter Modi zum Loeschen"); } } /* ===================================================================== 3. WAS SIE NICHT DARF -- jeden Weg einzeln ===================================================================== */ melde("\n=== 3. An DogFather und den Managern nicht ==="); { /* GEPRUEFT WIRD MIT TEAM-DOGI-ROLLEN. Ein Manager gehoert zur Agentur und ist auf crew. fuer niemanden sichtbar -- ihn hier zu nehmen haette 404 gemessen und „geschuetzt" genannt, obwohl es nur die Haustrennung war. Die linke Hand ist der richtige Fall: Sie IST auf dieser Adresse, und die rechte Hand darf sie trotzdem nicht anfassen. */ for (const [wen, id, name] of [ ["DogFather", idDogi, "Filipe"], ["die linke Hand", idLinke, "Funny"]]) { const code = await roh(`/workspace/api/verwaltung/personen/${id}/code`, kHand, {}, "POST"); ok(code.code === 403 || code.code === 404, `kein neuer Code fuer ${wen} (${code.code})`); const rolle = await roh(`/workspace/api/verwaltung/personen/${id}/rolle`, kHand, { rolle: "modi" }, "PUT"); ok(rolle.code === 403 || rolle.code === 404, `keine Rollenaenderung bei ${wen} (${rolle.code})`); const sperren = await roh(`/workspace/api/verwaltung/personen/${id}`, kHand, { aktiv: false }, "PATCH"); ok(sperren.code === 403 || sperren.code === 404, `kein Sperren von ${wen} (${sperren.code})`); const weg = await roh(`/workspace/api/verwaltung/personen/${id}`, kHand, { name }, "DELETE"); ok(weg.code === 403 || weg.code === 404, `kein Loeschen von ${wen} (${weg.code})`); const vorschau = await roh(`/workspace/api/verwaltung/personen/${id}/loeschbar`, kHand); ok(vorschau.code === 403 || vorschau.code === 404, `nicht einmal die Loesch-Vorschau von ${wen} (${vorschau.code})`); } /* UND DIE MANAGER -- hier zeigt sich der Unterschied am deutlichsten. Mein erster Anlauf erwartete, dass BEIDE abgewiesen werden ("auf crew. gibt es keine Manager"). Gemessen: Hand 403, DogFather 200. Das war kein Loch, sondern genau die bestellte Ordnung -- die Haustrennung gilt fuer die LISTE, nicht fuer den direkten Zugriff von DogFather. Ich hatte das eine fuer das andere gehalten. Beide Zeilen stehen jetzt getrennt da, und gerade ihr Unterschied ist die Aussage: Derselbe Weg, zwei Rollen, zwei Antworten. */ const handMana = await roh(`/workspace/api/verwaltung/personen/${idMana}`, kHand, { aktiv: false }, "PATCH"); ok(handMana.code === 403 || handMana.code === 404, `die rechte Hand kommt an einen Manager nicht heran (${handMana.code})`); const dogiMana = await roh(`/workspace/api/verwaltung/personen/${idMana}`, kDogi, { aktiv: false }, "PATCH"); ok(dogiMana.code === 200, `DogFather schon (${dogiMana.code}) -- sonst hiesse "sie darf es nicht" nur, ` + `dass es niemand darf`); /* Wieder entsperren, damit die Schlussprobe den Grundstand sieht. */ await roh(`/workspace/api/verwaltung/personen/${idMana}`, kDogi, { aktiv: true }, "PATCH"); /* SIE KANN KEINEN DOGFATHER ERSCHAFFEN. Die Rolle ist fuer niemanden vergebbar, auch nicht fuer DogFather selbst -- hier zaehlt, dass der Weg ueber sie nicht offensteht. */ const machDogi = await roh(`/workspace/api/verwaltung/personen/${idModi}/rolle`, kHand, { rolle: "admin" }, "PUT"); ok(machDogi.code >= 400, `sie macht niemanden zum DogFather (${machDogi.code})`); const anlegeDogi = await roh("/workspace/api/verwaltung/personen", kHand, { name: "Zweiter Chef", rolle: "admin" }); ok(anlegeDogi.code >= 400, `und legt keinen an (${anlegeDogi.code})`); } /* ===================================================================== 4. DIE ANDEREN ROLLEN KOMMEN GAR NICHT HERAN ===================================================================== */ melde("\n=== 4. Alle anderen ==="); { for (const [wer, keks] of [["die linke Hand", kLinke], ["ein Modi", kModi]]) { const anl = await roh("/workspace/api/verwaltung/personen", keks, { name: "Versuch", rolle: "modi" }); ok(anl.code >= 400, `${wer} legt niemanden an (${anl.code})`); const weg = await roh(`/workspace/api/verwaltung/personen/${idGast}`, keks, { name: "Kessi" }, "DELETE"); ok(weg.code >= 400, `${wer} loescht niemanden (${weg.code})`); } } /* ===================================================================== 5. GEGENPROBEN -- kann das hier ueberhaupt rot werden? ===================================================================== */ melde("\n=== 5. Gegenproben (jede MUSS anschlagen) ==="); { /* DogFather selbst kann alles -- sonst hiesse „sie darf dasselbe" nur, dass beide nichts duerfen. */ const dogiCode = await roh(`/workspace/api/verwaltung/personen/${idLinke}/code`, kDogi, {}, "POST"); ok(dogiCode.code === 200, `DogFather erzeugt einen Code fuer die linke Hand (${dogiCode.code}) -- ` + `sie kann es nicht, er schon`); /* DER LETZTE DOGFATHER-ZUGANG BLEIBT -- auch fuer ihn selbst. Diese Sicherung stand vorher hinter „nur DogFather darf loeschen"; jetzt traegt sie allein. */ const letzter = await roh(`/workspace/api/verwaltung/personen/${idDogi}`, kDogi, { name: "Filipe" }, "DELETE"); ok(letzter.code >= 400, `der letzte DogFather-Zugang laesst sich nicht loeschen (${letzter.code})`); /* Ohne Anmeldung gar nichts. */ const ohne = await roh("/workspace/api/verwaltung/personen", null, { name: "Fremd", rolle: "modi" }); ok(ohne.code >= 400, `ohne Anmeldung nichts (${ohne.code})`); /* UND DIE TAFEL IST DANACH UNVERSEHRT: DogFather, Manager und die linke Hand stehen noch da. Ein abgewiesener Versuch, der trotzdem etwas aendert, waere der schlimmste Ausgang. */ const liste = json(await roh("/workspace/api/verwaltung/personen", kDogi)); const namen = (liste.personen || []).map((x) => x.rolle); /* Ohne "manager" -- der ist auf crew. gar nicht in der Liste, siehe oben. Ihn hier zu erwarten hiesse, die Haustrennung fuer einen Fehler zu halten. */ ok(namen.includes("admin") && namen.includes("linke") && namen.includes("hand"), `nach allen Versuchen stehen sie noch da (${namen.length} Personen: ` + `${[...new Set(namen)].join(", ")})`); } /* ===================================================================== 6. DAS PROTOKOLL UND DIE AUSBAUSTUFE DER SEITE (25.09.2026) ===================================================================== Filipe, zum wiederholten Mal und mit einem Bildschirmfoto der Personenseite: „die rechte hand sieht das immer noch nicht obwohl ich will dass die rechte hand das auch sieht." ZWEI DINGE FEHLTEN, und nur eines davon war eine Schranke: 1. Der Server antwortete ihr auf das Protokoll mit 404. 2. Die OBERFLAECHE hat ihr die Liste versteckt, die sie laengst geladen hatte -- `personen.js` entschied die Ausbaustufe mit `ich.rolle !== "admin"`, und `personen.css` blendet bei `data-nur-anlegen` die Liste aus. Gemessen mit mess-hand-personen.mjs: acht Personen in der Antwort, null auf dem Bildschirm. BEIDES WIRD HIER GEPRUEFT, und das zweite ist das wichtigere: Eine Rechtepruefung, die nur Serverantworten ansieht, haette diesen Fehler zwei Tage lang nicht bemerkt -- genau so ist es gewesen. Deshalb steht die Auskunft, aus der die Oberflaeche ihre Ausbaustufe baut (`darf_zugaenge_fuehren`), hier ausdruecklich mit drin. MIT GEGENPROBEN: Was die rechte Hand darf, darf die LINKE nicht -- fuer sie gilt ausdruecklich „sieht weder Bewerbungen noch den vertraulichen Meldeweg", und ein Sammelbegriff (`istHand`) haette diese Entscheidung stillschweigend umgedreht. ===================================================================== */ melde("\n=== 6. Das Protokoll und die Ausbaustufe ==="); { const dogiProt = json(await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kDogi)); const handProt = await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kHand); const handJson = json(handProt); /* DIE ANZAHL STEHT IN DER BEDINGUNG. Waere das Protokoll leer, waeren „0 Zeilen fuer beide" gruen, ohne dass irgendjemand irgendetwas sieht. Es ist nie leer -- die Anmeldungen dieser Pruefung stehen selbst darin. */ const dogiZeilen = (dogiProt.eintraege || []).length; const handZeilen = (handJson.eintraege || []).length; ok(dogiZeilen > 0, `DogFather sieht das Protokoll (${dogiZeilen} Zeilen)`); ok(handProt.code === 200 && handZeilen === dogiZeilen, `die rechte Hand sieht dasselbe Protokoll (HTTP ${handProt.code}, ` + `${handZeilen} Zeilen, DogFather ${dogiZeilen})`); /* ---- FRISCHE ZUGAENGE FUER DIE GEGENPROBEN --------------------- Die aus Abschnitt 1 sind hier nicht mehr zu gebrauchen: Die Abschnitte davor SPERREN und LOESCHEN absichtlich Leute, und eine tote Sitzung antwortet mit 401. Das sieht aus wie „darf nicht" und heisst „gibt es nicht mehr" -- ein 401 als Gegenprobe fuer ein 404 waere ein Haken ohne Gegenstand. Beim ersten Lauf ist genau das passiert, viermal. Angelegt wird direkt in der Datenbank, wie oben: ueber die Route ginge es auch, aber dann haengt die Gegenprobe an einem Recht, das sie gar nicht pruefen will. */ const d3 = new DatabaseSync(process.env.WORKSPACE_DB); const frisch = (name, rolle, code) => { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d3.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n," + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", ks).update(code).digest("hex"), new Date().toISOString()); }; frisch("Kessi2", "linke", "CODE-LINK-0002"); frisch("Miss2", "modi", "CODE-MODI-0009"); d3.close(); const kLinke2 = await anmelden("linke", "CODE-LINK-0002"); const kModi2 = await anmelden("modi", "CODE-MODI-0009"); /* GEGENPROBE 1: die linke Hand. Sie steht in derselben Menge wie die rechte (`WIE_RECHTE_HAND`), und genau deshalb wird sie hier einzeln gefragt: Haette jemand `istHand` statt der Rolle benutzt, bekaeme sie das Protokoll mit -- unbemerkt. */ const linkeProt = await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kLinke2); ok(linkeProt.code === 404, `Gegenprobe: die linke Hand sieht es nicht (${linkeProt.code}) — ` + `„keine Bewerbungen, kein vertraulicher Meldeweg" gilt unveraendert`); /* GEGENPROBE 2: ein Modi. Wer hier durchkaeme, saehe jede Anmeldung mit IP-Adresse. */ const modiProt = await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kModi2); ok(modiProt.code === 404, `Gegenprobe: ein Modi sieht es nicht (${modiProt.code})`); /* DASS DIE FRISCHEN ZUGAENGE WIRKLICH LEBEN, steht in der Bedingung der naechsten Zeilen: `darf_zugaenge_fuehren === false` bekommt man nur aus einer echten Antwort. Bei einer toten Sitzung stuende dort `undefined`, und die Zeile waere rot -- so wie beim ersten Lauf. */ /* ---- UND DIE AUSKUNFT, AUS DER DIE SEITE IHRE STUFE BAUT ------- Sie ist der eigentliche Fund vom 25.09.2026: Der Server war grosszuegiger als die Oberflaeche, und niemand hat es gemerkt, weil keine Pruefung die Oberflaeche gefragt hat. */ const ichDogi = json(await roh("/workspace/api/ich", kDogi)); const ichHand = json(await roh("/workspace/api/ich", kHand)); const ichLinke = json(await roh("/workspace/api/ich", kLinke2)); ok(ichDogi.darf_zugaenge_fuehren === true, `DogFather bekommt die volle Seite gesagt (${ichDogi.darf_zugaenge_fuehren})`); ok(ichHand.darf_zugaenge_fuehren === true, `und die rechte Hand genauso (${ichHand.darf_zugaenge_fuehren}) — ` + `daran haengen Liste, Rollenkarten und Protokoll`); ok(ichLinke.darf_zugaenge_fuehren === false, `Gegenprobe: die linke Hand nicht (${ichLinke.darf_zugaenge_fuehren})`); /* DIE AUSKUNFT MUSS ZUR TUER PASSEN. Zwei Werte, die dasselbe sagen sollen, laufen sonst auseinander -- und dann zeigt die Seite einen Kasten, den der Server mit 404 beantwortet. Genau diese Sorte Widerspruch hat in dieser Datei am 22. und 24.09. schon zweimal Rechte verschluckt, nur in die andere Richtung. */ ok(ichHand.darf_zugaenge_fuehren === (handProt.code === 200), "die Auskunft an die Oberflaeche und die Tuer am Server sagen dasselbe"); ok(ichLinke.darf_zugaenge_fuehren === (linkeProt.code === 200), " und bei der linken Hand ebenfalls"); } try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(""); console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); process.exit(fehler ? 1 : 0);