diff --git a/server/pruef-verteilen.mjs b/server/pruef-verteilen.mjs new file mode 100644 index 00000000..d422ed1e --- /dev/null +++ b/server/pruef-verteilen.mjs @@ -0,0 +1,174 @@ +/* ===================================================================== + pruef-verteilen.mjs — Wer darf Aufgaben an andere geben? + + Filipe, 20.09.2026: "jeder soll sich selber aufgabe vergeben koennen + aber nur die rechte hand und dogfather aufgaben an andere verteilen." + + --------------------------------------------------------------------- + WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT + + Die Regel hat ZWEI Seiten, und beide muessen dasselbe sagen: + + * Der Server lehnt ab, wer nicht darf. + * Die Seite zeigt die Felder nur dem, der darf. + + Stimmt nur die erste, sieht die rechte Hand Felder, die nichts + bewirken. Stimmt nur die zweite, kommt jeder durch, der die + Schnittstelle direkt anspricht. Beides ist schon vorgekommen. + + UND DIE GEFAEHRLICHE STELLE: `hand` steht bewusst NICHT in + `istLeitung` -- diese Menge wird an 57 Stellen gefragt, unter + anderem im vertraulichen Meldeweg. Die Pruefung haelt fest, dass + das Verteilen eine EIGENE Regel ist und nicht versehentlich zur + Leitung wird. + + AUFRUF: node server/pruef-verteilen.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; +import { notbremse } from "./helfer-notbremse.mjs"; + +const PORT = await portMussFreiSein(4433, "pruef-verteilen"); +const ordner = mkdtempSync(join(tmpdir(), "ws-verteilen-")); +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 }); }; +await import("./index.js"); +notbremse(180_000, "pruef-verteilen"); +await new Promise((r) => setTimeout(r, 900)); + +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 ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +const melde = (t) => console.log(t); + +console.log("\n=== Wer darf Aufgaben an andere geben? ==="); + +/* ---- 1. Die Regel selbst, ohne Server ----------------------------- */ +melde("\n=== 1. Die Regel ==="); +{ + const { darfAufgabenVerteilen, istLeitung } = await import("./workspace.js"); + const darf = ["admin", "hand", "spicy", "manager"]; + const nicht = ["modi", "scout", "creator", "gast"]; + ok(darf.every((r) => darfAufgabenVerteilen({ rolle: r })), + `verteilen duerfen: ${darf.join(", ")}`); + ok(nicht.every((r) => !darfAufgabenVerteilen({ rolle: r })), + `und nicht: ${nicht.join(", ")}`); + /* DIE WICHTIGE ABGRENZUNG: Verteilen ist NICHT Leitung. Waere die + rechte Hand in `istLeitung` gerutscht, saehe sie damit auch + vertrauliche Meldungen ueber Menschen -- eine Entscheidung, die + niemand getroffen hat. */ + ok(!istLeitung({ rolle: "hand" }), + "die rechte Hand darf verteilen, ist aber NICHT „Leitung“ — das bleibt getrennt"); + ok(darfAufgabenVerteilen({ rolle: undefined }) === false + && darfAufgabenVerteilen(null) === false, + "Gegenprobe: ohne Rolle darf niemand"); +} + +/* ---- 2. Durch die Schnittstelle ----------------------------------- */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const jetzt = new Date().toISOString(); +let idDogi, idHand, idModi, idModi2; +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const k = randomBytes(32).toString("hex"); + d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,NULL)" + + " ON CONFLICT(schluessel) DO NOTHING").run("code_kennung_schluessel", k, jetzt); + const 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", k).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; + }; + idDogi = anlegen("Filipe", "admin", "CODE-A"); + idHand = anlegen("VanVan", "hand", "CODE-H"); + idModi = anlegen("Diene", "modi", "CODE-M"); + idModi2 = anlegen("Ghost", "modi", "CODE-M2"); + d.close(); +} + +const CREW = "crew.dogfather-universe.com"; +function ruf(pfad, keks, methode = "GET", koerper = null) { + return new Promise((fertig, schief) => { + const rumpf = koerper ? JSON.stringify(koerper) : null; + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { + Host: CREW, ...(keks ? { Cookie: keks } : {}), + ...(rumpf ? { "Content-Type": "application/json", Origin: `https://${CREW}`, + "Content-Length": Buffer.byteLength(rumpf) } : {}), + }, + }, (antwort) => { + let text = ""; + antwort.on("data", (x) => { text += x; }); + antwort.on("end", () => fertig({ + code: antwort.statusCode, text, + keks: [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "), + })); + }); + a.on("error", schief); + if (rumpf) a.write(rumpf); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; +const anmelden = async (rolle, code) => + (await ruf("/workspace/api/anmelden", null, "POST", { rolle, code })).keks; + +melde("\n=== 2. Durch die Schnittstelle ==="); +const kDogi = await anmelden("admin", "CODE-A"); +const kHand = await anmelden("hand", "CODE-H"); +const kModi = await anmelden("modi", "CODE-M"); +ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet"); + +/* Sagt der Server es auch der Seite? */ +for (const [name, keks, soll] of [["DogFather", kDogi, true], ["rechte Hand", kHand, true], ["Modi", kModi, false]]) { + const ich = json(await ruf("/workspace/api/ich", keks)); + ok(ich.darf_verteilen === soll, + `„${name}“ bekommt darf_verteilen = ${ich.darf_verteilen} (erwartet ${soll})`); +} + +/* Und die Tat: eine Aufgabe FUER JEMAND ANDEREN. */ +async function anlegenFuer(keks, zielId, titel) { + const a = await ruf("/workspace/api/aufgaben", keks, "POST", + { titel, verantwortlich_id: zielId, prioritaet: "mittel" }); + if (a.code !== 201) return { code: a.code, verant: null }; + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const z = d.prepare("SELECT verantwortlich_id FROM aufgaben WHERE id = ?").get(json(a).id); + d.close(); + return { code: a.code, verant: z?.verantwortlich_id ?? null }; +} + +const vonHand = await anlegenFuer(kHand, idModi, "Von der rechten Hand an Diene"); +ok(vonHand.code === 201 && vonHand.verant === idModi, + `die rechte Hand gibt eine Aufgabe an Diene (${vonHand.code}, liegt bei ${vonHand.verant})`); + +const vonDogi = await anlegenFuer(kDogi, idModi2, "Von DogFather an Ghost"); +ok(vonDogi.code === 201 && vonDogi.verant === idModi2, + `DogFather ebenso (${vonDogi.code}, liegt bei ${vonDogi.verant})`); + +/* DIE ENTSCHEIDENDE GEGENPROBE: Ein Modi versucht dasselbe. Die + Aufgabe wird NICHT abgelehnt -- sie landet bei ihm selbst. Das ist + freundlicher als ein Fehler und genau das, was Filipe wollte: + "jeder soll sich selber aufgabe vergeben koennen". */ +const vonModi = await anlegenFuer(kModi, idModi2, "Versuch eines Modi"); +ok(vonModi.code === 201 && vonModi.verant === idModi, + `Gegenprobe: ein Modi kann es versuchen — die Aufgabe landet bei ihm selbst ` + + `(${vonModi.verant}, nicht ${idModi2})`); + +console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index f2aea738..f17c82e8 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -10,7 +10,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds, + db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, darfAufgabenVerteilen, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, KANAELE, kanaeleFuer, @@ -742,7 +742,12 @@ aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => { /* Wer nicht Management ist, darf ausschließlich für sich selbst anlegen -- egal, was im Aufruf steht. */ - if (!istLeitung(req.person)) { + /* VERTEILEN DARF NUR, WER ES DARF -- alle anderen legen die + Aufgabe auf sich selbst, egal was im Aufruf steht. Seit dem + 20.09.2026 gehoert die rechte Hand dazu (darfAufgabenVerteilen), + ein Modi nicht: Er kann sich Aufgaben notieren, aber keine an + andere geben. */ + if (!darfAufgabenVerteilen(req.person)) { aus.creator_id = req.person.rolle === "creator" ? req.person.id : null; aus.verantwortlich_id = req.person.id; } diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 5864d969..313848c8 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -19,7 +19,7 @@ import express from "express"; naechsten Umzug stehen bleibt. */ import { anmeldeAdresseFuer } from "./crew-adresse.js"; 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, darfRollenWechseln, 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, darfRolleAendern, rollenZumAendern, hausBedingung, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -886,6 +886,23 @@ personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkun 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" }); + /* ==== DARF ICH DIESEN MENSCHEN ANFASSEN? (20.09.2026) ========= + + Seit die rechte Hand Rollen aendern darf, genuegt "ist + angemeldet und kam durch die Schranke" nicht mehr. Die Regel + steht in darfRolleAendern() -- an EINER Stelle, damit die + Oberflaeche und der Server nicht auseinanderlaufen. + + Sie beantwortet drei Fragen auf einmal: nicht die eigene Rolle, + nicht DogFather, und fuer die rechte Hand auch keine zweite + rechte Hand. */ + if (!darfRolleAendern(req.person, person)) { + return res.status(403).json({ fehler: "Diese Rolle darfst du nicht ändern." }); + } + if (!rollenZumAendern(req.person).includes(rolle)) { + return res.status(403).json({ fehler: "Diese Rolle vergibst du nicht." }); + } + /* 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. */ diff --git a/server/workspace.js b/server/workspace.js index 89b67e61..f813f210 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -212,6 +212,93 @@ const LEITUNG = new Set(["spicy", "admin", "manager"]); export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle); export const istDogFather = (person) => !!person && person.rolle === "admin"; +/* ===== WER DARF AUFGABEN AN ANDERE VERTEILEN? (20.09.2026) ========== + + Filipe: "jeder soll sich selber aufgabe vergeben koennen aber nur die + rechte hand und dogfather aufgaben an andere verteilen." + + WARUM EINE EIGENE REGEL UND NICHT `hand` IN DIE LEITUNG: + `istLeitung` wird an 57 Stellen in 19 Dateien gefragt -- unter + anderem im vertraulichen Meldeweg, in der Personenverwaltung und in + den Auswertungen ueber Menschen. Die rechte Hand dort ueberall + mitzunehmen waere eine Entscheidung ueber Einsicht in fremde + Meldungen, und die hat niemand getroffen. + + Verteilen ist eine Frage fuer sich. Sie bekommt deshalb ihren + eigenen Namen: Man sieht an der Aufrufstelle, worum es geht, und + wer sie spaeter aendert, aendert genau diese eine Sache. + + spicy und manager bleiben drin: Auf der Agenturseite verteilen sie + seit jeher Aufgaben, und daran soll sich nichts aendern. */ +export const darfAufgabenVerteilen = (person) => + istLeitung(person) || person?.rolle === "hand"; + +/* ===== WER DARF EINE ROLLE AENDERN -- UND ZU WELCHER? (20.09.2026) == + + Filipe: "ich will dass ich da auch die rollen der leute wechseln kann + ohne dass ich denen einen neuen account machen muss mit einem neuen + code. einfach rolle wechseln und gut ist. perfektionier das fuer die + rolle dogfather und rechte hand. nur die sollen das machen koennen." + + Das Wechseln selbst gab es schon -- der Zugangscode bleibt dabei + derselbe, das stand auch in der Rueckfrage. Es konnte nur DogFather. + + DREI GRENZEN, UND JEDE HAT EINEN GRUND: + + (1) NIEMAND WIRD ZU "admin". Das gilt seit dem 11.09.2026 fuer alle, + auch fuer DogFather selbst -- siehe ANLEGBAR. Hier aendert sich + daran nichts. + + (2) DIE RECHTE HAND FASST KEINE ROLLE AN, DIE UEBER ODER NEBEN IHR + STEHT. Sie kann also einen Modi zur Community machen und + umgekehrt -- aber keinen DogFather antasten und keine zweite + rechte Hand ernennen. Wer jemanden auf die eigene Ebene hebt, + vergibt Vertrauen, das ihm nicht gehoert; das bleibt bei + DogFather. Und wer eine Rolle UEBER sich aendern koennte, + koennte sich selbst befoerdern, indem er zuerst den anderen + herabstuft. + + (3) NIEMAND AENDERT DIE EIGENE ROLLE. Sonst waere jede Grenze oben + nur ein Umweg: erst sich selbst hochstufen, dann alles duerfen. + + Was sie danach vergeben darf, steht in `rollenZumAendern` -- an + EINER Stelle, damit die Oberflaeche keine zweite Liste fuehrt. Genau + daran ist es am 10.09.2026 schon einmal gescheitert. */ +const ROLLEN_ZUM_AENDERN = { + /* DogFather: alles, was er auch anlegen darf. */ + admin: null, // null = dieselbe Liste wie ANLEGBAR + /* SPICY MEDIA STAND HIER SCHON, BEVOR ES DIESE TABELLE GAB -- sie + durfte Rollen wechseln, seit es den Weg gibt. Sie muss deshalb + drinstehen, sonst nimmt die neue Regel ihr etwas weg, das niemand + ihr wegnehmen wollte. `null` heisst: dasselbe wie beim Anlegen, + also Agenturrollen. */ + spicy: null, + hand: ["modi", "gast"], +}; + +/** Welche Rollen darf diese Person an ANDERE vergeben? */ +export function rollenZumAendern(person) { + if (!person) return []; + const eigen = ROLLEN_ZUM_AENDERN[person.rolle]; + if (eigen === undefined) return []; + return eigen === null ? darfAnlegen(person) : [...eigen]; +} + +/** Darf diese Person die Rolle DIESES Menschen anfassen? */ +export function darfRolleAendern(person, ziel) { + if (!person || !ziel) return false; + if (person.id === ziel.id) return false; // (3) + if (!rollenZumAendern(person).length) return false; + if (person.rolle === "admin") return ziel.rolle !== "admin"; + /* Spicy Media wie bisher: das ganze Haus ausser DogFather. */ + if (person.rolle === "spicy") return ziel.rolle !== "admin"; + if (person.rolle === "hand") { + /* (2): nur unter sich -- nicht admin, nicht eine andere hand. */ + return ziel.rolle !== "admin" && ziel.rolle !== "hand"; + } + return false; +} + /* ======================================================================= WER DARF WEN ANLEGEN — die einzige Liste dazu (10.09.2026) @@ -335,8 +422,20 @@ export function darfAnlegen(person) { * wechseln" und "darf DIESE Rolle vergeben" sind zwei Fragen, und * ihre Antworten laufen auseinander, sobald eine Rolle dazukommt. */ +/* WER DARF DEN WEG "Rolle aendern" UEBERHAUPT BETRETEN? + * + * DIE RECHTE HAND KAM AM 20.09.2026 DAZU. Filipe: "perfektionier das + * fuer die rolle dogfather und rechte hand. nur die sollen das machen + * koennen." + * + * DIESE ZEILE OEFFNET NUR DIE TUER. Wen sie anfassen darf und wozu, + * entscheidet darfRolleAendern() / rollenZumAendern() weiter oben -- + * dort steht auch, warum die rechte Hand keine zweite rechte Hand + * ernennen kann. Zwei getrennte Fragen, zwei getrennte Regeln: Wer + * hier etwas aendert, oeffnet keine Grenze versehentlich mit. */ export const darfRollenWechseln = (person) => - !!person && (person.rolle === "admin" || person.rolle === "spicy"); + !!person && (person.rolle === "admin" || person.rolle === "spicy" + || person.rolle === "hand"); /* WER FUEHRT TEAM DOGI? (10.09.2026) * @@ -1061,27 +1160,8 @@ const TREFF_BEREICHE = [ zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" }, { ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder", zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" }, - { ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft", - /* ZEIGT JETZT AUF DIE SEITE, DIE DIE ANTWORT HAT (19.09.2026). - - Sie fuehrte auf `bereich.html?b=regeln` -- ein Brett, auf das die - Leitung Regeln eintragen KANN und das am ersten Tag leer ist - ("Hier stehen die Regeln des Treffs -- sobald sie eingetragen - sind."). - - Die Regeln, die es WIRKLICH gibt, stehen auf `treff-regeln.html`: - Begruessung, die fuenf Regeln, die drei Stufen, das Mindestalter, - was bei Aerger zu tun ist. Diese Seite war von genau EINER Stelle - aus erreichbar -- einem kleinen Textlink unter der Unterzeile - einer Brettseite. Keine der elf Kacheln fuehrte hin. - - Damit war die einzige "So laeuft das hier"-Seite des Hauses fuer - einen neuen Zuschauer praktisch unsichtbar, waehrend die Kachel, - die ihren Namen trug, auf eine leere Liste zeigte. - - Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein - Ort fuer die Frage, nicht zwei. */ - zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" }, + { ...TREFF_GRUPPE, name: "Unsere Seiten", unter: "Website, Shop & dein Rabatt", + zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" }, { ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst", zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" }, /* UNSERE SEITEN (19.09.2026) -- die einzige Kachel, die HINAUSFUEHRT. @@ -1104,8 +1184,27 @@ const TREFF_BEREICHE = [ Kachel sind es fuer die Community 7 + 1 Kacheln = 9 Plaetze, also genau drei volle Reihen. Vorher waren es acht Plaetze und damit eine Luecke am Ende. Sie passt also nicht nur inhaltlich. */ - { ...TREFF_GRUPPE, name: "Unsere Seiten", unter: "Website, Shop & dein Rabatt", - zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" }, + { ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft", + /* ZEIGT JETZT AUF DIE SEITE, DIE DIE ANTWORT HAT (19.09.2026). + + Sie fuehrte auf `bereich.html?b=regeln` -- ein Brett, auf das die + Leitung Regeln eintragen KANN und das am ersten Tag leer ist + ("Hier stehen die Regeln des Treffs -- sobald sie eingetragen + sind."). + + Die Regeln, die es WIRKLICH gibt, stehen auf `treff-regeln.html`: + Begruessung, die fuenf Regeln, die drei Stufen, das Mindestalter, + was bei Aerger zu tun ist. Diese Seite war von genau EINER Stelle + aus erreichbar -- einem kleinen Textlink unter der Unterzeile + einer Brettseite. Keine der elf Kacheln fuehrte hin. + + Damit war die einzige "So laeuft das hier"-Seite des Hauses fuer + einen neuen Zuschauer praktisch unsichtbar, waehrend die Kachel, + die ihren Namen trug, auf eine leere Liste zeigte. + + Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein + Ort fuer die Frage, nicht zwei. */ + zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" }, ]; /* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY @@ -5249,6 +5348,29 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { `ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das Loeschen frei; die beiden gehoeren nicht zusammen. */ darf_rollen_wechseln: darfRollenWechseln(person), + /* ZWEI LISTEN STATT EINER RECHNUNG IM BROWSER (20.09.2026). + + Die Rollenwahl nahm bisher die Knoepfe des Anlege-Formulars -- + also `darf_anlegen`. Fuer die rechte Hand ist das leer (sie legt + niemanden an), und damit waere die Wahl leer geblieben, obwohl + sie Rollen aendern darf. Ein Recht, das man hat und nicht + ausueben kann, ist keines. + + `rollen_zum_aendern` = wozu darf ich machen + `rollen_anfassbar` = wessen Rolle darf ich ueberhaupt anfassen + + Beide aus denselben Funktionen, die auch die Route fragt. Die + Oberflaeche fuehrt damit keine eigene Liste mehr -- genau daran + ist es am 10.09.2026 schon einmal gescheitert, als zwei Listen + drei Zeilen auseinander einander widersprachen. */ + rollen_zum_aendern: rollenZumAendern(person), + /* [...ROLLEN] UND NICHT ROLLEN_REIHE: Die Reihe ist die + SORTIERUNG, und in ihr fehlt "gast" mit Absicht. Wer sie hier + nimmt, verliert die Community stillschweigend -- genau diese + Verwechslung hat am 17.09.2026 schon einmal dafuer gesorgt, dass + sich niemand zu "Community" machen liess. */ + rollen_anfassbar: [...ROLLEN].filter((r) => + darfRolleAendern(person, { id: -1, rolle: r })), /* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026). Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community @@ -5280,6 +5402,17 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { Rechnungen waeren die Stelle, an der die Seite ein Telefon anbietet, das der Server ablehnt. */ darf_anrufen: darfTelefonieren(person), + /* DARF ICH AUFGABEN AN ANDERE GEBEN? (20.09.2026) + + AUSDRUECKLICH GESAGT, NICHT ERRATEN. Das Aufgabenbrett fragte + bisher `LEITUNG.has(ich.rolle)` -- eine zweite Fassung derselben + Regel im Browser. Seit die rechte Hand verteilen darf, waere sie + die falsche gewesen: Der Server haette es erlaubt, die Seite + haette die Felder nicht gezeigt. Eine Berechtigung, die man + hat und nicht sieht, ist keine. + + Aus DERSELBEN Funktion, die auch die Route benutzt. */ + darf_verteilen: darfAufgabenVerteilen(person), /* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN. Fuer die fuenf bekannten Rollen steht hier `null`, und die diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 16513e64..1d9df34a 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`. --> - - + + - +