diff --git a/server/pruef-rollen-anlegen.mjs b/server/pruef-rollen-anlegen.mjs new file mode 100644 index 00000000..5ccb8082 --- /dev/null +++ b/server/pruef-rollen-anlegen.mjs @@ -0,0 +1,215 @@ +/* ===================================================================== + WAS DIE OBERFLÄCHE ANBIETET, MUSS DER SERVER AUCH ANNEHMEN + (17.09.2026) + + Filipe: "und wieso kann ich immer noch keine community rolle + erstellen?" + + Nachgemessen: Die Auskunft `/api/verwaltung/personen` schickte + "gast/Community" als waehlbare Rolle mit, die Oberflaeche baute den + Knopf daraus -- und das Anlegen antwortete "Unbekannte Rolle." + + --------------------------------------------------------------------- + DIE URSACHE: EINE LISTE, ZWEI FRAGEN + + Die Pruefung las `ROLLEN`, und `ROLLEN` ist `ROLLEN_REIHE` -- die + SORTIERREIHENFOLGE. Dort fehlt "gast" mit Absicht; der Kommentar in + workspace.js sagt es woertlich ("steht mit Absicht NICHT in der + Liste, sie faellt ans Ende"). + + `ROLLEN_REIHE` beantwortet "in welcher Reihenfolge", nicht "welche + gibt es". Genau davor wird in diesem Haus an zehn Stellen gewarnt. + + NEBENBEFUND, und er war die groessere Luecke: Die alte Pruefung + liess "admin" durch -- `ROLLEN_REIHE` enthaelt sie. Filipes Regel vom + 11.09. ("dogfather soll man nicht auswaehlen koennen, das ist die + einzige die man nicht auswaehlen kann") stand nur in `ANLEGBAR`, und + die wurde hier nicht gefragt. + + --------------------------------------------------------------------- + DIE PRUEFUNG LEITET AB STATT ABZUSCHREIBEN + + Sie holt sich die angebotenen Rollen aus derselben Auskunft, aus der + die Oberflaeche ihre Knoepfe baut, und legt JEDE davon wirklich an. + Eine eigene Liste hier waere eine DRITTE -- und damit dieselbe Falle + noch einmal. + + Aufrufen mit: node server/pruef-rollen-anlegen.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4497, "pruef-rollen-anlegen"); +const ordner = mkdtempSync(join(tmpdir(), "ws-ra-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +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 }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-rollen-anlegen"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +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 sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); +const salt = randomBytes(16).toString("hex"); +const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, + createHmac("sha256", sch).update("CODE-DOGI-0001").digest("hex"), jetzt); +d.close(); + +const an = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), +}); +const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); +const kopf = { "Content-Type": "application/json", cookie: keks, origin: BASIS }; + +async function anlegen(name, rolle) { + const a = await fetch(BASIS + "/workspace/api/verwaltung/personen", { + method: "POST", headers: kopf, body: JSON.stringify({ name, rolle }), + }); + return { status: a.status, daten: await a.json().catch(() => null) }; +} + +/* ======================================================================= + 1. WAS DIE OBERFLAECHE ANBIETET + ======================================================================= */ +melde(""); +melde("=== 1. Das Angebot ==="); +const v = await fetch(BASIS + "/workspace/api/verwaltung/personen", { headers: { cookie: keks } }); +const auskunft = await v.json().catch(() => null); +const angeboten = [ + ...(auskunft?.rollen || []).map((r) => (typeof r === "string" ? r : r.wert)), + ...(auskunft?.zusatzrollen || []).map((r) => r.wert), +].filter(Boolean); +{ + /* DREI, NICHT MEHR -- und das ist richtig so: Die Grundrollen + (Creator, Scout, Manager, Spicy) stehen als Knoepfe in der + HTML-Seite, nur die ZUSATZROLLEN kommen aus der Auskunft. Beim + ersten Lauf habe ich fuenf erwartet und die Pruefung rot gemacht, + nicht den Code. */ + ok(v.status === 200 && angeboten.length >= 3, + `die Auskunft bietet ${angeboten.length} Zusatzrollen an (${angeboten.join(", ")})`); + ok(angeboten.includes("gast"), "darunter die Community-Rolle"); + + /* UND DIE VOLLE MENGE kommt aus derselben Quelle wie die Route: + `darfAnlegen`. Damit deckt Abschnitt 2 wirklich alles ab, was + jemand anlegen koennen soll -- nicht nur die Zusatzrollen. */ + const { darfAnlegen } = await import("./workspace.js"); + const alleErlaubt = darfAnlegen({ rolle: "admin", haus: "workspace" }); + ok(alleErlaubt.length >= 6, + `darfAnlegen nennt ${alleErlaubt.length} Rollen (${alleErlaubt.join(", ")})`); + for (const r of alleErlaubt) if (!angeboten.includes(r)) angeboten.push(r); +} + +/* ======================================================================= + 2. UND DER SERVER NIMMT JEDE DAVON AN + ======================================================================= */ +melde(""); +melde("=== 2. Die Annahme ==="); +{ + /* DAS IST DER EIGENTLICHE PUNKT. Ein Knopf, den der Server ablehnt, + ist schlimmer als kein Knopf: Man tippt einen Namen, drueckt, und + bekommt eine Absage fuer etwas, das angeboten wurde. */ + const abgelehnt = []; + let n = 0; + for (const rolle of angeboten) { + const a = await anlegen(`Probe ${++n}`, rolle); + if (a.status !== 201) abgelehnt.push(`${rolle} (${a.status} ${a.daten?.fehler || ""})`); + } + ok(abgelehnt.length === 0, + abgelehnt.length ? `abgelehnt: ${abgelehnt.join(" · ")}` + : `alle ${angeboten.length} angebotenen Rollen lassen sich anlegen`); +} + +/* ======================================================================= + 3. UND WAS NICHT ANGEBOTEN WIRD, GEHT AUCH NICHT + ======================================================================= */ +melde(""); +melde("=== 3. Die Gegenprobe ==="); +{ + /* OHNE DIESEN ABSCHNITT hiesse "alles geht" nur, dass gar nicht + geprueft wird -- man koennte die Pruefung bestehen, indem man sie + ganz entfernt. */ + const x = await anlegen("Erfunden", "oberchef"); + ok(x.status === 400, `eine erfundene Rolle wird abgelehnt (${x.status})`); + + /* UND DER NEBENBEFUND VON HEUTE: "admin" stand in ROLLEN_REIHE und + rutschte damit durch die alte Pruefung. Filipes Regel vom 11.09. + ("dogfather soll man nicht auswaehlen koennen") stand nur in + ANLEGBAR -- und die wurde nicht gefragt. */ + const a = await anlegen("Zweiter DogFather", "admin"); + ok(a.status === 400, + `ein zweiter DogFather-Zugang laesst sich NICHT anlegen (${a.status})`); + ok(!angeboten.includes("admin"), "und die Oberflaeche bietet ihn auch nicht an"); + + /* Ein leerer Wert ist keine Rolle. */ + const l = await anlegen("Ohne Rolle", ""); + ok(l.status === 400, `eine leere Rolle ebenfalls nicht (${l.status})`); +} + +/* ======================================================================= + 4. DIE BEIDEN LISTEN STEHEN NICHT MEHR NEBENEINANDER + ======================================================================= */ +melde(""); +melde("=== 4. Eine Quelle, nicht zwei ==="); +{ + const { readFileSync } = await import("node:fs"); + const quelle = readFileSync(new URL("./workspace-personen.js", import.meta.url), "utf8"); + /* KOMMENTARE RAUS, bevor gesucht wird. Beim ersten Lauf ist die + Pruefung an ihrem eigenen Erfolg gescheitert: Der Kommentar, der + den behobenen Fehler BESCHREIBT, enthaelt den alten Ausdruck -- + und die Suche fand ihn. Eine Pruefung, die Beschreibungen fuer + Code haelt, misst die Dokumentation. */ + const ohneKommentar = quelle + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/\/\/[^\r\n]*/g, " "); + const route = ohneKommentar.slice( + ohneKommentar.indexOf('personenRouter.post("/workspace/api/verwaltung/personen"')); + const stelle = route.slice(0, 3000); + ok(/darfAnlegen\(req\.person\)\.includes\(rolle\)/.test(stelle), + "die Route fragt darfAnlegen"); + /* UND NICHT MEHR DIE SORTIERREIHENFOLGE. Ein Kommentar daneben + wuerde es nicht verhindern -- das steht so in den Hausregeln. */ + ok(!/!ROLLEN\.includes\(rolle\)/.test(stelle), + "und nicht mehr die Sortierreihenfolge ROLLEN_REIHE"); + + /* ROLLEN_REIHE enthaelt "gast" weiterhin NICHT -- das ist richtig + so, sie ist eine Sortierung. Steht sie eines Tages doch drin, war + jemand an der falschen Stelle. */ + const { ROLLEN_REIHE } = await import("./workspace.js"); + ok(!ROLLEN_REIHE.includes("gast"), + `ROLLEN_REIHE bleibt eine Sortierung ohne "gast" (${ROLLEN_REIHE.join(", ")})`); +} + +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 449655b0..da6fa989 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -730,9 +730,32 @@ personenRouter.post("/workspace/api/verwaltung/personen", gleicheHerkunft, (req, 10.09.2026 sind es zwei, und die zweite waere hier sonst still durchgerutscht -- ein Manager haette eine rechte Hand anlegen koennen, ohne dass irgendwo etwas rot wird. */ - const unbekannt = !ROLLEN.includes(rolle) - || (TEAM_DOGI_ROLLEN.has(rolle) && !istDogFather(req.person)); - if (unbekannt) return res.status(400).json({ fehler: "Unbekannte Rolle." }); + /* GEFRAGT WIRD `darfAnlegen`, NICHT `ROLLEN` (17.09.2026). + + HIER STAND `!ROLLEN.includes(rolle)`, und `ROLLEN` ist + `ROLLEN_REIHE` -- die SORTIERREIHENFOLGE. Dort fehlt "gast" mit + Absicht (der Kommentar in workspace.js sagt es woertlich: "steht + mit Absicht NICHT in der Liste, sie faellt ans Ende"). + + Folge: Die Oberflaeche bot "Community" an -- sie holt ihre + Knoepfe aus `darfAnlegen` --, und der Server antwortete beim + Anlegen "Unbekannte Rolle." Filipe, am 17.09.: "und wieso kann + ich immer noch keine community rolle erstellen?" + + Das ist die Falle, vor der in diesem Haus an zehn Stellen + gewarnt wird: EINE Liste, zwei Fragen. `ROLLEN_REIHE` beantwortet + "in welcher Reihenfolge", nicht "welche gibt es". + + `darfAnlegen` beantwortet genau die Frage, die hier zaehlt -- + und es ist dieselbe Auskunft, aus der die Oberflaeche ihre + Knoepfe baut. Damit koennen die beiden nicht mehr auseinander- + laufen. Die Team-Dogi-Regel steckt schon darin (ANLEGBAR gibt + "hand" und "modi" nur DogFather), ebenso die Regel, dass sich + kein zweiter DogFather anlegen laesst -- die das alte `ROLLEN` + uebrigens NICHT abgedeckt hat. */ + if (!darfAnlegen(req.person).includes(rolle)) { + return res.status(400).json({ fehler: "Unbekannte Rolle." }); + } /* ERSTER VORBEHALT: Eine Leitung anlegen darf nur DogFather. Duerfte ein Manager das, koennte er sich einen zweiten Zugang mit @@ -859,9 +882,26 @@ personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkun /* Dieselbe Auskunft wie beim Anlegen: Wer eine Rolle nicht vergeben darf, darf sie auch nicht zuweisen. Sonst waere das Zuweisen der bequemere Weg an der Regel vorbei. */ - if (!ROLLEN.includes(rolle)) return res.status(400).json({ fehler: "Unbekannte Rolle." }); + /* EINE FRAGE, EINE ANTWORT (17.09.2026). + + Hier standen ZWEI Pruefungen untereinander. Die erste las + `ROLLEN` -- die Sortierreihenfolge, in der "gast" mit Absicht + fehlt -- und lehnte deshalb ab, BEVOR die zweite, richtige + ueberhaupt drankam. Eine bestehende Person liess sich damit + nicht zu "Community" machen, aus demselben Grund wie beim + Anlegen. + + Gefunden hat das nicht das Lesen, sondern pruef-rollen-anlegen: + Die Suche nach der alten Zeile reichte noch in diese Route + hinein, und dort stand sie ein zweites Mal. + + DIE ZWEITE ANTWORT WAR AUSSERDEM GESPRAECHIGER als die erste + ("Diese Rolle vergibst du nicht." mit 403 gegen "Unbekannte + Rolle." mit 400). Wer durchprobiert, haette daran ablesen + koennen, welche Rollen es gibt. Jetzt beide Faelle wortgleich + -- wie beim Anlegen. */ if (!darfAnlegen(req.person).includes(rolle)) { - return res.status(403).json({ fehler: "Diese Rolle vergibst du nicht." }); + return res.status(400).json({ fehler: "Unbekannte Rolle." }); } if (person.rolle === rolle) { return res.status(400).json({ fehler: "Diese Rolle hat sie schon." });