Rollen wechseln: auch Spicy Media -- und DogFather ist nicht mehr vergebbar

Filipe: "ich will dass die rolle spicy und dogfather, auch die rollen
wechseln koennen wenn die personen schon drin sind. von alle
kategorien, creator, scouts, manager spicy. dogfather soll man nicht
auswaehlen koennen. das ist die einzige die man nicht auswaehlen kann
bitte."

ZWEI AENDERUNGEN, BEIDE IN EINER LISTE (ANLEGBAR):
  admin: alles AUSSER der eigenen Rolle
  spicy: spicy, manager, scout, creator   (vorher ohne spicy)

Weil die Oberflaeche ihre Knoepfe aus derselben Auskunft baut
(`darf_anlegen` in /api/ich), verschwindet DogFather damit von selbst
aus JEDER Auswahl -- beim Anlegen wie beim Wechseln, bei Spicy Media
wie bei DogFather. Eine Liste, zwei Formulare, kein Nachziehen.

WAS DAS BEDEUTET, damit es niemand spaeter sucht: Es laesst sich kein
zweiter DogFather-Zugang mehr anlegen und niemand mehr zu einem
befoerdern. Der bestehende ist durch Sicherung 3 geschuetzt (nie den
letzten herabstufen) und kann nicht versehentlich verschwinden.
Zurueckdrehen laesst sich das nur in ANLEGBAR.

DIE TUER: /verwaltung haengt an nurAdmin, und dort steht jetzt eine
DRITTE enge Ausnahme -- PUT auf genau /personen/<Ziffern>/rolle, fuer
genau die Rollen aus darfRollenWechseln(). Gleiche Bauweise wie die
beiden davor (Liste fuer Spicy Media, Liste fuer die rechte Hand). Was
NICHT mitgeht: Codes, Sperren, Loeschen, Zuteilung, Protokoll.

EINE NEUE SICHERUNG, weil sich die Tuer geoeffnet hat: An einer
DogFather-Zeile aendert nur DogFather. Die bestehenden Pruefungen sehen
auf die ZIEL-Rolle ("darfst du 'manager' vergeben?") -- dass die
BETROFFENE Person DogFather ist, kam darin bis heute nicht vor.
Sicherung 3 haette es heute zufaellig abgefangen, weil es genau einen
gibt; eine Sperre, die nur wegen einer Zahl im Bestand haelt, ist
keine.

DIE OBERFLAECHE: "Rolle aendern" und "Loeschen" hingen an EINER Zeile
(`ich.rolle === 'admin'`). Sie gehoeren nicht zusammen -- Loeschen
bleibt bei DogFather. Und die CSS-Regel, die fuer Spicy Media die ganze
Knopfreihe ausblendete, ist weg: Welche Knoepfe es gibt, entscheidet
jetzt personen.js, und was nicht entsteht, muss man nicht verstecken.

pruef-spicy 62 -> 83. Gemessen wird jede der vier Kategorien EINZELN
(waere nur eine offen, saehe "geht" genauso aus), dazu: admin weder von
Spicy Media noch von DogFather vergebbar, an DogFathers Zeile aendert
sie nichts (und er ist danach nachweislich noch admin), ein Manager
kommt an den Weg nicht, loeschen bleibt zu -- und im Browser, dass der
KNOPF da ist, nicht nur das Recht. Genau daran war heute frueh im Chat
eine Stunde draufgegangen.

Dabei zwei eigene Messfehler gefunden: Die Knoepfe entstehen erst in
einer AUFGEKLAPPTEN Kategorie (vorher zu frueh gemessen), und /api/ich
wird jetzt als Vorbedingung geprueft.

DREI PRUEFUNGEN GEDREHT, KEINE GELOESCHT:
- pruef-personen-formular: 7 Karten, `admin` fehlt (eigene Aussage).
  Die Zeichenpruefung verglich die rechte Hand gegen die Admin-KARTE --
  die es nicht mehr gibt; sie lief gegen `undefined`. Ersatz ist das
  Zeichen selbst, und der Kommentar sagt, dass das schwaecher ist.
- pruef-haus-trennung: Der zweite DogFather entsteht jetzt im Bestand
  statt ueber die Schnittstelle, und DASS die Schnittstelle ihn
  ablehnt, ist der erste Prueffall geworden. Sonst waere "nie den
  letzten DogFather" ab heute ungeprueft -- weil ihre Voraussetzung
  schwerer herzustellen ist.
- pruef-creator-anlegen: aus einer Aussage zwei (die vier sind da UND
  admin fehlt).

Gruen: personen-liste, personen-kachel, verborgen, manager-sicht,
creator-anlegen, haus-trennung, personen-formular, spicy, rollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-12 00:25:47 +02:00
co-authored by Claude Opus 5
parent 1ab2729e47
commit e3aeeb01cd
37 changed files with 718 additions and 380 deletions
+14 -9
View File
@@ -431,16 +431,21 @@ console.log("\n=== Die Rollenknoepfe auf der Zugaenge-Seite ===");
const managerKnoepfe = await knoepfeFuer("manager", "CODE-MAX-00001");
ok(managerKnoepfe.length === 1 && managerKnoepfe[0] === "creator",
`ein Manager sieht genau einen: ${managerKnoepfe.join(", ")}`);
/* UND BEI DOGFATHER AENDERT SICH NICHTS. Er bekommt jetzt eine
laengere Liste vom Server (auch die verborgenen Rollen), die
Knopfliste der Seite kennt aber nur die fuenf sichtbaren -- der
Schnitt der beiden ist derselbe wie vorher. Das ist eine Behauptung
ueber MEINE Aenderung, also gehoert sie gemessen und nicht
geglaubt. */
/* BEI DOGFATHER FEHLT SEIT DEM 11.09.2026 GENAU EINE: seine eigene.
Filipe: "dogfather soll man nicht auswaehlen koennen. das ist die
einzige die man nicht auswaehlen kann bitte." Hier stand
`["spicy","admin","manager","scout","creator"].every(...)` -- die
Zeile war richtig, bis die Rolle aus ANLEGBAR fiel.
ZWEI AUSSAGEN STATT EINER: Die vier muessen DA sein, und `admin`
muss FEHLEN. Nur "vier sind da" waere auch dann gruen, wenn admin
wieder auftauchte. */
const dogiKnoepfe = await knoepfeFuer("admin", "CODE-DOGI-0001");
ok(dogiKnoepfe.length >= 5 && ["spicy", "admin", "manager", "scout", "creator"]
.every((r) => dogiKnoepfe.includes(r)),
`DogFather sieht weiterhin alle fuenf: ${dogiKnoepfe.join(", ")}`);
ok(["spicy", "manager", "scout", "creator"].every((r) => dogiKnoepfe.includes(r)),
`DogFather sieht die vier vergebbaren: ${dogiKnoepfe.join(", ")}`);
ok(!dogiKnoepfe.includes("admin"),
"seine eigene Rolle steht NICHT zur Wahl -- auch bei ihm nicht");
await browser.close();
}
+27 -6
View File
@@ -465,13 +465,34 @@ console.log("=== Die Rolle aendern ===");
keks: dogi.keks, rumpf: JSON.stringify({ rolle: "creator" }) });
ok(fremd.status === 403, `auf crew. auch nicht zum Creator (${fremd.status})`);
/* DER LETZTE DOGFATHER. Es gibt genau einen -- VanVan ist in dieser
Pruefung schon die rechte Hand. Also einen zweiten anlegen, den
ersten darf man dann herabstufen, den zweiten nicht mehr. */
const zweiter = await hole(weg, { host: WS, methode: "POST", keks: dogi.keks,
/* SEIT DEM 11.09.2026 LAESST SICH DIE ROLLE NICHT MEHR VERGEBEN.
Filipe: "dogfather soll man nicht auswaehlen koennen. das ist die
einzige die man nicht auswaehlen kann bitte." Hier stand bis dahin
ein Anlegen ueber die Schnittstelle -- das geht jetzt nicht mehr,
und das ist der erste Prueffall geworden statt eines Hilfsschritts. */
const versuch = await hole(weg, { host: WS, methode: "POST", keks: dogi.keks,
rumpf: JSON.stringify({ name: "Zweitzugang", rolle: "admin" }) });
const idZweit = zweiter.daten?.id;
ok(zweiter.status === 201 && idZweit > 0, `ein zweiter DogFather-Zugang (${zweiter.status})`);
ok(versuch.status === 403 || versuch.status === 400,
`ein zweiter DogFather laesst sich NICHT anlegen (${versuch.status})`);
/* DER LETZTE DOGFATHER -- und dafuer braucht es trotzdem zwei.
Der zweite entsteht jetzt direkt im Bestand, nicht ueber die
Schnittstelle. Das ist kein Trick an der Regel vorbei, sondern ihr
Gegenteil: Die Regel sagt "nicht ueber die Oberflaeche", und genau
das wird eine Zeile darueber nachgewiesen. Fuer die FOLGEfrage --
darf der vorletzte herabgestuft werden? -- braucht die Pruefung
einen Bestand, den die Oberflaeche nicht mehr herstellen kann.
Ohne diesen Umweg waere die Sicherung "nie den letzten DogFather"
ab heute ungeprueft. Eine Pruefung wegzulassen, weil ihre
Voraussetzung schwerer herzustellen ist, waere der bequeme Weg --
und die Sicherung steht zwischen einem Versehen und einem Haus
ohne Zugang. */
const idZweit = anlegen("Zweitzugang", "admin", "CODE-ZWEI-0001");
ok(Number.isInteger(idZweit) && idZweit > 0,
`ein zweiter DogFather-Zugang im Bestand (#${idZweit})`);
const runter = await hole(`${weg}/${idZweit}/rolle`, { host: WS, methode: "PUT",
keks: dogi.keks, rumpf: JSON.stringify({ rolle: "scout" }) });
+25 -7
View File
@@ -154,10 +154,20 @@ const mass = await dogi.evaluate(() => {
Geprüft wird ab jetzt die LISTE und nicht nur die Anzahl: Sechs
Karten könnten auch fünf richtige und eine doppelte sein. */
ok(mass.karten === 7, `alle sieben Rollen stehen zur Wahl (${mass.karten})`);
/* SECHS SEIT DEM 11.09.2026, NICHT SIEBEN. Filipe: "dogfather soll man
nicht auswaehlen koennen. das ist die einzige die man nicht
auswaehlen kann bitte." Die Karte faellt damit fuer JEDEN weg, auch
fuer DogFather selbst.
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(["admin", "creator", "hand", "manager", "modi", "scout", "spicy"]),
=== JSON.stringify(["creator", "gast", "hand", "manager", "modi", "scout", "spicy"]),
`und es sind die richtigen (${mass.kartenRollen.join(", ")})`);
ok(!mass.kartenRollen.includes("admin"),
"DogFather steht NICHT zur Wahl -- auch nicht fuer DogFather");
/* DIE STELLVERTRETUNG TRAEGT EIN EIGENES ZEICHEN.
@@ -179,11 +189,19 @@ ok(JSON.stringify([...mass.kartenRollen].sort())
ist daraus eine Gruppe geworden -- Husky bei den beiden mit dem
Ueberblick, Pfote bei der taeglichen Arbeit.
Verglichen wird gegen die Admin-Karte und nicht gegen "#r-husky":
Waere dort morgen ein anderes Zeichen, muesste die rechte Hand
mitwandern -- gewollt ist "dasselbe wie er", nicht "der Husky". */
ok(mass.kartenZeichen.hand && mass.kartenZeichen.hand === mass.kartenZeichen.admin,
`die Rechte Hand trägt dasselbe Zeichen wie DogFather (${mass.kartenZeichen.hand} / `
BIS ZUM 11.09.2026 WURDE GEGEN DIE ADMIN-KARTE VERGLICHEN, und das
war die bessere Messung: gewollt ist "dasselbe wie er", nicht "der
Husky". Seit DogFather nicht mehr vergeben werden kann, gibt es
diese Karte nicht mehr -- der Vergleich lief gegen `undefined` und
waere kommentarlos rot geblieben.
Ersatz ist das Zeichen selbst. Das ist schwaecher, und es steht hier,
damit es niemand fuer gleichwertig haelt: Aendert jemand DogFathers
Husky, faellt es hier NICHT mehr auf. Wer die alte Staerke
zurueckwill, braucht eine Quelle fuer "DogFathers Zeichen", die
nicht an der Auswahlkarte haengt. */
ok(mass.kartenZeichen.hand === "#r-husky",
`die Rechte Hand trägt den Husky (${mass.kartenZeichen.hand} / `
+ `${mass.kartenZeichen.admin})`);
/* DIE GEGENPROBE GLEICH DANEBEN: Der Modi muss sich davon
unterscheiden. Ohne sie waere die Zeile darueber auch dann gruen,
+137
View File
@@ -319,6 +319,78 @@ const ruf = async (art, weg, keks, koerper) => {
ok(m.status === 201 && m.daten?.rolle === "manager",
`und einen Manager (HTTP ${m.status}, ${m.daten?.rolle})`);
}
{
/* ---- ROLLEN WECHSELN (11.09.2026) -------------------------------
Filipe: "ich will dass die rolle spicy und dogfather, auch die
rollen wechseln koennen wenn die personen schon drin sind. von
alle kategorien, creator, scouts, manager spicy. dogfather soll
man nicht auswaehlen koennen."
AN EINEM EIGENEN MENSCHEN, nicht an Luna: Wer hier Luna umstellt,
aendert stillschweigend die Listen, die weiter unten verglichen
werden -- und der Fehlschlag stuende dann an einer Stelle, die
nichts damit zu tun hat. */
/* VORBEDINGUNG: Die Oberflaeche baut den Knopf aus `/api/ich`. Fehlt
die Angabe dort, ist jede Aussage ueber den Knopf weiter unten
eine Aussage ueber etwas anderes. */
const ichSpicy = await ruf("GET", "/workspace/api/ich", keksSpicy);
ok(ichSpicy.daten?.darf_rollen_wechseln === true,
`/api/ich sagt ihr, dass sie Rollen wechseln darf (${ichSpicy.daten?.darf_rollen_wechseln})`);
const neu = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy,
{ name: "Wechselkind" });
const idWechsel = neu.daten?.id;
ok(neu.status === 201 && Number.isInteger(idWechsel),
`ein Mensch zum Umstellen ist da (#${idWechsel})`);
const zu = async (keks, rolle, id = idWechsel) =>
ruf("PUT", `/workspace/api/verwaltung/personen/${id}/rolle`, keks, { rolle });
/* Die vier Kategorien, die Filipe genannt hat -- einzeln, nicht als
"irgendeine geht". Waere nur eine davon offen, saehe ein "geht"
genauso aus. */
for (const r of ["scout", "manager", "spicy"]) {
const a = await zu(keksSpicy, r);
ok(a.status === 200 && a.daten?.rolle === r,
`Spicy Media stellt auf "${r}" um (HTTP ${a.status})`);
}
const zurueck = await zu(keksSpicy, "creator");
ok(zurueck.status === 200, `und wieder zurueck auf Creator (HTTP ${zurueck.status})`);
/* DIE EINZIGE, DIE NICHT GEHT. */
const hoch = await zu(keksSpicy, "admin");
ok(hoch.status === 403,
`DogFather laesst sich nicht vergeben (HTTP ${hoch.status}, ${hoch.daten?.fehler || ""})`);
/* AUCH NICHT VON DOGFATHER SELBST. Das ist der Satz "das ist die
einzige die man nicht auswaehlen kann" -- ohne Ausnahme. */
const vonDogi = await zu(keksDogi, "admin");
ok(vonDogi.status === 403,
`auch DogFather kann sie nicht vergeben (HTTP ${vonDogi.status})`);
/* UND AN DOGFATHER SELBST AENDERT SIE NICHTS. Die Pruefungen oben
sehen auf die ZIEL-Rolle; dass die BETROFFENE Person DogFather
ist, faellt dort gar nicht auf. */
const anDogi = await zu(keksSpicy, "manager", idDogi);
ok(anDogi.status === 403,
`an DogFathers Zeile aendert sie nichts (HTTP ${anDogi.status})`);
const dogiNoch = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
const istNoch = (dogiNoch.daten?.personen || [])
.find((x) => x.id === idDogi)?.rolle;
ok(istNoch === "admin", `und er ist immer noch DogFather (${istNoch})`);
/* GEGENPROBE ZUR TUER: Ein Manager kommt an diesen Weg gar nicht --
sonst hiesse "Spicy Media darf" nur, dass jeder darf. */
const vonMax = await zu(keksMax, "scout");
ok(vonMax.status === 404, `ein Manager kommt an diesen Weg nicht (HTTP ${vonMax.status})`);
/* UND DIE UEBRIGE VERWALTUNG BLEIBT ZU. Die Tuer war eng gebaut;
diese Zeile ist der Beweis, dass sie eng geblieben ist. */
const wegDamit = await ruf("DELETE",
`/workspace/api/verwaltung/personen/${idWechsel}`, keksSpicy);
ok(wegDamit.status === 404,
`loeschen geht fuer sie weiterhin nicht (HTTP ${wegDamit.status})`);
}
{
/* GEGENPROBE: Ein Manager kann KEINEN Manager anlegen -- sonst waere
die zweite Tuer nur Zierde. */
@@ -549,6 +621,49 @@ console.log("\n=== Die Anmeldeseite ===");
`bei ihr steht der Abschnitt DogFather (${alsText.slice(0, 70) || "keine Abschnitte"})`);
ok(/Manager|Creator|Scout/.test(alsText),
"und die uebrigen Abschnitte ebenfalls -- die Liste ist nicht leer");
/* ERST AUFKLAPPEN, DANN MESSEN (11.09.2026).
Die Kategorien stehen zugeklappt da, und `zeile(p)` -- die Funktion,
die die Knoepfe baut -- laeuft nur fuer eine OFFENE Kategorie. Der
erste Entwurf las sofort nach dem Laden und meldete "kein Knopf".
Das war kein Befund, das war eine zu frueh gestellte Frage; genau
derselbe Fehler wie heute frueh bei den Kacheln der Startseite.
Geoeffnet wird die Kategorie, die es bei beiden Rollen gibt. */
for (const kopf of await seite.$$(".gruppe__kopf")) {
const t = (await kopf.textContent()) || "";
if (/Creator/.test(t)) { await kopf.click(); break; }
}
await seite.waitForTimeout(600);
const zeilenSpicy = await seite.$$eval(".person", (z) => z.length);
ok(zeilenSpicy > 0, `eine Kategorie ist aufgeklappt (${zeilenSpicy} Zeilen)`);
/* ---- DER KNOPF, NICHT NUR DAS RECHT (11.09.2026) ----------------
Am selben Tag ist mir im Chat genau das passiert: Das Recht war
richtig vergeben, die Oberflaeche wusste nichts davon, und der
Knopf blieb unsichtbar. Eine Pruefung, die nur die Schnittstelle
misst, waere dabei gruen geblieben.
ZWEI AUSSAGEN, NICHT EINE: Der Knopf "Rolle aendern" muss da sein,
"Loeschen" NICHT -- das gehoert weiterhin DogFather. Vorher hingen
beide an derselben Zeile. */
const knoepfeSpicy = await seite.$$eval(".person__tu, .tu-knopf, button",
(b) => b.map((x) => x.textContent.trim()));
ok(knoepfeSpicy.some((t) => /Rolle ändern/.test(t)),
`Spicy Media sieht "Rolle ändern" (${knoepfeSpicy.filter((t) => /Rolle|Löschen/.test(t)).join(", ") || "nichts davon"})`);
ok(!knoepfeSpicy.some((t) => /^Löschen$/.test(t)),
"und \"Löschen\" NICHT -- das bleibt bei DogFather");
/* Und in der Auswahl steht DogFather nicht. Gemessen an den
Knoepfen, die wirklich gebaut werden -- nicht an der Liste im
Quelltext, aus der sie entstehen. */
const rollenKnoepfe = await seite.$$eval("#f-rolle .rollenwahl__knopf",
(b) => b.map((x) => x.dataset.rolle));
ok(rollenKnoepfe.length >= 3 && !rollenKnoepfe.includes("admin"),
`zur Auswahl stehen ${rollenKnoepfe.join(", ")} -- ohne DogFather`);
ok(rollenKnoepfe.includes("spicy"),
"ihre eigene Rolle ist dabei -- Filipes Liste nennt sie ausdruecklich");
await ctx.close();
/* GEGENPROBE BEI DOGFATHER: Bei ihm steht der Abschnitt "Spicy Media"
@@ -568,6 +683,28 @@ console.log("\n=== Die Anmeldeseite ===");
ok(/Spicy Media/.test(abschnitteDogi),
`bei DogFather steht der Abschnitt "Spicy Media" (${abschnitteDogi.slice(0, 80)})`);
ok(/DogFather/.test(abschnitteDogi), "und seiner ebenfalls");
for (const kopf of await seite2.$$(".gruppe__kopf")) {
const t = (await kopf.textContent()) || "";
if (/Creator/.test(t)) { await kopf.click(); break; }
}
await seite2.waitForTimeout(600);
const zeilenDogi = await seite2.$$eval(".person", (z) => z.length);
ok(zeilenDogi > 0, `auch bei ihm ist sie aufgeklappt (${zeilenDogi} Zeilen)`);
/* GEGENPROBE BEI DOGFATHER: Bei ihm steht "Loeschen" sehr wohl --
sonst hiesse die Zeile oben nur, dass es den Knopf nirgends gibt.
Und auch in SEINER Auswahl fehlt DogFather. */
const knoepfeDogi = await seite2.$$eval(".person__tu, .tu-knopf, button",
(b) => b.map((x) => x.textContent.trim()));
ok(knoepfeDogi.some((t) => /^Löschen$/.test(t)),
"bei DogFather steht \"Löschen\" da");
ok(knoepfeDogi.some((t) => /Rolle ändern/.test(t)),
"und \"Rolle ändern\" ebenfalls");
const rollenDogi = await seite2.$$eval("#f-rolle .rollenwahl__knopf",
(b) => b.map((x) => x.dataset.rolle));
ok(rollenDogi.length >= 3 && !rollenDogi.includes("admin"),
`auch bei ihm ohne DogFather (${rollenDogi.join(", ")})`);
await ctx2.close();
await browser.close();
}
+53 -1
View File
@@ -15,7 +15,7 @@
import express from "express";
import {
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtModis, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, darfAnlegen, hausBedingung,
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtModis, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, darfAnlegen, darfRollenWechseln, hausBedingung,
} from "./workspace.js";
import { sicherungJetzt } from "./workspace-sicherung.js";
@@ -100,6 +100,35 @@ function nurAdmin(req, res, next) {
return next();
}
/* SPICY MEDIA DARF ROLLEN WECHSELN (11.09.2026).
Filipe: "ich will dass die rolle spicy und dogfather, auch die
rollen wechseln koennen wenn die personen schon drin sind. von
alle kategorien, creator, scouts, manager spicy."
DRITTE AUSNAHME, GLEICHE BAUWEISE wie die beiden darueber: eine
Methode, eine Adresse, eine Rolle. Wer hier etwas anderes baut,
hat drei Fassungen derselben Ausnahme -- und die dritte laesst
irgendwann mehr durch als gedacht.
Codes, Sperren, Loeschen, Zuteilung und Protokoll bleiben bei
DogFather. Filipe hat das Rollenwechseln genannt, nicht die
Verwaltung.
DER PFAD TRAEGT EINE NUMMER, laesst sich also nicht wie oben
vergleichen. Das Muster ist absichtlich streng: genau
/personen/<Ziffern>/rolle, nichts davor und nichts danach.
WELCHE Rolle sie vergeben darf, steht NICHT hier -- das entscheidet
ANLEGBAR in der Route. Diese Zeile oeffnet nur die Tuer. */
const rollenWeg = req.method === "PUT"
&& /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/rolle$/
.test(req.baseUrl + req.path);
if (darfRollenWechseln(person) && rollenWeg) {
req.person = person;
return next();
}
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
req.person = person;
next();
@@ -817,6 +846,29 @@ personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkun
return res.status(400).json({ fehler: "Diese Rolle hat sie schon." });
}
/* SICHERUNG 6 -- NEU AM 11.09.2026, WEIL SICH DIE TUER GEOEFFNET HAT.
Seit Spicy Media Rollen wechseln darf, gibt es einen Fall, den
es vorher nicht geben konnte: jemand ohne DogFather-Rolle, der
an einer DogFather-Zeile steht.
DIE PRUEFUNGEN DARUEBER FANGEN DAS NICHT. Sie sehen auf die
ZIEL-Rolle ("darfst du 'manager' vergeben?") -- und 'manager'
darf Spicy Media vergeben. Dass die Person, die da herabgestuft
wird, DogFather ist, steht in der AUSGANGS-Rolle, und die kam
bis heute nirgends vor.
Sicherung 3 (nie den letzten DogFather) haette es heute
zufaellig abgefangen, weil es genau einen gibt. Eine Sperre,
die nur wegen einer Zahl im Bestand haelt, ist keine.
Zusaetzlich sieht Spicy Media DogFather gar nicht in der Liste
(`req.ohneDogFather`). Das ist eine Sicht, keine Schranke -- wer
die Nummer kennt, ruft den Weg direkt auf. */
if (person.rolle === "admin" && !istDogFather(req.person)) {
return res.status(403).json({ fehler: "An der DogFather-Rolle ändert nur DogFather." });
}
/* SICHERUNG 2 */
if (id === req.person.id) {
return res.status(400).json({ fehler: "Die eigene Rolle lässt sich nicht ändern.",
+55 -4
View File
@@ -219,10 +219,35 @@ export const istDogFather = (person) => !!person && person.rolle === "admin";
endgueltigen Rechten, und den vergibt nur DogFather selbst.
======================================================================= */
const ANLEGBAR = {
/* DogFather: alles, was es gibt. Er geht ueber /verwaltung/personen. */
admin: [...ROLLEN],
/* Spicy Media: das ganze Team, aber keine Leitung ueber sich. */
spicy: ["manager", "scout", "creator"],
/* DogFather: alles ausser der eigenen Rolle.
DIE ROLLE "admin" IST SEIT DEM 11.09.2026 NICHT MEHR VERGEBBAR --
von niemandem, auch nicht von DogFather selbst. Filipe: "dogfather
soll man nicht auswaehlen koennen. das ist die einzige die man
nicht auswaehlen kann bitte."
WAS DAS BEDEUTET, damit es niemand spaeter sucht: Es laesst sich
kein zweiter DogFather-Zugang mehr anlegen, und niemand laesst
sich zu einem befoerdern. Der bestehende bleibt unberuehrt und ist
durch Sicherung 3 (nie den letzten DogFather herabstufen)
geschuetzt -- er kann also nicht versehentlich verschwinden.
Zurueckdrehen laesst sich das nur hier in dieser Liste.
Nebenwirkung, und sie ist erwuenscht: Damit gibt es keinen Weg
mehr, sich ueber die Oberflaeche zur hoechsten Rolle zu machen. */
admin: [...ROLLEN].filter((r) => r !== "admin"),
/* Spicy Media: das ganze Team -- und seit dem 11.09.2026 auch die
eigene Rolle.
Filipe: "ich will dass die rolle spicy und dogfather, auch die
rollen wechseln koennen wenn die personen schon drin sind. von
alle kategorien, creator, scouts, manager spicy."
Dass jemand seine eigene Rolle weitergeben kann, ist eine
Entscheidung und kein Versehen: Spicy Media fuehrt die Agentur.
DogFather bleibt trotzdem ausserhalb der Liste -- niemand hebt
sich ueber die Rolle, die ihn eingesetzt hat. */
spicy: ["spicy", "manager", "scout", "creator"],
/* Ein Manager stellt Creator ein, die er dann auch betreut. */
manager: ["creator"],
};
@@ -247,6 +272,26 @@ export function darfAnlegen(person) {
return alle.filter((r) => HAUS_TEAM_ROLLEN.has(r));
}
/** Wer darf die Rolle einer Person aendern, die schon da ist?
*
* (11.09.2026) Filipe: "ich will dass die rolle spicy und dogfather,
* auch die rollen wechseln koennen wenn die personen schon drin
* sind."
*
* EINE REGEL, DREI BENUTZER: die Schranke in nurAdmin, der Knopf in
* der Oberflaeche (ueber `darf_rollen_wechseln` in /api/ich) und die
* Route selbst. Stuende sie dreimal da, waere die dritte Abschrift
* die, die eine Rolle vergisst -- genau so ist am selben Tag im Chat
* ein Knopf unsichtbar geblieben, obwohl das Recht stimmte.
*
* WAS SIE NICHT ENTSCHEIDET: WELCHE Rolle vergeben werden darf. Das
* steht in ANLEGBAR und ist bewusst getrennt -- "darf ueberhaupt
* wechseln" und "darf DIESE Rolle vergeben" sind zwei Fragen, und
* ihre Antworten laufen auseinander, sobald eine Rolle dazukommt.
*/
export const darfRollenWechseln = (person) =>
!!person && (person.rolle === "admin" || person.rolle === "spicy");
/* WER FUEHRT TEAM DOGI? (10.09.2026)
*
* DogFather und seine rechte Hand -- und sonst niemand. Spicy Media und
@@ -3982,6 +4027,12 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
serverseitig offen war. Wer die Antwort nur an einer Stelle hat,
kann sie nicht an zweien verschieden haben. */
darf_anlegen: darfAnlegen(person),
/* OB DER KNOPF "ROLLE AENDERN" ERSCHEINT (11.09.2026). Aus
derselben Regel wie die Schranke dahinter -- die Oberflaeche
vergleicht keine Rollennamen mehr selbst. Vorher stand dort
`ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das
Loeschen frei; die beiden gehoeren nicht zusammen. */
darf_rollen_wechseln: darfRollenWechseln(person),
/* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN.
Fuer die fuenf bekannten Rollen steht hier `null`, und die