Die Community-Rolle laesst sich endlich anlegen

Filipe: "und wieso kann ich immer noch keine community rolle
erstellen?"

Nachgemessen statt geraten: Die Auskunft schickte "gast/Community" als
waehlbare Rolle mit, die Oberflaeche baute den Knopf daraus -- und das
Anlegen antwortete 400 "Unbekannte Rolle." Genau so reproduziert.

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 -- und
hier stand es vier Zeilen ueber der Stelle, an der es passiert ist.

Gefragt wird jetzt `darfAnlegen` -- dieselbe Auskunft, aus der die
Oberflaeche ihre Knoepfe baut. Damit koennen die beiden nicht mehr
auseinanderlaufen.

ZWEI NEBENBEFUNDE, beide groesser als der gemeldete Fehler:

1. 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. Es liess sich
   also ein zweiter DogFather-Zugang anlegen.

2. DIESELBE ZEILE STAND EIN ZWEITES MAL, beim Rollenwechsel. Auch eine
   bestehende Person liess sich nicht zu "Community" machen. Dort stand
   die richtige Pruefung (`darfAnlegen`) schon direkt darunter -- die
   falsche davor kam ihr nur zuvor. Und sie antwortete gespraechiger
   (403 "Diese Rolle vergibst du nicht." gegen 400 "Unbekannte
   Rolle."); wer durchprobiert, haette daran ablesen koennen, welche
   Rollen es gibt. Jetzt beide Faelle wortgleich.

   Gefunden hat das nicht das Lesen, sondern die neue Pruefung: Ihre
   Suche nach der alten Zeile reichte in die Nachbarroute hinein.

NEU: pruef-rollen-anlegen (11 Pruefungen)
Sie schreibt keine Liste ab, sondern holt sich die Rollen aus
`darfAnlegen` -- derselben Quelle wie Route und Oberflaeche -- und legt
JEDE davon wirklich an. Eine eigene Liste waere eine dritte gewesen und
damit dieselbe Falle noch einmal.

Dazu die Gegenproben: eine erfundene Rolle, eine leere, und ein zweiter
DogFather -- alle drei abgelehnt.

pruef-rollen 315, pruef-personen-formular 36, pruef-personen-liste,
pruef-rechtetafel 19: gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 12:24:11 +02:00
co-authored by Claude Opus 5
parent 79e675b846
commit 4bd19a4096
2 changed files with 260 additions and 5 deletions
+215
View File
@@ -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);
+45 -5
View File
@@ -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." });