diff --git a/server/pruef-haus-trennung.mjs b/server/pruef-haus-trennung.mjs index 14bffd05..9fb69f59 100644 --- a/server/pruef-haus-trennung.mjs +++ b/server/pruef-haus-trennung.mjs @@ -681,12 +681,50 @@ console.log("=== Alle Kategorien ==="); ok((beiHand?.zusatzrollen || []).length > 0, "samt der Ueberschriften"); - /* SIE LIEST NUR. Jeder schreibende Weg bleibt zu -- gemessen, nicht - angenommen, denn genau hier waere aus "sehen" versehentlich - "verwalten" geworden. */ - const codeVersuch = await hole(`/workspace/api/verwaltung/personen/${idModi}/code`, + /* SIE DARF AUCH CODES NEU SETZEN (22.09.2026). + ------------------------------------------------------------------ + Hier stand `=== 404` ("sie liest nur"). Filipe hat das umgedreht: + "die codes genau so sieht wie dogfather, damit sie das auch machen + kann wenn er live ist." + + Ein Code wird genau einmal angezeigt. Geht er verloren, ist der + Zugang ohne diesen Weg tot -- und dann muesste doch wieder + DogFather ran, also genau das, was nicht sein soll. */ + /* EIN FRISCHER MODI, KEIN GEBRAUCHTER (berichtigt 22.09.2026). + Hier stand `idModi` -- und der ist weiter oben laengst zur rechten + Hand befoerdert worden. Die Pruefung mass damit "darf sie an einer + rechten Hand etwas aendern" und nicht "darf sie an einem Modi". + Dieselbe Falle wie am 21.09. bei Ayla. */ + const frischFuerCode = await hole("/workspace/api/verwaltung/personen", + { host: CREW, methode: "POST", keks: dogi.keks, + rumpf: JSON.stringify({ name: "Codeprobe", rolle: "modi" }) }); + const idFuerCode = frischFuerCode.daten?.person?.id ?? frischFuerCode.daten?.id; + ok(!!idFuerCode, `ein frischer Modi zum Messen (${idFuerCode || "keiner"})`); + const codeVersuch = await hole(`/workspace/api/verwaltung/personen/${idFuerCode}/code`, { host: CREW, methode: "POST", keks: hand.keks, rumpf: "{}" }); - ok(codeVersuch.status === 404, `sie kann keinen Code neu setzen (${codeVersuch.status})`); + ok(codeVersuch.status === 200, + `sie kann einen Code neu setzen (${codeVersuch.status})`); + ok(typeof codeVersuch.daten?.code === "string" && codeVersuch.daten.code.length > 6, + `und sieht ihn dabei (${codeVersuch.daten?.code ? "ja" : "NEIN"})`); + + /* DIE GRENZE DAZU: an einer LINKEN HAND aendert sie nichts. Ohne + diese Schranke haette sie einen Zugang in der Hand, der fast so + viel darf wie sie selbst -- und aufgefallen waere es nicht, denn + die alte Schranke kannte nur "admin" und "manager". */ + { + const frisch = await hole("/workspace/api/verwaltung/personen", + { host: CREW, methode: "POST", keks: dogi.keks, + rumpf: JSON.stringify({ name: "Lino Zweit", rolle: "linke" }) }); + const idLinke = frisch.daten?.person?.id ?? frisch.daten?.id; + if (idLinke) { + const anLinke = await hole(`/workspace/api/verwaltung/personen/${idLinke}/code`, + { host: CREW, methode: "POST", keks: hand.keks, rumpf: "{}" }); + ok(anLinke.status === 403, + `an einer linken Hand aendert sie nichts (${anLinke.status})`); + } else { + ok(false, "die Gegenprobe konnte keine linke Hand anlegen -- nicht messbar"); + } + } /* ROLLEN WECHSELN DARF SIE SEIT DEM 20.09. -- aber nicht alles. Filipe: "ich will dass ich da auch die rollen der leute wechseln @@ -740,10 +778,24 @@ console.log("=== Alle Kategorien ==="); { host: CREW, methode: "PUT", keks: hand.keks, rumpf: JSON.stringify({ rolle: "hand" }) }); ok(zuHoch.status === 400, `eine zweite rechte Hand ernennt sie nicht (${zuHoch.status})`); + /* UND SIE LEGT PERSONEN AN (22.09.2026) -- Filipes ausdruecklicher + Wunsch. Hier stand `=== 404`. */ const anlegen = await hole("/workspace/api/verwaltung/personen", { host: CREW, methode: "POST", keks: hand.keks, rumpf: JSON.stringify({ name: "Nochwer", rolle: "modi" }) }); - ok(anlegen.status === 404, `und niemanden anlegen (${anlegen.status})`); + ok(anlegen.status === 201, `sie legt einen Modi an (${anlegen.status})`); + ok(typeof (anlegen.daten?.code) === "string", + `und bekommt den Code zu sehen (${anlegen.daten?.code ? "ja" : "NEIN"})`); + + /* ABER KEINE ZWEITE RECHTE HAND UND KEINE LINKE -- sonst waere + "sie darf anlegen" ein Weg, sich selbst hoehere Rechte zu bauen. */ + for (const rolle of ["hand", "linke", "admin"]) { + const zuHochAnlegen = await hole("/workspace/api/verwaltung/personen", + { host: CREW, methode: "POST", keks: hand.keks, + rumpf: JSON.stringify({ name: "Zu hoch " + rolle, rolle }) }); + ok(zuHochAnlegen.status >= 400, + `eine Rolle "${rolle}" legt sie nicht an (${zuHochAnlegen.status})`); + } /* GEGENPROBE: DogFather darf all das sehr wohl -- sonst hiessen die drei 404 oben auch dann gruen, wenn die Wege ueberhaupt kaputt diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs index e3153ad5..cd931682 100644 --- a/server/pruef-personen-formular.mjs +++ b/server/pruef-personen-formular.mjs @@ -169,10 +169,25 @@ const mass = await dogi.evaluate(() => { DIE ZEILE WURDE GEDREHT, NICHT GELOESCHT: Dass `admin` FEHLT, ist jetzt eine eigene Aussage -- sonst haette die Liste unten still eine Rolle mehr bekommen koennen, ohne dass es jemand merkt. */ -ok(mass.karten === 7, `alle sieben vergebbaren Rollen stehen zur Wahl (${mass.karten})`); -ok(JSON.stringify([...mass.kartenRollen].sort()) - === JSON.stringify(["creator", "gast", "hand", "manager", "modi", "scout", "spicy"]), - `und es sind die richtigen (${mass.kartenRollen.join(", ")})`); +/* ABGELEITET, NICHT GEZAEHLT (berichtigt 22.09.2026). + --------------------------------------------------------------------- + Hier stand `=== 7` und eine abgeschriebene Liste von sieben Namen. + Am 21.09. kam die Rolle "linke" dazu, und beide Zeilen wurden rot, + obwohl das Formular richtig war -- gemessen acht Karten, erwartet + sieben. Die Pruefung war einen Tag lang rot und hat dabei nichts + gefunden ausser sich selbst. + + Die eigentliche Aussage ist auch eine andere: Das Formular soll + GENAU DAS anbieten, was der Server auch annimmt. Zwei Listen an zwei + Stellen sind zwei Gelegenheiten, eine davon zu vergessen -- genau + daran ist es am 17.09. schon einmal gescheitert ("wieso kann ich + immer noch keine community rolle erstellen?"). */ +const w = await import("./workspace.js"); +const sollKarten = [...w.darfAnlegen({ rolle: "admin", haus: "agentur" })].sort(); +ok(mass.karten === sollKarten.length, + `alle ${sollKarten.length} vergebbaren Rollen stehen zur Wahl (${mass.karten})`); +ok(JSON.stringify([...mass.kartenRollen].sort()) === JSON.stringify(sollKarten), + `und es sind genau die, die der Server annimmt (${mass.kartenRollen.join(", ")})`); ok(!mass.kartenRollen.includes("admin"), "DogFather steht NICHT zur Wahl -- auch nicht fuer DogFather"); @@ -413,6 +428,164 @@ const ERWARTET = 17; console.log(""); ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); +/* =================================================================== + TEIL: DIE RECHTE HAND LEGT SELBST JEMANDEN AN (22.09.2026) + =================================================================== + + Filipe: "dan will ich dass die rechte hand auch neue personen + hinzufuegen kann ... und die codes genau so sieht wie dogfather, + damit sie das auch machen kann wenn er live ist." + + GEPRUEFT WIRD DER GANZE WEG, nicht die Rechtetabelle. Die sagt seit + heute "ja"; ob ein Mensch damit jemanden anlegen kann, ist eine + andere Frage. Beim ersten Anlauf war sie mit "nein" zu beantworten: + Die Seite entschied den Sendeweg an `ich.rolle === 'admin'`, und + fuer die rechte Hand gab es damit GAR KEINEN -- sie bekam "Fuer die + Rolle modi gibt es hier keinen Weg", einen Satz, der wie ein Fehler + im Formular klingt. + =================================================================== */ +console.log("\n— Die rechte Hand legt jemanden an —"); +{ + /* EIGENER BROWSER AUF DER CREW-ADRESSE. + ------------------------------------------------------------------ + 127.0.0.1 ist die AGENTURWAND -- dort gibt es die Kachel "rechte + Hand" gar nicht, und der erste Anlauf lief in eine + Zeitueberschreitung. Das war kein Befund, sondern die falsche Tuer. + + Chromium zwingt `crew.dogfather-universe.com` auf https (die + Adresse steht in seiner eingebauten HSTS-Liste), deshalb ein + Vorbau mit eigenem Zertifikat -- derselbe Weg wie in + pruef-handy-teamdogi und pruef-zuteilung. */ + const { execFileSync } = await import("node:child_process"); + const { createServer: httpsServer } = await import("node:https"); + const { request: httpAnfrage } = await import("node:http"); + const { readFileSync: lesen } = await import("node:fs"); + const HOST = "crew.dogfather-universe.com"; + const sk = join(ordner, "pf.key"), zt = join(ordner, "pf.crt"); + execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", sk, "-out", zt, "-days", "2", "-subj", `/CN=${HOST}`, + "-addext", `subjectAltName=DNS:${HOST}`], { stdio: "ignore" }); + const HP = PORT + 1; + const vorbau = httpsServer({ key: lesen(sk), cert: lesen(zt) }, (von, nach) => { + const w = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url, + method: von.method, headers: { ...von.headers, host: `${HOST}:${HP}` } }, + (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); }); + von.pipe(w); + }); + await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r)); + const CREW = `https://${HOST}:${HP}`; + const crewBrowser = await chromium.launch({ + args: [`--host-resolver-rules=MAP ${HOST} 127.0.0.1`, "--ignore-certificate-errors"], + }); + const kontext = await crewBrowser.newContext({ viewport: { width: 1440, height: 1000 } }); + const hand = await kontext.newPage(); + try { + await hand.goto(CREW + "/workspace/", { waitUntil: "domcontentloaded" }); + await hand.click('.rolle[data-rolle="hand"]'); + await hand.fill("#code", "CODE-HAND-0001"); + await hand.click("button[type=submit]"); + await hand.waitForURL("**/start.html", { timeout: 20000 }); + await hand.goto(CREW + "/workspace/personen.html", { waitUntil: "networkidle" }); + await hand.waitForTimeout(900); + await hand.click("#neu-oeffnen").catch(() => {}); + await hand.waitForTimeout(500); + + const bild = await hand.evaluate(async () => { + const a = await fetch("/workspace/api/ich").then((r) => r.json()).catch(() => ({})); + const f = document.getElementById("neu"); + const auf = document.getElementById("neu-oeffnen"); + return { + formDa: !!f && !f.hidden, + knopfDa: !!auf && !auf.hidden, + rollen: [...document.querySelectorAll("#neu .rollenwahl__knopf")] + .map((k) => k.dataset.rolle || k.getAttribute("data-rolle")).filter(Boolean), + woSonst: [...document.querySelectorAll(".rollenwahl__knopf")] + .map((k) => (k.closest("[id]") || {}).id + ":" + (k.dataset.rolle || "?")), + vomServer: a.darf_anlegen, + meineRolle: a.rolle, + }; + }); + console.log(" /api/ich sagt: rolle=" + bild.meineRolle + + " darf_anlegen=" + JSON.stringify(bild.vomServer) + + " Knopf=" + bild.knopfDa + " Form offen=" + bild.formDa); + console.log(" Rollenknoepfe: " + JSON.stringify(bild.woSonst)); + ok(bild.formDa, "sie bekommt das Formular ueberhaupt zu sehen"); + ok(bild.rollen.length > 0 && bild.rollen.every((r) => ["modi", "gast"].includes(r)), + `und genau die Rollen, die sie darf (${bild.rollen.join(", ") || "keine"})`); + + /* JETZT WIRKLICH ANLEGEN. Ein Formular, das dasteht und beim + Absenden scheitert, ist schlimmer als keines. */ + const name = "Neue Modi " + Date.now().toString().slice(-5); + await hand.evaluate((n) => { + const k = [...document.querySelectorAll(".rollenwahl__knopf")] + .find((x) => (x.dataset.rolle || "") === "modi"); + if (k) k.click(); + document.getElementById("f-name").value = n; + }, name); + await hand.waitForTimeout(250); + await hand.click("#neu button[type=submit]").catch(async () => { + await hand.evaluate(() => document.getElementById("neu").requestSubmit()); + }); + await hand.waitForTimeout(2200); + + /* DIE ABSCHNITTE DER LISTE SIND ZUGEKLAPPT -- `innerText` sieht + zugeklappten Inhalt nicht. Dieselbe Falle wie am 21.09. bei + pruef-nachfrage. Erst aufklappen, dann messen. */ + await hand.evaluate(() => { + for (const d of document.querySelectorAll("details")) d.open = true; + }); + await hand.waitForTimeout(400); + const danach = await hand.evaluate((n) => { + const kasten = document.querySelector(".code-kasten, #code-kasten"); + const text = document.body.innerText; + return { + fehler: (document.getElementById("neu-fehler") || {}).textContent || "", + codeDa: !!kasten && !kasten.hidden, + codeText: kasten ? (kasten.innerText || "").slice(0, 200) : "", + stehtInListe: text.includes(n), + listeDa: !!document.getElementById("liste"), + listeText: (document.getElementById("liste") || {}).innerText + ? document.getElementById("liste").innerText.replace(/\s+/g, " ").slice(-260) : "(leer)", + gesucht: n, + }; + }, name); + ok(!danach.fehler, `kein Fehler beim Absenden (${danach.fehler || "keiner"})`); + ok(danach.codeDa, "der Zugangscode wird ihr angezeigt"); + ok(/[A-Z0-9]{4}/.test(danach.codeText), + `und er sieht auch wie einer aus (${danach.codeText.replace(/\s+/g, " ").slice(0, 60)})`); + /* GEMESSEN WIRD DIE LISTE DES SERVERS, NICHT DER SICHTBARE TEXT + (berichtigt 22.09.2026). + + Hier stand `document.body.innerText.includes(name)`. Die + Abschnitte der Personenliste sind zugeklappt und zeigen nur die + Anfangsbuchstaben -- der volle Name steht dort also gar nicht, + obwohl die Person laengst angelegt ist. Die Zeile war rot, und + kaputt war nur sie selbst. + + Jetzt wird gefragt, was sie WIRKLICH bekommt: dieselbe Auskunft, + aus der die Liste gebaut wird. Das beweist beides auf einmal -- + dass sie anlegen kann UND dass sie das Ergebnis sieht. */ + const ausListe = await hand.evaluate(async (n) => { + const a = await fetch("/workspace/api/verwaltung/personen") + .then((r) => r.json()).catch(() => null); + return { + code: Array.isArray(a?.personen) ? 200 : 0, + dabei: (a?.personen || []).some((x) => x.name === n), + zahl: (a?.personen || []).length, + }; + }, name); + ok(ausListe.code === 200, + `sie bekommt die Personenliste (${ausListe.zahl} Menschen)`); + ok(ausListe.dabei, ausListe.dabei + ? "und die eben angelegte Person steht darin" + : `die eben angelegte Person fehlt in ihrer Liste (${name})`); + } finally { + await kontext.close(); + await crewBrowser.close(); + await new Promise((r) => vorbau.close(r)); + } +} + await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); diff --git a/server/workspace-personen.js b/server/workspace-personen.js index e5ed89f7..f65574f3 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -128,6 +128,36 @@ function nurAdmin(req, res, next) { WELCHE Rolle sie vergeben darf, steht NICHT hier -- das entscheidet ANLEGBAR in der Route. Diese Zeile oeffnet nur die Tuer. */ + /* DIE RECHTE HAND LEGT PERSONEN AN UND SIEHT IHRE CODES (22.09.2026). + + Filipe: "damit sie das auch machen kann wenn er live ist." + + NUR DIE RECHTE, NICHT DIE LINKE. `istHand()` wuerde beide treffen; + fuer die linke Hand ist "legt niemanden an" eine ausdrueckliche + Entscheidung vom 21.09. Deshalb steht hier die Rolle und nicht der + Sammelbegriff -- der haette die Entscheidung stillschweigend + umgedreht. + + ZWEI WEGE, NICHT DIE GANZE VERWALTUNG: anlegen und einen Code neu + erzeugen. Sperren, Loeschen, Betreuung und Protokoll bleiben bei + DogFather. Und WELCHE Rolle sie anlegen darf, entscheidet nicht + diese Zeile, sondern ANLEGBAR in der Route -- diese Tuer oeffnet + nur den Weg. + + WARUM AUCH "Code neu erzeugen": Ein Code wird genau einmal + angezeigt. Geht er verloren, ist der Zugang ohne diesen Weg tot -- + und dann muesste doch wieder DogFather ran, also genau das, was + nicht sein soll. */ + const anlegeWeg = req.method === "POST" + && req.baseUrl + req.path === "/workspace/api/verwaltung/personen"; + const codeWeg = req.method === "POST" + && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/code$/ + .test(req.baseUrl + req.path); + if (person.rolle === "hand" && (anlegeWeg || codeWeg)) { + req.person = person; + return next(); + } + const rollenWeg = req.method === "PUT" && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/rolle$/ .test(req.baseUrl + req.path); @@ -169,6 +199,25 @@ function nurDogFatherBeiLeitung(req, res, next) { const id = Number(req.params.id); if (!Number.isInteger(id)) return next(); const ziel = db().prepare("SELECT rolle FROM personen WHERE id = ?").get(id); + + /* DIE RECHTE HAND NUR AN DENEN, DIE SIE AUCH ANLEGEN DUERFTE + (22.09.2026). + + Ohne diese Zeile koennte sie den Code einer LINKEN HAND neu + erzeugen -- und haette damit einen Zugang, der fast so viel darf + wie sie selbst, in der Hand. Das ist nicht gemeint und waere auch + nicht aufgefallen: Die Schranke darunter kennt nur "admin" und + "manager". + + Gefragt wird dieselbe Liste wie beim Anlegen. Eine zweite waere + die, die beim naechsten Umbau auseinanderlaeuft. */ + if (req.person?.rolle === "hand" && ziel + && !darfAnlegen(req.person).includes(ziel.rolle)) { + return res.status(403).json({ + fehler: "An dieser Rolle ändert die rechte Hand nichts.", + }); + } + if (ziel && (ziel.rolle === "admin" || ziel.rolle === "manager")) { return res.status(403).json({ fehler: "An DogFather und Managern ändert nur DogFather etwas.", diff --git a/server/workspace.js b/server/workspace.js index bad63857..274915ac 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -452,6 +452,24 @@ const ANLEGBAR = { spicy: ["spicy", "manager", "scout", "creator"], /* Ein Manager stellt Creator ein, die er dann auch betreut. */ manager: ["creator"], + /* DIE RECHTE HAND LEGT PERSONEN AN (22.09.2026). + + Filipe: "dan will ich dass die rechte hand auch neue personen + hinzufuegen kann. also neue erstellen kann und die codes genau so + sieht wie dogfather, damit sie das auch machen kann wenn er live + ist." + + DIESELBE LISTE, DIE SIE AUCH VERGEBEN DARF -- abgeleitet aus + ROLLEN_ZUM_AENDERN, nicht danebengeschrieben. Zwei Listen mit + denselben zwei Rollen waeren zwei Gelegenheiten, eine davon zu + aendern und die andere zu vergessen; dann duerfte sie eine Rolle + anlegen, aber nicht vergeben, und niemand wuesste warum. + + WAS DAMIT AUSDRUECKLICH NICHT GEHT: eine zweite rechte Hand, eine + linke Hand oder einen zweiten DogFather. Sie kann sich also auch + ueber den Umweg "neuen Zugang anlegen" keine hoeheren Rechte + verschaffen. */ + hand: ROLLEN_ZUM_AENDERN.hand, /* DIE LINKE HAND LEGT NIEMANDEN AN (21.09.2026) -- ausdruecklich im Auftrag. Sie stuende auch ohne diese Zeile auf [] (darfAnlegen nimmt `?? []`), aber dann saehe es aus wie vergessen. Eine diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 9c14d79f..9c9cc12b 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +