From 710b766ffa042f011f3eda50c35354b4d115f16b Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 24 Sep 2026 11:16:26 +0200 Subject: [PATCH] =?UTF-8?q?Die=20rechte=20Hand:=20dieselben=20Rechte,=20au?= =?UTF-8?q?=C3=9Fer=20an=20DogFather?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe: „die rechte hand soll das auch sehen. und die selben rechte da haben wie dogfather. das einzige was sie nicht kann ist die dogfather rolle oder leute anfassen. also da kann sie nichts verändern." ZUERST EIN IRRTUM VON MIR. Ich hatte den Bildschirmfoto-Ausschnitt fuer die Rechtetafel gehalten und dort gebaut. „Vertritt dich im Alltag und koordiniert das Team" steht aber in workspace-personen.js: Gemeint war die PERSONENSEITE. Die Arbeit an der Rechtetafel ist trotzdem drin (siehe unten) -- sie loeste dasselbe Problem an einer zweiten Stelle. === DIE PERSONENSEITE === DIE OBERFLAECHE WAR STRENGER ALS DER SERVER. In personen.js stand `if (ich.rolle === 'hand') { keine Knoepfe }` mit der Begruendung „Der Server antwortet ihr auf jeden davon mit 404". Am 22.09. stimmte das. Seither wurde der Server ZWEIMAL erweitert -- sie durfte Personen anlegen, Codes neu erzeugen und Rollen aendern -- und diese Zeile blieb stehen. Sie hatte drei Rechte und sah keinen einzigen Knopf. Ein Rollenvergleich im Browser ist genau die zweite Wahrheit, die still veraltet. Jetzt fragt die Oberflaeche den Server (`darf_zugaenge_verwalten`, `darf_personen_loeschen`, `rollen_anfassbar`). Dazu kommen SPERREN und LOESCHEN, die bis heute ausdruecklich bei DogFather lagen -- Filipes „das einzige" ist juenger und eindeutig. Die Knoepfe „Neuer Code" und „Sperren" standen inline im DogFather-Zweig; sie sind jetzt Funktionen und werden von beiden Stellen benutzt. Eine zweite Abschrift waere die geworden, die beim naechsten Umbau nur halb nachgezogen wird. ZWEI ECHTE LOECHER FAND DIE NEUE PRUEFUNG: * `PUT /personen/:id/rolle` hatte `nurDogFatherBeiLeitung` NICHT. Bis heute folgenlos; mit den neuen Rechten konnte die rechte Hand darueber die Rolle eines MANAGERS aendern -- waehrend derselbe Manager fuer DogFather auf crew. gar nicht in der Liste steht. Zwei Wege, zwei Antworten, und der laxere galt fuer die Rolle mit weniger Rechten. * Die Loesch-Route hatte dieselbe Middleware ebenfalls nicht. Das war harmlos, solange die Route selbst nur DogFather durchliess -- seit die rechte Hand loescht, ist es die Stelle, an der DogFather geschuetzt wird. === DIE RECHTETAFEL (nicht bestellt, aber dasselbe Problem) === Dort durfte sie sehen, aber nichts umstellen. Jetzt umstellen wie DogFather -- ausser der Spalte „DogFather" und der Zeile „Personen & Zugaenge" (wer die freischaltet, hat Zugaenge vergeben, ohne einen anzulegen). Zuruecksetzen bleibt bei DogFather: Der Knopf naehme genau diese zwei Sperren mit, und eine Sperre, die ein zweiter Knopf daneben aufhebt, ist keine. Sie kann sich auch selbst nicht aussperren -- das hat er nicht gesagt, aber eine Sperre, aus der man sich aussperren kann, ist eine Falle. Ein festes Feld ist jetzt gar kein Knopf mehr und nennt den Grund, der fuer DIESE Person gilt. === DREI MESSFEHLER VON MIR === Ein Manager ist auf crew. fuer die LISTE unsichtbar, fuer DogFathers direkten Zugriff aber nicht -- ich hielt das eine fuer das andere und erwartete, dass beide abgewiesen werden. Die Loesch-Vorschau (GET) fehlte in meinem Waechter. Und `rollen_anfassbar` beantwortet eine andere Frage als „wen sehe ich". Gemessen: pruef-hand-personen, 40 Pruefungen, 0 Fehler -- jeder der fuenf Wege einzeln gegen DogFather und die linke Hand, plus die Gegenprobe, dass DogFather es kann. pruef-rechte-umstellen von 46 auf 56 Pruefungen. Gruen: pruef-personen-liste, pruef-personen-loeschen, pruef-personen-kachel, pruef-rollen-anlegen, pruef-rechtetafel. Co-Authored-By: Claude Opus 5 --- server/pruef-hand-personen.mjs | 333 ++++++++++++++++++++++++++++++ server/pruef-rechte-umstellen.mjs | 74 ++++++- server/rechte.js | 89 +++++++- server/workspace-personen.js | 87 +++++++- server/workspace-rechte.js | 67 +++++- server/workspace.js | 25 +++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/treff.css | 7 + workspace/assets/js/personen.js | 152 ++++++++++---- workspace/assets/js/rechte.js | 49 ++++- workspace/aufgaben.html | 38 ++-- workspace/automation.html | 32 +-- workspace/befinden.html | 34 +-- workspace/bereich.html | 44 ++-- workspace/bewerben.html | 38 ++-- workspace/bewerbungen.html | 36 ++-- workspace/calls.html | 34 +-- workspace/chat.html | 38 ++-- workspace/content.html | 34 +-- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +-- workspace/entwicklung.html | 36 ++-- workspace/hilfe.html | 30 +-- workspace/index.html | 10 +- workspace/kalender.html | 34 +-- workspace/leistung.html | 32 +-- workspace/material.html | 32 +-- workspace/personen.html | 32 +-- workspace/profil.html | 36 ++-- workspace/rechte.html | 34 +-- workspace/report.html | 36 ++-- workspace/scouting.html | 32 +-- workspace/start.html | 32 +-- workspace/startcheck.html | 34 +-- workspace/steckbrief.html | 36 ++-- workspace/support.html | 28 +-- workspace/talente.html | 36 ++-- workspace/team.html | 32 +-- workspace/teamlage.html | 34 +-- workspace/teilen.html | 30 +-- workspace/treff-moderation.html | 32 +-- workspace/treff-regeln.html | 32 +-- workspace/uebersicht.html | 32 +-- workspace/unsere-seiten.html | 30 +-- workspace/werdegang.html | 32 +-- workspace/willkommen.html | 30 +-- workspace/wissen.html | 34 +-- 49 files changed, 1429 insertions(+), 682 deletions(-) create mode 100644 server/pruef-hand-personen.mjs diff --git a/server/pruef-hand-personen.mjs b/server/pruef-hand-personen.mjs new file mode 100644 index 00000000..2801ae39 --- /dev/null +++ b/server/pruef-hand-personen.mjs @@ -0,0 +1,333 @@ +/* ===================================================================== + DIE RECHTE HAND AUF DER PERSONENSEITE (24.09.2026) + + Filipe: „die rechte hand soll das auch sehen. und die selben rechte + da haben wie dogfather. das einzige was sie nicht kann ist die + dogfather rolle oder leute anfassen. also da kann sie nichts + verändern." + + WAS HIER GEPRUEFT WIRD, und warum jede Zeile: + + * SIE DARF, was DogFather darf -- anlegen, Code, Rolle, sperren, + loeschen. Bis heute war die Oberflaeche strenger als der Server: + Sie hatte drei dieser Rechte und sah keinen einzigen Knopf. + * SIE DARF NICHT an DogFather und an die Manager. Das ist die + Grenze aus seinem Satz, und sie wird EINZELN geprueft -- fuer + jeden der fuenf Wege. + * DER LETZTE DOGFATHER-ZUGANG bleibt. Diese Sicherung stand + vorher hinter „nur DogFather darf loeschen"; seit die rechte + Hand loescht, muss sie allein tragen. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-hp-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +import { notbremse } from "./helfer-notbremse.mjs"; +const { eigenerPort } = await import("./helfer-port.mjs"); +const PORT = await eigenerPort(import.meta, "die Hand-Personen-Pruefung"); + +process.env.PORT = `${PORT}`; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +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 }); }; +await import("./index.js"); +notbremse(240_000, "pruef-hand-personen"); +await new Promise((r) => setTimeout(r, 900)); + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +const melde = (t) => console.log(t); + +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 ks = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", ks, jetzt); +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, 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(name, rolle, hash, salt, 32768, + createHmac("sha256", ks).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); +const idLinke = anlegen("Funny", "linke", "CODE-LINK-0001"); +const idModi = anlegen("Miss", "modi", "CODE-MODI-0001"); +const idModi2 = anlegen("Ghost", "modi", "CODE-MODI-0002"); +const idMana = anlegen("Patrick", "manager", "CODE-MANA-0001"); +const idGast = anlegen("Kessi", "gast", "CODE-GAST-0001"); +d.close(); + +const CREW = "crew.dogfather-universe.com"; +const { request } = await import("node:http"); + +/** Ein Aufruf mit Hausadresse -- `fetch` verwirft den Host-Kopf. */ +function roh(pfad, keks, koerper, methode) { + return new Promise((fertig, schief) => { + const daten = koerper ? JSON.stringify(koerper) : null; + const a = request({ + host: "127.0.0.1", port: PORT, path: pfad, + method: methode || (daten ? "POST" : "GET"), + headers: { + Host: CREW, Origin: `https://${CREW}`, + ...(keks ? { Cookie: keks } : {}), + ...(daten ? { "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(daten) } : {}), + }, + }, (antwort) => { + let text = ""; + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text })); + }); + a.on("error", schief); + if (daten) a.write(daten); + a.end(); + }); +} +const json = (r) => { try { return JSON.parse(r.text); } catch { return {}; } }; + +async function anmelden(rolle, code) { + const r = await new Promise((fertig, schief) => { + const daten = JSON.stringify({ rolle, code, alter_ok: true }); + const a = request({ + host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", + method: "POST", + headers: { Host: CREW, Origin: `https://${CREW}`, + "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) }, + }, (antwort) => { + let text = ""; + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers })); + }); + a.on("error", schief); + a.write(daten); a.end(); + }); + if (r.code !== 200) return null; + return [].concat(r.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +} + +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +const kHand = await anmelden("hand", "CODE-HAND-0001"); +const kLinke = await anmelden("linke", "CODE-LINK-0001"); +const kModi = await anmelden("modi", "CODE-MODI-0001"); + +melde("\n=== 0. Alle vier kommen herein ==="); +ok(!!kDogi && !!kHand && !!kLinke && !!kModi, "DogFather, rechte Hand, linke Hand, Modi"); + +/* ===================================================================== + 1. WAS DIE OBERFLAECHE ERFAEHRT + ===================================================================== */ +melde("\n=== 1. Die Auskunft an die Oberflaeche ==="); +{ + const ichDogi = json(await roh("/workspace/api/ich", kDogi)); + const ichHand = json(await roh("/workspace/api/ich", kHand)); + const ichLinke = json(await roh("/workspace/api/ich", kLinke)); + const ichModi = json(await roh("/workspace/api/ich", kModi)); + + ok(ichDogi.darf_zugaenge_verwalten === true, "DogFather verwaltet Zugaenge"); + ok(ichHand.darf_zugaenge_verwalten === true, "die rechte Hand auch"); + ok(ichLinke.darf_zugaenge_verwalten === false, + "die LINKE Hand nicht -- das war eine eigene Entscheidung vom 21.09."); + ok(ichModi.darf_zugaenge_verwalten !== true, "ein Modi erst recht nicht"); + + ok(ichHand.darf_personen_loeschen === true, "sie darf loeschen"); + ok(ichLinke.darf_personen_loeschen !== true, "die linke Hand nicht"); + + /* WEN sie anfassen darf -- die Grenze aus Filipes Satz. */ + const anf = ichHand.rollen_anfassbar || []; + ok(!anf.includes("admin"), `DogFather steht NICHT darin (${anf.join(", ")})`); + /* DIE MANAGER STEHEN IN `rollen_anfassbar` -- und das ist KEIN + Mangel, sondern die falsche Frage. `rollen_anfassbar` sagt, welche + Rollen sie grundsaetzlich aendern duerfte; ob ein bestimmter + Mensch fuer sie existiert, entscheidet die Haustrennung: Auf + crew. liefert `hausBedingung` nur Team-Dogi-Rollen, ein Manager + ist dort fuer NIEMANDEN sichtbar, auch nicht fuer DogFather. + Gemessen wird das in Abschnitt 3 an den echten Wegen. */ + ok(Array.isArray(anf), `rollen_anfassbar ist eine Liste (${anf.join(", ")})`); + ok(anf.includes("modi") || anf.includes("gast"), + "Modis und Community schon -- sonst waere das Recht leer"); +} + +/* ===================================================================== + 2. WAS SIE DARF + ===================================================================== */ +melde("\n=== 2. Was sie darf ==="); +let neuerModi = 0; +{ + const anlegen2 = await roh("/workspace/api/verwaltung/personen", kHand, + { name: "Neue Modi", rolle: "modi" }); + ok(anlegen2.code === 200 || anlegen2.code === 201, + `sie legt einen Modi an (${anlegen2.code})`); + neuerModi = json(anlegen2).id || 0; + ok(!!json(anlegen2).code, "und sieht den Code genau einmal"); + + const code = await roh(`/workspace/api/verwaltung/personen/${idModi}/code`, kHand, {}, "POST"); + ok(code.code === 200, `sie erzeugt einen Code neu (${code.code})`); + + const rolle = await roh(`/workspace/api/verwaltung/personen/${idModi2}/rolle`, kHand, + { rolle: "gast" }, "PUT"); + ok(rolle.code === 200, `sie aendert eine Rolle (${rolle.code})`); + + const sperren = await roh(`/workspace/api/verwaltung/personen/${idModi}`, kHand, + { aktiv: false }, "PATCH"); + ok(sperren.code === 200, `sie sperrt (${sperren.code}) -- das ist neu seit heute`); + + const entsperren = await roh(`/workspace/api/verwaltung/personen/${idModi}`, kHand, + { aktiv: true }, "PATCH"); + ok(entsperren.code === 200, `und entsperrt wieder (${entsperren.code})`); + + if (neuerModi) { + const vorschau = await roh(`/workspace/api/verwaltung/personen/${neuerModi}/loeschbar`, kHand); + ok(vorschau.code === 200, `sie sieht die Loesch-Vorschau (${vorschau.code})`); + const weg = await roh(`/workspace/api/verwaltung/personen/${neuerModi}`, kHand, + { name: "Neue Modi" }, "DELETE"); + ok(weg.code === 200, `sie loescht (${weg.code}) -- das ist neu seit heute`); + } else { + ok(false, "kein angelegter Modi zum Loeschen"); + } +} + +/* ===================================================================== + 3. WAS SIE NICHT DARF -- jeden Weg einzeln + ===================================================================== */ +melde("\n=== 3. An DogFather und den Managern nicht ==="); +{ + /* GEPRUEFT WIRD MIT TEAM-DOGI-ROLLEN. Ein Manager gehoert zur + Agentur und ist auf crew. fuer niemanden sichtbar -- ihn hier zu + nehmen haette 404 gemessen und „geschuetzt" genannt, obwohl es + nur die Haustrennung war. Die linke Hand ist der richtige Fall: + Sie IST auf dieser Adresse, und die rechte Hand darf sie + trotzdem nicht anfassen. */ + for (const [wen, id, name] of [ + ["DogFather", idDogi, "Filipe"], ["die linke Hand", idLinke, "Funny"]]) { + const code = await roh(`/workspace/api/verwaltung/personen/${id}/code`, kHand, {}, "POST"); + ok(code.code === 403 || code.code === 404, + `kein neuer Code fuer ${wen} (${code.code})`); + + const rolle = await roh(`/workspace/api/verwaltung/personen/${id}/rolle`, kHand, + { rolle: "modi" }, "PUT"); + ok(rolle.code === 403 || rolle.code === 404, + `keine Rollenaenderung bei ${wen} (${rolle.code})`); + + const sperren = await roh(`/workspace/api/verwaltung/personen/${id}`, kHand, + { aktiv: false }, "PATCH"); + ok(sperren.code === 403 || sperren.code === 404, + `kein Sperren von ${wen} (${sperren.code})`); + + const weg = await roh(`/workspace/api/verwaltung/personen/${id}`, kHand, + { name }, "DELETE"); + ok(weg.code === 403 || weg.code === 404, + `kein Loeschen von ${wen} (${weg.code})`); + + const vorschau = await roh(`/workspace/api/verwaltung/personen/${id}/loeschbar`, kHand); + ok(vorschau.code === 403 || vorschau.code === 404, + `nicht einmal die Loesch-Vorschau von ${wen} (${vorschau.code})`); + } + + /* UND DIE MANAGER -- hier zeigt sich der Unterschied am deutlichsten. + + Mein erster Anlauf erwartete, dass BEIDE abgewiesen werden ("auf + crew. gibt es keine Manager"). Gemessen: Hand 403, DogFather 200. + Das war kein Loch, sondern genau die bestellte Ordnung -- die + Haustrennung gilt fuer die LISTE, nicht fuer den direkten Zugriff + von DogFather. Ich hatte das eine fuer das andere gehalten. + + Beide Zeilen stehen jetzt getrennt da, und gerade ihr Unterschied + ist die Aussage: Derselbe Weg, zwei Rollen, zwei Antworten. */ + const handMana = await roh(`/workspace/api/verwaltung/personen/${idMana}`, kHand, + { aktiv: false }, "PATCH"); + ok(handMana.code === 403 || handMana.code === 404, + `die rechte Hand kommt an einen Manager nicht heran (${handMana.code})`); + const dogiMana = await roh(`/workspace/api/verwaltung/personen/${idMana}`, kDogi, + { aktiv: false }, "PATCH"); + ok(dogiMana.code === 200, + `DogFather schon (${dogiMana.code}) -- sonst hiesse "sie darf es nicht" nur, ` + + `dass es niemand darf`); + /* Wieder entsperren, damit die Schlussprobe den Grundstand sieht. */ + await roh(`/workspace/api/verwaltung/personen/${idMana}`, kDogi, { aktiv: true }, "PATCH"); + + /* SIE KANN KEINEN DOGFATHER ERSCHAFFEN. Die Rolle ist fuer niemanden + vergebbar, auch nicht fuer DogFather selbst -- hier zaehlt, dass + der Weg ueber sie nicht offensteht. */ + const machDogi = await roh(`/workspace/api/verwaltung/personen/${idModi}/rolle`, kHand, + { rolle: "admin" }, "PUT"); + ok(machDogi.code >= 400, `sie macht niemanden zum DogFather (${machDogi.code})`); + + const anlegeDogi = await roh("/workspace/api/verwaltung/personen", kHand, + { name: "Zweiter Chef", rolle: "admin" }); + ok(anlegeDogi.code >= 400, `und legt keinen an (${anlegeDogi.code})`); +} + +/* ===================================================================== + 4. DIE ANDEREN ROLLEN KOMMEN GAR NICHT HERAN + ===================================================================== */ +melde("\n=== 4. Alle anderen ==="); +{ + for (const [wer, keks] of [["die linke Hand", kLinke], ["ein Modi", kModi]]) { + const anl = await roh("/workspace/api/verwaltung/personen", keks, + { name: "Versuch", rolle: "modi" }); + ok(anl.code >= 400, `${wer} legt niemanden an (${anl.code})`); + const weg = await roh(`/workspace/api/verwaltung/personen/${idGast}`, keks, + { name: "Kessi" }, "DELETE"); + ok(weg.code >= 400, `${wer} loescht niemanden (${weg.code})`); + } +} + +/* ===================================================================== + 5. GEGENPROBEN -- kann das hier ueberhaupt rot werden? + ===================================================================== */ +melde("\n=== 5. Gegenproben (jede MUSS anschlagen) ==="); +{ + /* DogFather selbst kann alles -- sonst hiesse „sie darf dasselbe" + nur, dass beide nichts duerfen. */ + const dogiCode = await roh(`/workspace/api/verwaltung/personen/${idLinke}/code`, kDogi, {}, "POST"); + ok(dogiCode.code === 200, + `DogFather erzeugt einen Code fuer die linke Hand (${dogiCode.code}) -- ` + + `sie kann es nicht, er schon`); + + /* DER LETZTE DOGFATHER-ZUGANG BLEIBT -- auch fuer ihn selbst. + Diese Sicherung stand vorher hinter „nur DogFather darf + loeschen"; jetzt traegt sie allein. */ + const letzter = await roh(`/workspace/api/verwaltung/personen/${idDogi}`, kDogi, + { name: "Filipe" }, "DELETE"); + ok(letzter.code >= 400, + `der letzte DogFather-Zugang laesst sich nicht loeschen (${letzter.code})`); + + /* Ohne Anmeldung gar nichts. */ + const ohne = await roh("/workspace/api/verwaltung/personen", null, + { name: "Fremd", rolle: "modi" }); + ok(ohne.code >= 400, `ohne Anmeldung nichts (${ohne.code})`); + + /* UND DIE TAFEL IST DANACH UNVERSEHRT: DogFather, Manager und die + linke Hand stehen noch da. Ein abgewiesener Versuch, der trotzdem + etwas aendert, waere der schlimmste Ausgang. */ + const liste = json(await roh("/workspace/api/verwaltung/personen", kDogi)); + const namen = (liste.personen || []).map((x) => x.rolle); + /* Ohne "manager" -- der ist auf crew. gar nicht in der Liste, siehe + oben. Ihn hier zu erwarten hiesse, die Haustrennung fuer einen + Fehler zu halten. */ + ok(namen.includes("admin") && namen.includes("linke") && namen.includes("hand"), + `nach allen Versuchen stehen sie noch da (${namen.length} Personen: ` + + `${[...new Set(namen)].join(", ")})`); +} + +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + +console.log(""); +console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-rechte-umstellen.mjs b/server/pruef-rechte-umstellen.mjs index ccf66421..66e9cf39 100644 --- a/server/pruef-rechte-umstellen.mjs +++ b/server/pruef-rechte-umstellen.mjs @@ -127,7 +127,13 @@ melde("=== 1. Wer die Tafel sieht ==="); const alsDogi = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); const alsHand = json(await roh("/workspace/api/rechte/tafel", CREW, { cookie: kHand })); ok(alsDogi.darf_aendern === true, "DogFather darf umstellen"); - ok(alsHand.darf_aendern === false, "die rechte Hand sieht dieselbe Tafel, darf aber nicht"); + /* SEIT DEM 24.09.2026 DARF SIE AUCH UMSTELLEN. + Filipe: „die selben rechte da haben wie dogfather. das einzige + was sie nicht kann ist die dogfather rolle oder leute anfassen." + Was sie NICHT darf, wird in Abschnitt 5 einzeln geprueft. */ + ok(alsHand.darf_aendern === true, "die rechte Hand darf jetzt auch umstellen"); + ok(alsDogi.darf_zuruecksetzen === true && alsHand.darf_zuruecksetzen === false, + "zuruecksetzen bleibt bei DogFather -- es naehme auch ihre Sperren mit"); ok(alsHand.seiten?.length === alsDogi.seiten?.length, `und sie bekommt wirklich dieselbe (${alsHand.seiten?.length} Seiten)`); @@ -257,20 +263,76 @@ melde("=== 4. Aussperren geht nicht ==="); 5. WER NICHT DARF, DARF AUCH ÜBER DIE SCHNITTSTELLE NICHT ======================================================================= */ melde(""); -melde("=== 5. Nur DogFather stellt um ==="); +melde("=== 5. Die rechte Hand stellt um -- aber nicht alles ==="); { - const hand = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + /* WAS SIE DARF (seit 24.09.2026). */ + const darf = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, { pfad: PROBE, rolle: "modi", erlaubt: false }, "PUT"); - ok(hand.code === 403, `die rechte Hand bekommt 403 (${hand.code})`); - ok(/DogFather/.test(json(hand).fehler || ""), "und erfährt, wer es darf"); + ok(darf.code === 200, `sie stellt ein gewoehnliches Feld um (${darf.code})`); + /* UND ES WIRKT WIRKLICH -- nicht nur die Anzeige. */ + const zu = await roh(PROBE, CREW, { cookie: kModi }); + ok(zu.code !== 200, `danach kommt der Modi nicht mehr auf die Seite (${zu.code})`); + /* Gleich wieder zurueck, damit die folgenden Abschnitte den + Grundstand vorfinden. */ + await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + { pfad: PROBE, rolle: "modi", erlaubt: true }, "PUT"); + /* WAS SIE NICHT DARF -- die drei Sperren, einzeln. */ + const dogiSpalte = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + { pfad: PROBE, rolle: "admin", erlaubt: false }, "PUT"); + ok(dogiSpalte.code === 400, + `die Spalte DogFather nicht (${dogiSpalte.code})`); + ok(/DogFather/.test(json(dogiSpalte).fehler || ""), + "und sie erfaehrt, warum"); + + /* AUCH NICHT ZUSCHALTEN. Nur das Wegnehmen zu sperren waere + dieselbe Tuer, nur andersherum -- sie koennte ihm jede Seite + AUFdruecken. */ + const dogiZu = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + { pfad: "/workspace/befinden.html", rolle: "admin", erlaubt: true }, "PUT"); + ok(dogiZu.code === 400, + `auch zuschalten kann sie ihm nichts (${dogiZu.code})`); + + const leute = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + { pfad: "/workspace/personen.html", rolle: "modi", erlaubt: true }, "PUT"); + ok(leute.code === 400, + `und "Personen & Zugaenge" fuer niemanden (${leute.code})`); + + /* UND SIE KANN SICH NICHT SELBST AUSSPERREN. Das hat Filipe nicht + gesagt; eine Sperre, aus der man sich aussperren kann, ist aber + keine Einstellung, sondern eine Falle. */ + const selbst = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + { pfad: "/workspace/rechte.html", rolle: "hand", erlaubt: false }, "PUT"); + ok(selbst.code === 400, + `sie kann sich die Rechteseite nicht selbst wegnehmen (${selbst.code})`); + + /* DIE OBERFLAECHE WEISS DASSELBE -- sonst zeigt sie einen Knopf, + den der Server danach ablehnt. */ + const tafelHand = json(await roh("/workspace/api/rechte/tafel", CREW, { cookie: kHand })); + const zeileProbe = tafelHand.seiten.find((x) => x.pfad === PROBE); + const zeilePers = tafelHand.seiten.find((x) => x.pfad === "/workspace/personen.html"); + ok(zeileProbe?.fest?.includes("admin"), + "die Tafel zeichnet ihr die Spalte DogFather als fest"); + ok(zeilePers?.fest?.length >= 5, + `und die ganze Zeile "Personen & Zugaenge" (${zeilePers?.fest?.length} Felder)`); + ok(!!zeileProbe?.fest_grund?.admin, + "mit einem Grund, der danebensteht"); + + /* GEGENPROBE: Bei DogFather ist genau das NICHT fest -- sonst + hiesse "fest" nur, dass die Tafel immer dasselbe sagt. */ + const tafelDogi2 = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + const dogiProbe = tafelDogi2.seiten.find((x) => x.pfad === PROBE); + ok(!dogiProbe?.fest?.includes("admin"), + "fuer DogFather selbst ist dieselbe Spalte NICHT fest"); + + /* Andere Rollen kommen weiterhin gar nicht heran. */ const modi = await roh("/workspace/api/rechte/feld", CREW, { cookie: kModi }, { pfad: PROBE, rolle: "modi", erlaubt: true }, "PUT"); ok(modi.code === 404, `ein Modi bekommt 404 – für ihn gibt es die Tafel nicht (${modi.code})`); const alles = await roh("/workspace/api/rechte/abweichungen", CREW, { cookie: kHand }, null, "DELETE"); - ok(alles.code === 403, `und zurücksetzen darf sie auch nicht (${alles.code})`); + ok(alles.code === 403, `und zurücksetzen darf sie nicht (${alles.code})`); /* GEGENPROBE: Nach all dem ist die Tafel unverändert. Ein abgewiesener Versuch, der trotzdem etwas ändert, wäre der diff --git a/server/rechte.js b/server/rechte.js index abb65316..b54d8ffb 100644 --- a/server/rechte.js +++ b/server/rechte.js @@ -676,10 +676,97 @@ export const UNANTASTBAR = [ { pfad: "/workspace/start.html", rolle: "admin" }, ]; +/* ===================================================================== + WAS DIE RECHTE HAND NICHT ANFASSEN DARF (24.09.2026) + + Filipe: „die rechte hand soll das auch sehen. und die selben rechte + da haben wie dogfather. das einzige was sie nicht kann ist die + dogfather rolle oder leute anfassen. also da kann sie nichts + verändern." + + SEHEN KONNTE SIE DIE TAFEL SCHON -- `nurLeitung` liess sie herein, + und die Kachel steht in ihrer Liste. Was ihr fehlte, war das + Umstellen: Jeder Knopf antwortete mit „Umstellen kann das nur + DogFather." + + ZWEI SPERREN, und beide stehen hier statt in der Route -- damit die + Oberflaeche dieselbe Auskunft bekommt und den Knopf gar nicht erst + als Knopf zeigt: + + 1. DIE SPALTE „DogFather". Sie entscheidet, was er selbst sieht. + Wer sie aendern kann, kann ihm Seiten wegnehmen. + 2. DIE ZEILE „Personen & Zugaenge" -- „leute anfassen". Wer + personen.html fuer eine Rolle freischaltet, hat damit + Zugaenge vergeben, ohne einen einzigen anzulegen. + + DRITTENS, UND DAS HAT ER NICHT GESAGT: Sie kann sich auch selbst + nicht aussperren. Dieselbe Ueberlegung wie bei UNANTASTBAR fuer + DogFather -- eine Sperre, aus der man sich aussperren kann, ist + keine Einstellung, sondern eine Falle. Haette ich es weggelassen, + waere der erste Fehlklick auf „rechte.html / Rechte Hand" das Ende + ihres Zugangs zu dieser Seite gewesen. + ===================================================================== */ + +/** Die Spalte, die nur DogFather selbst aendern darf. */ +const NUR_DOGFATHER_SPALTE = "admin"; +/** Die Zeile, die nur DogFather aendern darf -- fuer JEDE Rolle. */ +const NUR_DOGFATHER_ZEILE = "/workspace/personen.html"; +/** Und was die rechte Hand sich selbst nicht wegnehmen kann. */ +const HAND_BRAUCHT = ["/workspace/rechte.html", "/workspace/start.html"]; + +/** Darf DIESE PERSON dieses Feld anfassen? -- unabhaengig davon, ob + * sie es an- oder abwaehlen will. + * + * Getrennt von `darfUmstellen`, weil die Oberflaeche genau diese + * Frage stellt, bevor sie ein Feld zeichnet: Ein Knopf, der eine + * Absage bringt, ist schlimmer als keiner. + * + * @returns {string|null} Grund -- oder null, wenn es geht. */ +export function darfFeldAnfassen(person, pfad, rolle) { + const wer = person?.rolle; + if (wer === "admin") return null; + if (wer !== "hand") return "Umstellen kann das nur DogFather und die rechte Hand."; + + if (rolle === NUR_DOGFATHER_SPALTE) { + return "Was DogFather sieht, stellt nur er selbst um."; + } + if (pfad === NUR_DOGFATHER_ZEILE) { + return "Wer Zugänge vergeben darf, entscheidet nur DogFather."; + } + if (rolle === "hand" && HAND_BRAUCHT.includes(pfad)) { + return "Diese Seite kannst du dir nicht selbst wegnehmen – " + + "sonst kämst du an die Rechte nicht mehr heran."; + } + return null; +} + +/** Alle Felder, die diese Person nicht anfassen darf -- fuer die + * Oberflaeche, damit sie sie als fest zeichnet. + * + * ABGELEITET AUS `darfFeldAnfassen`, nicht danebengeschrieben: Zwei + * Listen fuer dieselbe Regel waeren die, die beim naechsten Umbau + * auseinanderlaufen -- und die in der Oberflaeche waere die, die + * jeder herunterlaedt. */ +export function festeFelder(person, pfad) { + const aus = UNANTASTBAR.filter((u) => u.pfad === pfad).map((u) => u.rolle); + for (const r of ALLE_ROLLEN) { + if (aus.includes(r)) continue; + if (darfFeldAnfassen(person, pfad, r)) aus.push(r); + } + return aus; +} + /** @returns {string|null} Grund, warum das nicht geht -- oder null. */ -export function darfUmstellen(pfad, rolle, erlaubt) { +export function darfUmstellen(pfad, rolle, erlaubt, person) { if (!Object.prototype.hasOwnProperty.call(SEITEN, pfad)) return "Diese Seite gibt es nicht."; if (!ALLE_ROLLEN.includes(rolle)) return "Diese Rolle gibt es nicht."; + /* ZUERST DIE PERSON, DANN DER INHALT. Wer ein Feld gar nicht + anfassen darf, bekommt denselben Grund -- egal ob er an- oder + abwaehlen wollte. Stuende diese Frage nach `if (erlaubt) return + null`, koennte die rechte Hand DogFather jede Seite ZUSCHALTEN + und nur nicht wegnehmen; das waere dieselbe Tuer, nur andersherum. */ + const nein = person ? darfFeldAnfassen(person, pfad, rolle) : null; + if (nein) return nein; if (erlaubt) return null; const fest = UNANTASTBAR.find((u) => u.pfad === pfad && u.rolle === rolle); if (fest) { diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 0085ee5e..bfd045cc 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -153,7 +153,43 @@ function nurAdmin(req, res, next) { const codeWeg = req.method === "POST" && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/code$/ .test(req.baseUrl + req.path); - if (person.rolle === "hand" && (anlegeWeg || codeWeg)) { + /* SPERREN UND LOESCHEN KOMMEN DAZU (24.09.2026). + + Filipe: „die rechte hand soll das auch sehen. und die selben + rechte da haben wie dogfather. das einzige was sie nicht kann ist + die dogfather rolle oder leute anfassen. also da kann sie nichts + verändern." + + „Das einzige" ist der Punkt. Bis heute blieben Sperren und + Loeschen ausdruecklich bei DogFather (siehe der Absatz darueber, + 22.09.) -- das ist damit ueberholt. + + WAS SIE WEITERHIN NICHT KANN, steht nicht hier, sondern in den + Routen: An DogFather und Managern aendert nur DogFather etwas + (`ziel.rolle === "admin" || "manager"`), und welche Rollen sie + ueberhaupt anfassen darf, sagt ANLEGBAR. Diese Tuer oeffnet nur + den Weg -- wen er trifft, entscheidet die Route dahinter. + + LOESCHEN IST ENDGUELTIG, und der Hausgrundsatz „Nur DogFather hat + alle endgueltigen Rechte" stand dem entgegen. Filipes Ansage ist + juenger und ausdruecklich; die Sicherungen, die wirklich zaehlen + (nie den letzten DogFather, nie an einem Admin), greifen + unabhaengig davon. */ + const sperrWeg = req.method === "PATCH" + && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+$/ + .test(req.baseUrl + req.path); + const loeschWeg = req.method === "DELETE" + && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+$/ + .test(req.baseUrl + req.path); + /* DIE VORSCHAU GEHOERT ZUM LOESCHEN. Sie sagt, wie viel an einer + Person haengt -- wer loeschen darf, muss sie sehen, sonst + entscheidet er blind. Vergessen beim ersten Anlauf, gefunden von + pruef-hand-personen ("sie sieht die Loesch-Vorschau: 404"). */ + const vorschauWeg = req.method === "GET" + && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/loeschbar$/ + .test(req.baseUrl + req.path); + if (person.rolle === "hand" + && (anlegeWeg || codeWeg || sperrWeg || loeschWeg || vorschauWeg)) { req.person = person; return next(); } @@ -947,8 +983,18 @@ personenRouter.post("/workspace/api/verwaltung/personen/:id/code", gleicheHerkun ohne es zu merken. Wer einen neuen Code will, hat den Knopf daneben. ===================================================================== */ +/* `nurDogFatherBeiLeitung` FEHLTE HIER (ergaenzt 24.09.2026, gefunden + von pruef-hand-personen). + + Bis heute war das ohne Folgen: Die Route hat eigene Pruefungen, und + wer sie ueberhaupt erreichte, war DogFather oder jemand mit + `darfRollenWechseln`. Gemessen hat die neue Pruefung aber, dass die + rechte Hand darueber die Rolle eines MANAGERS aendern konnte -- + waehrend derselbe Manager fuer DogFather auf dieser Adresse gar + nicht existiert. Zwei Wege, zwei Antworten, und der laxere war der + fuer die Rolle mit weniger Rechten. */ personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkunft, - (req, res) => { + nurDogFatherBeiLeitung, (req, res) => { try { const id = Number(req.params.id); const rolle = String(req.body?.rolle ?? ""); @@ -1209,22 +1255,39 @@ function anhang(id) { Steht getrennt, damit Vorschau und Löschen GARANTIERT dieselbe Antwort geben. Zwei Stellen mit derselben Regel laufen irgendwann auseinander. */ function darfGeloeschtWerden(person, akteur) { - if (!istDogFather(akteur)) return "Löschen darf nur DogFather."; + /* DIE RECHTE HAND DARF SEIT DEM 24.09.2026 AUCH LOESCHEN. + Filipe: „die selben rechte da haben wie dogfather. das einzige + was sie nicht kann ist die dogfather rolle oder leute anfassen." + WEN sie nicht anfassen darf, entscheidet `nurDogFatherBeiLeitung` + vor dieser Funktion -- DogFather, Manager und alles ausserhalb + von ANLEGBAR. Hier bleiben die Sicherungen, die fuer JEDEN + gelten. */ + if (!istDogFather(akteur) && akteur?.rolle !== "hand") { + return "Löschen darf nur DogFather und die rechte Hand."; + } if (person.id === akteur.id) return "Dich selbst kannst du nicht löschen."; if (istDogFather(person)) { + /* DOPPELT, UND DAS IST ABSICHT: `nurDogFatherBeiLeitung` haelt die + rechte Hand schon vorher auf. Diese Zeile gilt DogFather selbst + -- der letzte Zugang bleibt, auch wenn er es selbst versucht. */ const { n } = db().prepare("SELECT COUNT(*) AS n FROM personen WHERE rolle = 'admin'").get(); if (n <= 1) return "Das ist der letzte DogFather-Zugang."; } return null; } -personenRouter.get("/workspace/api/verwaltung/personen/:id/loeschbar", (req, res) => { +personenRouter.get("/workspace/api/verwaltung/personen/:id/loeschbar", + nurDogFatherBeiLeitung, (req, res) => { try { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); /* Wer gar nicht löschen darf, sieht auch die Vorschau nicht -- sie - verrät, wie viel an einer Person hängt. */ - if (!istDogFather(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + verrät, wie viel an einer Person hängt. Seit dem 24.09.2026 + gehoert die rechte Hand dazu; WEN sie sehen darf, regelt + `nurDogFatherBeiLeitung` auf dieser Route. */ + if (!istDogFather(req.person) && req.person?.rolle !== "hand") { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } const person = db().prepare("SELECT id, name, rolle, aktiv FROM personen WHERE id = ?").get(id); if (!person) return res.status(404).json({ fehler: "nicht_gefunden" }); @@ -1240,11 +1303,19 @@ personenRouter.get("/workspace/api/verwaltung/personen/:id/loeschbar", (req, res } }); -personenRouter.delete("/workspace/api/verwaltung/personen/:id", gleicheHerkunft, (req, res) => { +/* `nurDogFatherBeiLeitung` STAND HIER NICHT -- und das war bis heute + harmlos, weil die Route eine Zeile weiter unten ohnehin nur + DogFather durchliess. Seit die rechte Hand loeschen darf, ist es + die Stelle, an der DogFather und die Manager geschuetzt werden. + Ohne diese Middleware koennte sie DogFather loeschen. */ +personenRouter.delete("/workspace/api/verwaltung/personen/:id", gleicheHerkunft, + nurDogFatherBeiLeitung, (req, res) => { try { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); - if (!istDogFather(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!istDogFather(req.person) && req.person?.rolle !== "hand") { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } const person = db().prepare("SELECT id, name, rolle, aktiv FROM personen WHERE id = ?").get(id); if (!person) return res.status(404).json({ fehler: "nicht_gefunden" }); diff --git a/server/workspace-rechte.js b/server/workspace-rechte.js index 6274f7ae..d52828e2 100644 --- a/server/workspace-rechte.js +++ b/server/workspace-rechte.js @@ -60,7 +60,8 @@ import { } from "./workspace.js"; import { SEITEN, ALLE_ROLLEN, OHNE_ANMELDUNG, UNANTASTBAR, - tafelSetzen, tafelJetzt, darfUmstellen, alleSeiten, + tafelSetzen, tafelJetzt, darfUmstellen, alleSeiten, festeFelder, + darfFeldAnfassen, } from "./rechte.js"; export const rechteRouter = express.Router(); @@ -149,11 +150,36 @@ function nurLeitung(req, res, next) { return res.status(404).json({ fehler: "nicht_gefunden" }); } -/** Umstellen: nur DogFather. */ +/** Umstellen: DogFather und die rechte Hand (24.09.2026). + * + * Filipe: „die selben rechte da haben wie dogfather. das einzige was + * sie nicht kann ist die dogfather rolle oder leute anfassen." + * + * WAS SIE NICHT DARF, STEHT NICHT HIER, sondern in + * `darfFeldAnfassen` (rechte.js) -- an genau einer Stelle, aus der + * auch die Oberflaeche ihre Auskunft zieht. Dieser Waechter laesst + * nur die beiden Rollen ueberhaupt herein; welches FELD sie anfassen + * duerfen, entscheidet die Route darunter. */ +function nurLeitungUmstellen(req, res, next) { + if (req.person?.rolle === "admin" || req.person?.rolle === "hand") return next(); + return res.status(403).json({ + fehler: "Umstellen kann das nur DogFather und die rechte Hand. " + + "Ansehen darfst du alles.", + }); +} + +/** Alles zurueck auf den Grundstand: weiterhin nur DogFather. + * + * UND DAS IST ABSICHT, keine Vergesslichkeit. Ein Knopf, der die + * ganze Tafel zuruecksetzt, wuerde auch Aenderungen an der Spalte + * „DogFather" und an der Zeile „Personen & Zugaenge" mitnehmen -- + * genau die zwei Dinge, die die rechte Hand nicht anfassen soll. + * Eine Sperre, die ein zweiter Knopf daneben aufhebt, ist keine. */ function nurDogFather(req, res, next) { if (req.person?.rolle === "admin") return next(); return res.status(403).json({ - fehler: "Umstellen kann das nur DogFather. Ansehen darfst du alles.", + fehler: "Alles zurücksetzen kann nur DogFather – es würde auch " + + "Einstellungen mitnehmen, die dir nicht offenstehen.", }); } @@ -179,11 +205,35 @@ rechteRouter.get("/workspace/api/rechte/tafel", (req, res) => { /* Was DogFather hier NICHT abwaehlen kann -- damit die Oberflaeche den Knopf gar nicht erst als Knopf zeigt statt ihn eine Absage bringen zu lassen. */ - fest: UNANTASTBAR.filter((u) => u.pfad === pfad).map((u) => u.rolle), + /* WAS DIESE PERSON NICHT ANFASSEN DARF -- nicht nur, was + allgemein festliegt (24.09.2026). Fuer DogFather unveraendert + UNANTASTBAR; die rechte Hand bekommt zusaetzlich die Spalte + „DogFather", die Zeile „Personen & Zugaenge" und ihre eigenen + zwei Zugaenge als fest gezeichnet. + + Abgeleitet aus derselben Funktion, die die Route gleich + darunter befragt -- damit die Oberflaeche nie einen Knopf + zeigt, den der Server danach ablehnt. */ + fest: festeFelder(req.person, pfad), + /* UND WARUM. Ein festes Feld ohne Grund ist eine graue Fläche, + bei der man sich fragt, ob etwas kaputt ist. Der Text kommt + aus derselben Funktion, die das Feld festgesetzt hat -- nicht + aus einer Liste in der Oberfläche, die beim nächsten neuen + Grund nicht mitwächst. */ + fest_grund: Object.fromEntries(ALLE_ROLLEN + .map((r) => [r, darfFeldAnfassen(req.person, pfad, r) + || (UNANTASTBAR.some((u) => u.pfad === pfad && u.rolle === r) + ? "Diese Seite kannst du dir nicht selbst wegnehmen – sonst käme " + + "niemand mehr an die Rechte, an die Zugänge oder auf die Startseite." + : null)]) + .filter(([, grund]) => grund)), })); res.json({ - darf_aendern: req.person.rolle === "admin", + darf_aendern: req.person.rolle === "admin" || req.person.rolle === "hand", + /* Zuruecksetzen bleibt bei DogFather -- die Oberflaeche zeigt + den Knopf deshalb nur ihm. */ + darf_zuruecksetzen: req.person.rolle === "admin", rollen: ALLE_ROLLEN.map((r) => ({ schluessel: r, name: ROLLEN_NAME[r] ?? r, @@ -209,13 +259,16 @@ rechteRouter.get("/workspace/api/rechte/tafel", (req, res) => { Aenderung verliert -- und bei Rechten ist eine halbe Aenderung die gefaehrlichste Lage von allen. Zurueckgenommen wird mit demselben Klick. */ -rechteRouter.put("/workspace/api/rechte/feld", gleicheHerkunft, nurDogFather, (req, res) => { +rechteRouter.put("/workspace/api/rechte/feld", gleicheHerkunft, nurLeitungUmstellen, (req, res) => { try { const pfad = String(req.body?.pfad || ""); const rolle = String(req.body?.rolle || ""); const erlaubt = req.body?.erlaubt === true; - const nein = darfUmstellen(pfad, rolle, erlaubt); + /* DIE PERSON GEHT MIT. Ohne sie prueft `darfUmstellen` nur die + allgemeinen Sperren -- und die rechte Hand koennte DogFathers + Spalte aendern. */ + const nein = darfUmstellen(pfad, rolle, erlaubt, req.person); if (nein) return res.status(400).json({ fehler: nein }); const imGrundstand = (SEITEN[pfad] || []).includes(rolle); diff --git a/server/workspace.js b/server/workspace.js index a66f6b94..28f7108a 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -6247,6 +6247,31 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { sich niemand zu "Community" machen liess. */ rollen_anfassbar: [...ROLLEN].filter((r) => darfRolleAendern(person, { id: -1, rolle: r })), + /* DARF ICH ZUGAENGE VERWALTEN? (24.09.2026) + + Filipe: „die rechte hand soll das auch sehen. und die selben + rechte da haben wie dogfather. das einzige was sie nicht kann + ist die dogfather rolle oder leute anfassen." + + WARUM DAS HIER STEHT UND NICHT IN personen.js: Dort stand + `if (ich.rolle === 'hand') { keine Knoepfe }` -- mit der + Begruendung „Der Server antwortet ihr auf jeden davon mit 404". + Das stimmte am 22.09., als sie nur anlegen und Codes erzeugen + durfte. Seither wurde der Server zweimal erweitert und die + Oberflaeche nie nachgezogen: Sie durfte Rollen aendern und + Codes erzeugen, sah aber keinen einzigen Knopf. Eine + Rollenabfrage im Browser ist genau die zweite Wahrheit, die + still veraltet. + + `darfAnlegen(person).length > 0` ist die Ableitung, nicht eine + Liste: Wer ueberhaupt jemanden anlegen darf, darf auch mit ihm + weiterarbeiten. Fuer DogFather und die rechte Hand ist sie + nicht leer, fuer alle anderen schon. */ + darf_zugaenge_verwalten: darfAnlegen(person).length > 0, + /* UND LOESCHEN -- getrennt, weil es das Einzige ist, was sich nicht + zuruecknehmen laesst. Heute dieselbe Antwort; sollte Filipe es + spaeter wieder einschraenken, ist hier die Stelle. */ + darf_personen_loeschen: person?.rolle === "admin" || person?.rolle === "hand", /* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026). Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 5e1643d0..ff5ee714 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`. --> - - + + - +