/* ===================================================================== EIN MANAGER LEGT EINEN CREATOR AN — und sonst nichts. Wunsch Filipe (07.09.2026): "ich will auch dass manager ab jetzt creator und auch wirklich nur creator hinzufuegen koennen." --------------------------------------------------------------------- WARUM DIESE DATEI MEHR PRUEFT, ALS DER WUNSCH VERLANGT Der neue Weg ist der erste, der einem Manager erlaubt, ein KONTO anzulegen. Ein Konto ist ein Zugang; ein Fehler hier ist kein Schoenheitsfehler, sondern ein zweiter Schluessel zum Haus. Deshalb wird nicht nur geprueft, dass es geht, sondern vor allem, was NICHT geht. Zu jeder erlaubten Handlung steht die verbotene daneben: - Ein Manager legt einen Creator an -> 201 - ... und der Creator gehoert danach IHM -> Betreuung - Ein Manager kann die Rolle NICHT bestimmen -> immer creator - Ein Manager kommt NICHT in die Personenverwaltung -> 404 - Ein Scout kann diesen Weg nicht benutzen -> 404 - Ein Creator kann diesen Weg nicht benutzen -> 404 - Ein Manager teilt keinen FREMDEN Scout zu -> 403 - Ein Manager teilt seinen EIGENEN Scout zu -> 201 Der vorletzte Punkt ist der, auf den es ankommt. Er ist der einzige, bei dem eine Zahl im Aufruf ueber eine Zuordnung entscheidet -- und genau so ist am 02.09.2026 schon einmal eine Schranke umgangen worden ("es reichte, eine Zahl in die Adresse zu schreiben"). Aufruf: node server/pruef-creator-anlegen.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import { DatabaseSync } from "node:sqlite"; import { scryptSync, randomBytes } from "node:crypto"; import { notbremse } from "./helfer-notbremse.mjs"; notbremse(240_000, "die Creator-Anlegen-Pruefung"); const HIER = dirname(fileURLToPath(import.meta.url)); const { portMussFreiSein } = await import("./helfer-port.mjs"); const PORT = 4347; await portMussFreiSein(PORT, "die Creator-Anlegen-Pruefung"); const BASIS = `http://127.0.0.1:${PORT}`; const ordner = mkdtempSync(join(tmpdir(), "ws-canlegen-")); const DB = join(ordner, "workspace.db"); let fehler = 0, gemacht = 0; const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; let kind = null; async function starten() { kind = spawn(process.execPath, [join(HIER, "index.js")], { env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT), SITE_ACCESS_SECRET: "lokaler-test", SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" }, stdio: ["ignore", "pipe", "pipe"] }); let aus = ""; kind.stdout.on("data", (d) => { aus += d; }); kind.stderr.on("data", (d) => { aus += d; }); for (let i = 0; i < 80; i++) { await new Promise((r) => setTimeout(r, 250)); if (kind.exitCode !== null) break; try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); if (a.status < 500) return; } catch { /* noch nicht oben */ } } throw new Error("Server startete nicht:\n" + aus.slice(-1500)); } await starten(); kind.kill(); await new Promise((r) => setTimeout(r, 600)); const jetzt = new Date().toISOString(); function anlegen(d, name, rolle, code) { const salz = randomBytes(16).toString("hex"); const hash = scryptSync(code, salz, 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 (?,?,?,?,?,1,?)") .run(name, rolle, hash, salz, 32768, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } /* Zwei Manager mit je einem eigenen Scout -- der zweite ist der ganze Grund dieser Pruefung: An IHM zeigt sich, ob "nur die eigenen" wirklich gilt. Mit nur einem Manager waere jede Zuteilung zufaellig richtig. */ let idDogi, idMax, idMia, idScoutMax, idScoutMia, idCreator; { const d = new DatabaseSync(DB); idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001"); idMax = anlegen(d, "Max", "manager", "CODE-MAX-00001"); idMia = anlegen(d, "Mia", "manager", "CODE-MIA-00001"); idScoutMax = anlegen(d, "SamVonMax", "scout", "CODE-SAM-00001"); idScoutMia = anlegen(d, "TimVonMia", "scout", "CODE-TIM-00001"); idCreator = anlegen(d, "Luna", "creator", "CODE-LUNA-0001"); d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)") .run(idScoutMax, idMax, jetzt); d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)") .run(idScoutMia, idMia, jetzt); d.close(); } await starten(); const anmelden = async (rolle, code) => { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle, code }) }); const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); return keks; }; const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); const keksMax = await anmelden("manager", "CODE-MAX-00001"); const keksMia = await anmelden("manager", "CODE-MIA-00001"); const keksScout = await anmelden("scout", "CODE-SAM-00001"); const keksLuna = await anmelden("creator", "CODE-LUNA-0001"); const ruf = async (art, weg, keks, koerper) => { const a = await fetch(BASIS + weg, { method: art, headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS }, body: koerper ? JSON.stringify(koerper) : undefined }); let daten = null; try { daten = await a.json(); } catch { /* leer ist erlaubt */ } return { status: a.status, daten }; }; const WEG = "/workspace/api/creator-anlegen"; /* ======================================================================= 1. Der Alltagsfall ======================================================================= */ console.log("=== Ein Manager legt einen Creator an ==="); let neuerId = null; { const { status, daten } = await ruf("POST", WEG, keksMax, { name: "Nova" }); ok(status === 201, `Max legt "Nova" an (HTTP ${status})`); ok(daten?.rolle === "creator", `sie ist ein Creator (${daten?.rolle})`); ok(typeof daten?.code === "string" && daten.code.length >= 8, `der Zugangscode kommt genau einmal mit (${daten?.code ? daten.code.length + " Zeichen" : "fehlt"})`); neuerId = daten?.id ?? null; const d = new DatabaseSync(DB, { readOnly: true }); const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(neuerId); d.close(); ok(b?.betreuer_id === idMax, `und gehoert sofort Max (Betreuung: ${b?.betreuer_id ?? "keine"})`); } { /* DER PUNKT DAHINTER: Ein Creator ohne Betreuung ist fuer alle ausser DogFather unsichtbar. Ohne die Zuteilung haette Max ihn angelegt und danach nicht mehr gesehen -- ein Fehler, den man erst beim Suchen bemerkt. */ const { daten } = await ruf("GET", "/workspace/api/personen", keksMax); const namen = (daten?.personen || []).map((p) => p.name); ok(namen.includes("Nova"), `Max sieht sie danach auch (${namen.join(", ")})`); } { const { daten } = await ruf("GET", "/workspace/api/personen", keksMia); const namen = (daten?.personen || []).map((p) => p.name); ok(!namen.includes("Nova"), `die ANDERE Managerin sieht sie NICHT (${namen.join(", ")})`); } /* ======================================================================= 2. Was nicht geht ======================================================================= */ console.log("\n=== Was ein Manager NICHT kann ==="); { /* Die Rolle steht nicht im Aufruf -- also kann sie auch niemand setzen. Geprueft wird trotzdem, was passiert, wenn es jemand versucht: Das Feld muss WIRKUNGSLOS sein, nicht bloss abgelehnt. */ const { status, daten } = await ruf("POST", WEG, keksMax, { name: "Versuch1", rolle: "admin" }); ok(status === 201, `ein mitgeschicktes Feld "rolle" bricht nichts ab (HTTP ${status})`); ok(daten?.rolle === "creator", `es bleibt wirkungslos -- angelegt wurde ein ${daten?.rolle}`); } { const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksMax); ok(status === 404, `die Personenverwaltung bleibt fuer Max zu (HTTP ${status})`); } { const { status } = await ruf("POST", "/workspace/api/verwaltung/personen", keksMax, { name: "Schleichweg", rolle: "creator" }); ok(status === 404, `auch schreibend (HTTP ${status})`); } { const { status } = await ruf("POST", `/workspace/api/verwaltung/personen/${idCreator}/code`, keksMax); ok(status === 404, `einen fremden Code neu setzen geht nicht (HTTP ${status})`); } { const { status } = await ruf("POST", WEG, keksScout, { name: "VonScout" }); ok(status === 404, `ein Scout kann diesen Weg nicht benutzen (HTTP ${status})`); } { const { status } = await ruf("POST", WEG, keksLuna, { name: "VonCreator" }); ok(status === 404, `ein Creator auch nicht (HTTP ${status})`); } { const { status } = await ruf("POST", WEG, keksMax, { name: "A" }); ok(status === 400, `ein zu kurzer Name wird abgelehnt (HTTP ${status})`); } { const { status } = await ruf("POST", WEG, keksMax, { name: "Nova" }); ok(status === 409, `ein doppelter Name wird abgelehnt (HTTP ${status})`); } /* ======================================================================= 3. Die Zuteilung -- der eigentliche Prüfstein ======================================================================= */ console.log("\n=== Zugeteilt wird nur der EIGENE Scout ==="); { const { status, daten } = await ruf("POST", WEG, keksMax, { name: "MitEigenemScout", betreuer_id: idScoutMax }); ok(status === 201, `Max gibt seinen eigenen Scout mit (HTTP ${status})`); const d = new DatabaseSync(DB, { readOnly: true }); const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(daten?.id); d.close(); ok(b?.betreuer_id === idScoutMax, `und der steht auch da (${b?.betreuer_id})`); } { /* DER FALL, AUF DEN ES ANKOMMT. Eine Zahl im Aufruf darf keine Zuordnung erzeugen, die es nicht geben soll. */ const { status } = await ruf("POST", WEG, keksMax, { name: "MitFremdemScout", betreuer_id: idScoutMia }); ok(status === 403, `Mias Scout kann Max NICHT zuteilen (HTTP ${status})`); const d = new DatabaseSync(DB, { readOnly: true }); const z = d.prepare("SELECT 1 FROM personen WHERE name = 'MitFremdemScout'").get(); d.close(); ok(!z, "und der Creator wurde dabei GAR NICHT ERST angelegt"); } { /* Stillschweigend auf sich selbst zurueckzufallen waere schlimmer als die Absage: Der Manager glaubte dann, er haette zugeteilt. */ const { status } = await ruf("POST", WEG, keksMax, { name: "MitUnfug", betreuer_id: 99999 }); ok(status === 403, `eine erfundene Nummer wird abgelehnt statt ignoriert (HTTP ${status})`); } { const { daten } = await ruf("GET", WEG + "/scouts", keksMax); const namen = (daten?.scouts || []).map((s) => s.name); ok(namen.length === 1 && namen[0] === "SamVonMax", `die angebotene Liste enthaelt genau seinen Scout (${namen.join(", ") || "leer"})`); } { /* GEGENPROBE zur Liste: Sie muss fuer die andere Managerin ANDERS aussehen. Waere sie fuer alle gleich, pruefte die Zeile darueber nur, dass ueberhaupt etwas zurueckkommt. */ const { daten } = await ruf("GET", WEG + "/scouts", keksMia); const namen = (daten?.scouts || []).map((s) => s.name); ok(namen.length === 1 && namen[0] === "TimVonMia", `und fuer Mia eine andere (${namen.join(", ") || "leer"})`); } { const { status } = await ruf("GET", WEG + "/scouts", keksScout); ok(status === 404, `ein Scout bekommt die Liste nicht (HTTP ${status})`); } /* ======================================================================= 4. DogFather kann weiterhin alles ======================================================================= */ console.log("\n=== DogFather bleibt unangetastet ==="); { const { status, daten } = await ruf("POST", WEG, keksDogi, { name: "VonDogi", betreuer_id: idScoutMia }); ok(status === 201, `er darf jeden Scout zuteilen, auch Mias (HTTP ${status})`); ok(daten?.betreuer_id === idScoutMia, `und es wirkt (${daten?.betreuer_id})`); } { const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi); ok(status === 200, `die Personenverwaltung steht ihm weiter offen (HTTP ${status})`); } /* ======================================================================= 5. Der Knopf auf der Seite ======================================================================= */ console.log("\n=== Der Knopf auf dem Dashboard ==="); { const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); const sichtbarFuer = async (rolle, code) => { const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } }); const seite = await ctx.newPage(); await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); await seite.click(`.rolle[data-rolle="${rolle}"]`); await seite.fill("#code", code); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 25000 }); await seite.goto(BASIS + "/workspace/uebersicht.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(1400); const da = await seite.$eval("#creator-neu", (e) => !e.hidden).catch(() => null); await ctx.close(); return da; }; ok(await sichtbarFuer("manager", "CODE-MAX-00001") === true, "ein Manager sieht ihn"); ok(await sichtbarFuer("admin", "CODE-DOGI-0001") === true, "DogFather auch"); /* Ein Scout kommt auf dieselbe Seite -- der Knopf muss dort fehlen. Das ist keine Sicherung (die steht im Server), sondern die Zusage, dass niemand einen Knopf sieht, der nur eine Absage bringt. */ ok(await sichtbarFuer("scout", "CODE-SAM-00001") === false, "ein Scout nicht"); await browser.close(); } kind.kill(); await new Promise((r) => setTimeout(r, 400)); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } console.log(`\n${gemacht} Pruefungen`); console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); process.exit(fehler ? 1 : 0);