diff --git a/server/pruef-rechtetafel.mjs b/server/pruef-rechtetafel.mjs new file mode 100644 index 00000000..00373810 --- /dev/null +++ b/server/pruef-rechtetafel.mjs @@ -0,0 +1,280 @@ +/* ===================================================================== + DIE RECHTETAFEL — die Pruefung, die aus einer Absicht eine Garantie + macht (11.09.2026) + + Filipe: + + "egal welche rolle oder person hinzugefuegt wird soll immer nur das + sehen wass ich erlaube. mehr nicht. soll nichts so sein dass wenn + mann eine rolle oder jemanden hinzufuegt dass er dan alles sieht." + + --------------------------------------------------------------------- + WARUM DIESE DATEI UND NICHT EIN KOMMENTAR + + In `rechte.js` stand bis heute ein Satz, der genau das Richtige + verlangte: *"Wer eine Rolle hinzufuegt, muss diese Liste durchgehen."* + Er stand seit dem 10.09. da, und am Tag darauf standen immer noch neun + Seiten auf `null`. Ein Satz, der sich auf ein Gedaechtnis verlaesst, + ist keine Sicherung. + + Diese Datei verlaesst sich auf nichts. Sie wird rot, wenn + + * eine Rolle keinen Eintrag hat, + * eine Seite keinen Eintrag hat, + * eine unbekannte Rolle irgendwo hineinkaeme, + * oder eine unbekannte Seite sich fuer irgendwen oeffnet. + + Man KANN es damit nicht mehr vergessen -- der Prueflauf laesst es + nicht zu. + + --------------------------------------------------------------------- + DREI EBENEN, UND JEDE BEANTWORTET ETWAS ANDERES + + 1. DIE TAFEL SELBST, ohne Server. Vollstaendig, schnell, und sie + vergleicht gegen das DATEISYSTEM statt gegen eine zweite Liste -- + eine zweite Liste waere die Stelle, an der beide auseinanderlaufen. + + 2. DER SERVER. Dass die Tafel richtig ist, heisst nicht, dass sie + auch gefragt wird. Genau dieser Unterschied hat am 10.09. drei + Stunden gekostet: Kachel und Schnittstelle stimmten, die Seite + warf trotzdem zurueck. Hier wird deshalb wirklich angeklopft. + + 3. DIE GEGENPROBE. Eine Pruefung, die immer bestaetigt, bestaetigt + nichts. Am Ende bekommt sie absichtlich eine kaputte Tafel und + MUSS sie als kaputt melden. + ===================================================================== */ +import { mkdtempSync, rmSync, readdirSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4397, "pruef-rechtetafel"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-rechte-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-rechtetafel"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +/* EIGENE ZUHOERER. index.js haengt fuer den Betrieb Auffangnetze an + `uncaughtException` -- eine Pruefung, die dieselbe Datei in denselben + Prozess laedt, erbt sie und wuerde nach einem Absturz weiterhaengen, + statt rot zu werden. */ +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const rechte = await import("./rechte.js"); +const w = await import("./workspace.js"); + +/* ======================================================================= + 1. DIE TAFEL IST VOLLSTAENDIG + ======================================================================= */ +melde(""); +melde("=== 1. Jede Rolle und jede Seite hat einen Eintrag ==="); + +/* DIE ROLLENLISTE DARF NICHT ZWEIMAL EXISTIEREN. + + rechte.js kennt ALLE_ROLLEN, workspace.js kennt ROLLEN. Zwei Listen + derselben Sache laufen auseinander -- die Frage ist nur wann. Hier + werden sie gegeneinandergehalten, damit das Wann "beim naechsten + Prueflauf" ist und nicht "wenn jemand hineinfaellt". */ +{ + const ausWorkspace = [...(w.ROLLEN_NAME ? Object.keys(w.ROLLEN_NAME) : [])].sort(); + const ausTafel = [...rechte.ALLE_ROLLEN].sort(); + ok(ausWorkspace.length > 0, `workspace.js kennt ${ausWorkspace.length} Rollen`); + ok(JSON.stringify(ausWorkspace) === JSON.stringify(ausTafel), + `beide Listen sind identisch (${ausTafel.join(", ")})`); +} + +/* JEDE DATEI AUF DER PLATTE BRAUCHT EINEN EINTRAG. + + Verglichen wird gegen das Dateisystem, nicht gegen eine Liste im Code. + Eine neue Seite entsteht als DATEI -- wer sie anlegt und den Eintrag + vergisst, faellt genau hier auf. */ +const SEITEN_ORDNER = new URL("../workspace/", import.meta.url).pathname + .replace(/^\/([A-Za-z]:)/, "$1"); +{ + const dateien = readdirSync(SEITEN_ORDNER) + .filter((f) => f.endsWith(".html")) + .map((f) => "/workspace/" + f).sort(); + const inTafel = new Set([...rechte.alleSeiten(), ...rechte.OHNE_ANMELDUNG]); + const fehlend = dateien.filter((p) => !inTafel.has(p)); + const ueberzaehlig = rechte.alleSeiten().filter((p) => !dateien.includes(p)); + + ok(dateien.length >= 20, `${dateien.length} Seiten liegen im Ordner`); + ok(fehlend.length === 0, + `jede davon steht in der Tafel${fehlend.length ? " -- FEHLT: " + fehlend.join(", ") : ""}`); + ok(ueberzaehlig.length === 0, + `und jeder Eintrag hat eine Datei${ueberzaehlig.length ? " -- OHNE DATEI: " + ueberzaehlig.join(", ") : ""}`); +} + +/* KEIN EINTRAG DARF UNSCHARF SEIN. `null` war der alte Fehler; eine + Rolle, die es gar nicht gibt, waere der naechste. */ +{ + const bekannt = new Set(rechte.ALLE_ROLLEN); + const kaputt = []; + for (const [pfad, rollen] of Object.entries(rechte.SEITEN)) { + if (!Array.isArray(rollen)) { kaputt.push(`${pfad}: kein Feld`); continue; } + for (const r of rollen) if (!bekannt.has(r)) kaputt.push(`${pfad}: unbekannte Rolle "${r}"`); + } + ok(kaputt.length === 0, `alle Eintraege sind Listen bekannter Rollen${kaputt.length ? " -- " + kaputt.join(" | ") : ""}`); +} + +/* ======================================================================= + 2. WAS NICHT IN DER TAFEL STEHT, IST VERBOTEN + ======================================================================= */ +melde(""); +melde("=== 2. Die Regel selbst ==="); + +const PHANTOM = "xx-gibt-es-nicht"; +{ + const alle = rechte.alleSeiten(); + const durchgelassen = alle.filter((p) => rechte.darfSeite({ rolle: PHANTOM }, p)); + ok(alle.length === 20, `${alle.length} Seiten in der Tafel`); + ok(durchgelassen.length === 0, + `eine unbekannte Rolle kommt auf KEINE davon${durchgelassen.length ? " -- durchgelassen: " + durchgelassen.join(", ") : ""}`); + + /* DIE GEGENPROBE ZUR OBIGEN ZEILE. Ohne sie waere sie auch dann + gruen, wenn darfSeite() schlicht immer nein sagt -- und dann waere + das ganze Haus zu, ohne dass diese Datei es meldet. */ + const echteWege = rechte.ALLE_ROLLEN + .map((r) => [r, alle.filter((p) => rechte.darfSeite({ rolle: r }, p)).length]); + const ohneSeite = echteWege.filter(([, n]) => n === 0).map(([r]) => r); + ok(ohneSeite.length === 0, + `und jede ECHTE Rolle kommt irgendwo hin (${echteWege.map(([r, n]) => r + ":" + n).join(" ")})`); +} + +{ + /* EINE SEITE, DIE ES NICHT GIBT. Das war das zweite Loch: `undefined` + fiel durch dieselbe Bedingung wie `null`. */ + const erfunden = rechte.ALLE_ROLLEN + .filter((r) => rechte.darfSeite({ rolle: r }, "/workspace/gibt-es-nicht.html")); + ok(erfunden.length === 0, + `eine Seite ohne Eintrag oeffnet sich fuer niemanden -- auch nicht fuer DogFather`); + ok(rechte.darfSeite(null, "/workspace/start.html") === false, + "und ohne Anmeldung erst recht nicht"); +} + +/* ======================================================================= + 3. DIE TAFEL WIRD AUCH WIRKLICH GEFRAGT + ======================================================================= */ +melde(""); +melde("=== 3. Der Server haelt sich daran ==="); + +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 schluessel = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, 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", schluessel).update(code).digest("hex"), jetzt); +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Mara", "manager", "CODE-MANA-0001"); +d.close(); + +async function anmelden(rolle, code) { + const a = await fetch(`${BASIS}/workspace/api/anmelden`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }), + }); + return a.headers.getSetCookie().map((c) => c.split(";")[0]).join("; "); +} +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +const kMana = await anmelden("manager", "CODE-MANA-0001"); + +async function hole(pfad, keks) { + const a = await fetch(BASIS + pfad, { headers: { cookie: keks }, redirect: "manual" }); + return { status: a.status, ziel: a.headers.get("location") || "" }; +} + +ok(kDogi.length > 0 && kMana.length > 0, "beide Anmeldungen haben eine Sitzung"); + +{ + const a = await hole("/workspace/automation.html", kDogi); + ok(a.status === 200, `DogFather kommt auf automation.html (${a.status})`); + const b = await hole("/workspace/automation.html", kMana); + ok(b.status === 302 && b.ziel.endsWith("start.html"), + `eine Managerin nicht -- sie landet auf der Startseite (${b.status} ${b.ziel})`); +} + +{ + /* DER EIGENTLICHE NEUE FALL: eine Seite, die es in der Tafel nicht + gibt. Vorher fiel sie durch die Schranke und traf danach auf + express.static -- also 404, was von aussen aussieht wie "gibt es + nicht", obwohl sie in Wahrheit einfach nicht geschuetzt war. Jetzt + greift die Schranke davor. */ + const a = await hole("/workspace/gibt-es-nicht.html", kDogi); + ok(a.status === 302 && a.ziel.endsWith("start.html"), + `eine Seite ohne Eintrag wird abgewiesen, nicht ausgeliefert (${a.status} ${a.ziel})`); +} + +{ + /* Und die Gegenprobe: Die geoeffneten Seiten oeffnen sich wirklich. + Sonst waere "alles abgewiesen" ebenfalls gruen. */ + let auf = 0; + for (const p of rechte.seitenFuer("admin")) { + const a = await hole(p, kDogi); + if (a.status === 200) auf++; + } + ok(auf === rechte.seitenFuer("admin").length, + `alle ${auf} Seiten der DogFather-Rolle liefern wirklich aus`); +} + +/* ======================================================================= + 4. DIE GEGENPROBE: KANN DIESE PRUEFUNG UEBERHAUPT ROT WERDEN? + ======================================================================= */ +melde(""); +melde("=== 4. Gegenprobe -- eine kaputte Tafel MUSS auffallen ==="); + +/* Dieselbe Vergleichslogik wie in Abschnitt 1, aber mit absichtlich + beschaedigten Daten. Steht ganz am Ende, damit ihr Schaden nichts + verfaelscht, was davor gemessen wurde -- die Lektion vom 09.09., als + eine zerstoerende Probe in der Mitte stand und alles danach verbog. */ +{ + const dateien = readdirSync(SEITEN_ORDNER).filter((f) => f.endsWith(".html")) + .map((f) => "/workspace/" + f); + + const luecke = new Set([...rechte.alleSeiten(), ...rechte.OHNE_ANMELDUNG]); + luecke.delete("/workspace/start.html"); + ok(dateien.some((p) => !luecke.has(p)), + "eine entfernte Seite wird als Luecke erkannt"); + + const falscheRolle = { "/workspace/x.html": ["gibt-es-nicht"] }; + const bekannt = new Set(rechte.ALLE_ROLLEN); + const gefunden = Object.values(falscheRolle)[0].some((r) => !bekannt.has(r)); + ok(gefunden, "eine unbekannte Rolle in einem Eintrag wird erkannt"); + + const alsNull = { "/workspace/x.html": null }; + ok(!Array.isArray(Object.values(alsNull)[0]), + "und ein `null`-Eintrag wird als kein Feld erkannt"); +} + +/* Der Server haelt die Datenbankdatei offen -- unter Windows laesst + sie sich dann nicht loeschen. Das ist ein Aufraeumrest im + Temp-Ordner, kein Befund, und darf die Pruefung nicht rot machen. */ +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(""); +console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); +process.exit(fehler === 0 ? 0 : 1); diff --git a/server/rechte.js b/server/rechte.js new file mode 100644 index 00000000..d74c9800 --- /dev/null +++ b/server/rechte.js @@ -0,0 +1,307 @@ +/* ===================================================================== + DIE RECHTETAFEL — was jede Rolle sehen darf, an einer Stelle + ===================================================================== + + Filipe, 11.09.2026: + + "egal welche rolle oder person hinzugefuegt wird soll immer nur + das sehen wass ich erlaube. mehr nicht. soll nichts so sein dass + wenn mann eine rolle oder jemanden hinzufuegt dass er dan alles + sieht, soll jede rolle immer nur das sehen was sie sehen sollen." + + --------------------------------------------------------------------- + DAS IST EINE UMKEHRUNG DER BAUWEISE, KEINE EINSTELLUNG + + Vorher galt: erlauben, ausser es steht etwas dagegen. Die Tabelle + der geschuetzten Seiten hiess sogar so -- "GESCHUETZT ist die + Ausnahme, nicht die Regel" -- und sie kannte zwei Wege, auf denen + etwas durchfiel: + + 1. `null` als Wert hiess "jede angemeldete Rolle". NEUN von zwanzig + Seiten standen darauf: Start, Uebersicht, Aufgaben, Chat, + Kalender, Dateien, Calls, Start-Check, Wissen. Eine neue Rolle + erbte sie alle, ohne dass jemand etwas erlaubt haette. + + 2. Eine Seite, die GAR NICHT in der Tabelle stand, fiel durch + dieselbe Bedingung (`if (erlaubt && ...)`) und war damit + ebenfalls fuer jeden offen. Das betraf am 11.09.2026 keine + einzige Seite -- gemessen, nicht gehofft --, aber jede neue + waere so entstanden. + + Ab hier gilt das Gegenteil: WAS NICHT IN DIESER TAFEL STEHT, IST + VERBOTEN. Kein `null`, kein `undefined`, kein Zweig, der etwas + durchlaesst. `darfSeite()` kennt genau zwei Antworten. + + --------------------------------------------------------------------- + WARUM DAS NICHT "DIE LISTE EINMAL DURCHGEHEN" IST + + Das waere die naheliegende Antwort und die falsche. Die Rolle wird + im Haus an 74 Stellen in 20 Modulen abgefragt (gezaehlt, nicht + geschaetzt). Drei Gruende, und alle drei sind hier schon eingetreten: + + * Eine Liste ALTERT. Am 09.09. war `content.html` fuer eine neue + Rolle offen -- nicht aus Nachlaessigkeit, sondern weil die Seite + spaeter dazukam als die Ueberlegung. + * Eine vergessene Erlaubnis SCHWEIGT. Sie faellt nur dem auf, der + hindurchgeht, und der merkt nichts: Die Seite laedt ja. In der + anderen Richtung ist es schlimmer -- eine Seite, die zu viel + zeigt, sieht aus wie eine Seite, die funktioniert. + * Bei 74 Stellen ist "ich gehe sie alle durch" kein Verfahren, + sondern ein Vorsatz. + + Deshalb steht die Antwort nicht in einem Kommentar, sondern in + `pruef-rechtetafel.mjs`: Eine Rolle ohne Eintrag macht den Prueflauf + rot. Eine Seite ohne Eintrag macht den Prueflauf rot. Man KANN es + nicht mehr vergessen. + + --------------------------------------------------------------------- + DIESE DATEI HAENGT VON NICHTS AB + + Kein Import aus workspace.js -- sonst gaebe es einen Ring, und die + Tafel muesste warten, bis das grosse Modul geladen ist. Sie ist + damit auch ohne laufenden Server pruefbar, genau wie crew-adresse.js. + ===================================================================== */ + +/** Alle Rollen, die es gibt. Muss zu ROLLEN in workspace.js passen -- + * pruef-rechtetafel vergleicht die beiden und wird rot, wenn eine + * Rolle nur in einer der beiden Listen steht. */ +export const ALLE_ROLLEN = [ + "spicy", "admin", "manager", "scout", "creator", "hand", "modi", +]; + +/* Kurzform fuer die Tafel unten. `ALLE` heisst hier ausdruecklich + "alle Rollen, die es GIBT" -- nicht "alle, die es geben wird". + Genau darin liegt der Unterschied zum alten `null`. */ +const ALLE = ALLE_ROLLEN; + +/* Schutz der angemeldeten Seiten. Serverseitig, nicht nur im Browser -- + sonst könnte man die Seite einfach direkt aufrufen. */ +/* Pfad -> erlaubte Rollen. `null` heisst: jede angemeldete Rolle. + + Die Rolle wird hier mitgeprueft und nicht nur in der Schnittstelle. + Sonst bekommt z. B. ein Creator die Verwaltungsseite zwar ausgeliefert + (HTTP 200) und sieht ihr Geruest, auch wenn sie danach leer bleibt und + das Skript ihn wegschickt. Sichtbar sein soll sie gar nicht. */ + +export const SEITEN = { + /* AUS `null` WURDE EINE LISTE (11.09.2026). + + Filipe: "egal welche rolle oder person hinzugefuegt wird soll + immer nur das sehen wass ich erlaube." + + Diese neun Seiten standen auf `null` -- das hiess "jede + angemeldete Rolle", also auch jede Rolle, die es noch gar nicht + gibt. Jetzt stehen die sieben, die es wirklich gibt. + + FUER DIE VORHANDENEN ROLLEN AENDERT SICH DAMIT NICHTS. Das ist + Absicht: Eine Umkehrung, die nebenbei Rechte entzieht, waere + zwei Aenderungen in einer -- und man wuesste hinterher nicht, + welche der beiden etwas kaputtgemacht hat. Wer hier enger + stellen will, macht das als eigenen Schritt. */ + "/workspace/start.html": [...ALLE], + "/workspace/uebersicht.html": [...ALLE], + /* AUSDRUECKLICH OHNE 'modi' (10.09.2026). Vorher stand hier `null` + ("jede angemeldete Rolle") -- und mit der neuen Rolle hiess das + auch: sie. Die Seite laedt dann zwar, ihre Schnittstelle antwortet + ihm aber mit 404, weil `content` nicht zu seinen Bereichen gehoert. + Eine Seite, die man oeffnen kann und die dann leer bleibt, sieht + aus wie ein Fehler -- und ist einer. + + Das ist die Kehrseite von "geschuetzt ist die Regel, nicht die + Ausnahme": Eine NEUE ROLLE erbt jedes `null` automatisch. Wer eine + Rolle hinzufuegt, muss diese Liste durchgehen -- der Kachel-Durchgang + in pruef-rollen findet die Faelle, die dabei auffallen. */ + "/workspace/content.html": ["spicy", "admin", "manager", "scout", "creator"], + "/workspace/aufgaben.html": [...ALLE], + /* Chat (06.09.2026): jede Rolle. WEN man erreicht, entscheidet + schreibbareIds() -- nicht der Zugang zur Seite. Ein Creator, der + die Seite nicht öffnen dürfte, könnte auch nicht antworten, und + genau das Antworten ist der Zweck. */ + "/workspace/chat.html": [...ALLE], + /* NUR DogFather (07.09.2026). Wunsch Filipe: "nimm die kategorie + zahlen bei jedem weg, nur die rolle dogfather soll die sehen." + + Bis heute stand hier `null` -- jede Rolle durfte die Seite oeffnen, + und WESSEN Zahlen jemand sah, entschied sichtbareCreatorIds(). + Die Begruendung dafuer war gut (es sind SEINE Zahlen), sie ist + jetzt aber nicht mehr meine Entscheidung. + + WICHTIG: Die Kachel wegzunehmen haette NICHT gereicht. Eine Kachel + ist ein Weg, keine Schranke -- wer die Adresse kennt oder ein altes + Lesezeichen hat, waere weiterhin hineingekommen. Beides gehoert + zusammen: bereiche.js zeigt sie nur noch DogFather, diese Zeile + laesst nur ihn hinein. */ + /* JETZT WIEDER FUER ALLE (11.09.2026). Filipe, mit der Kachel im + Bild: "jetzt soll jede rolle diese kachel sehen. spicy, dogfather, + manager, scout und creator. aber die creator kriegen dan quasi nur + ihre daten zu sehen und der manager scout dogfather oder spicy + koennen eintragen." + + Die Zeile stand seit dem 07.09.2026 auf ["admin"] -- erst "nimm + die kategorie zahlen bei jedem weg", dann die Praezisierung, dass + auch Spicy Media sie nicht sieht. Beides ist damit aufgehoben. + + AUSDRUECKLICH ALS LISTE, NICHT ALS `null`. `null` hiesse "jede + angemeldete Rolle" -- und damit automatisch auch jede Rolle, die + es noch gar nicht gibt. Genau das ist am 10.09. bei content.html + schiefgegangen. Hier stehen die fuenf, die Filipe genannt hat. + + WER WAS DARF, steht NICHT hier. Diese Zeile oeffnet nur die Tuer. + Dahinter gilt weiterhin: sichtbareCreatorIds() entscheidet, WESSEN + Zahlen jemand sieht (ein Creator sieht genau sich selbst), und + darfEintragen() entscheidet, wer schreiben darf (Leitung + Scout, + also NICHT der Creator). Beides war schon vorher so gebaut und + bleibt unangetastet -- es war nur niemand da, der es benutzen + konnte. */ + "/workspace/leistung.html": ["spicy", "admin", "manager", "scout", "creator"], + /* NUR DogFather (02.09.2026). Die Rollenauswahl verspricht das seit + jeher ("Manager -- dieselben Rechte, ausser der + Personenverwaltung"), die Schranke hielt sich nur nicht daran. + Wunsch: "die manager sollen diese kategorien garnicht sehen". */ + /* PERSONEN & ZUGAENGE: jetzt auch fuer Manager und Spicy Media + (07.09.2026). + + Filipe: "die manager sehen immer noch nicht die kachel personen & + zugangscode, wo sie dan die neuen creator hinzufuegen koennen ... + und die sollen in der kategorie wo die jetzt sehen werden auch noch + das mit den hinzufuegen sehen, alles andere auf der seite sollen die + weiterhin nicht sehen." + + DIE SEITE OEFFNET SICH, DIE SCHNITTSTELLEN NICHT. Das ist der ganze + Trick und der Grund, warum das hier ungefaehrlich ist: Alles unter + /workspace/api/verwaltung haengt weiterhin an `nurAdmin` -- Rollen + aendern, Codes neu setzen, sperren, loeschen, Protokoll lesen + bleiben zu und antworten mit 404. Wer die Seite oeffnet, bekommt + also genau die Teile zu sehen, fuer die es auch einen Weg gibt. + + Eine Seite ist kein Schutz, sie ist ein Weg. Der Schutz steht in den + Schnittstellen, und der ist unveraendert. */ + /* 'hand' kam am 10.09.2026 dazu. Filipe: "ich will dass die rechte + Hand auch alle sieht." + + ES WAR DIE DRITTE SCHICHT, DIE ZUSTIMMEN MUSSTE. Die Kachel stand + da, die Schnittstelle liess sie lesen -- und die Seite selbst warf + sie auf die Startseite zurueck. Gefunden hat das nicht das Auge, + sondern pruef-rollen: Sie geht jede Kachel jeder Rolle ab und + schaut, wo man landet ("Rechte Hand Kachel personen.html LANDET + AUF start.html"). + + Dass die Seite aufgeht, gibt ihr nichts, was die Schnittstellen ihr + nicht ohnehin geben: Alles unter /workspace/api/verwaltung ausser + der Liste antwortet ihr weiterhin mit 404. Eine Seite ist kein + Schutz, sie ist ein Weg. */ + "/workspace/personen.html": ["spicy", "admin", "manager", "hand"], + "/workspace/profil.html": ["spicy", "admin", "manager", "scout", "creator"], + /* Der eigene Steckbrief -- jede Rolle hat einen. Ein Creator wird + dort nicht hingeschickt (er sieht seinen auf profil.html), darf die + Seite aber aufrufen: Sie zeigt ihm dasselbe, nur ohne die Akte. */ + /* 'modi' kam am 10.09.2026 dazu. Der Steckbrief ist die EIGENE Seite + ("Dein Bild und deine Kanäle"); profil.html daneben ist der + Entwicklungsplan eines Creators und fuer ihn sinnlos. Der erste + Anlauf hatte genau das verwechselt -- die Kachel hiess "Mein + Profil" und fuehrte auf eine Seite, deren Schnittstelle mit 404 + antwortet, weil sie ausdruecklich nur Creator kennt. Gefunden vom + neuen Kachel-Durchgang in pruef-rollen. */ + "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"], + "/workspace/kalender.html": [...ALLE], + "/workspace/dateien.html": [...ALLE], + /* 'modi' kam am 10.09.2026 dazu -- und es war ein echter Fund: Ohne + diese Zeile leiteten VIER seiner Kacheln (Live-Ablauf, Community, + Technik, Ideen-Board) wortlos auf die Startseite zurueck. Der + Rollen-Rundgang meldete sie trotzdem als "ok": Eine Umleitung ist + kein Fehler, die Seite laedt ja -- nur eben eine andere. Seither + prueft pruef-rollen fuer JEDE Rolle, dass jede Kachel, die sie + bekommt, auch wirklich dort ankommt. + + WELCHE Bereiche ein Modi oeffnen darf, entscheidet die Liste + seiner Kacheln (MODI_BEREICHE_ERLAUBT) -- sonst kaeme er ueber die + Adresszeile auch in die Agentur-Ablage. */ + "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"], + "/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"], + "/workspace/scouting.html": ["spicy", "admin", "manager", "scout"], + /* DIE ARBEITSLAGE DES TEAMS (09.09.2026). + + Filipe: "so eine kategorie wie ueber die creator will ich dass nur + fuer die spicy und dogfather rolle auch ueber manager und scouts + gibt." + + Nur diese beiden Rollen. Ein Manager sieht seine eigene Zeile hier + ausdruecklich NICHT -- das ist kein Versehen: Eine Uebersicht ueber + die Arbeit der Kollegen gehoert der Leitung, nicht der Reihe. Der + Schutz steht ohnehin in der Schnittstelle (workspace-team.js); + diese Zeile sorgt nur dafuer, dass niemand vor einer leeren Seite + steht. */ + "/workspace/team.html": ["spicy", "admin"], + "/workspace/calls.html": [...ALLE], + "/workspace/startcheck.html": [...ALLE], + "/workspace/wissen.html": [...ALLE], + /* Ebenfalls nur DogFather: Was von allein passiert, bestimmt, was + allen anderen zugeschoben wird. */ + /* AUTOMATIONEN BLEIBEN BEI DogFather (07.09.2026). Ausdruecklich: + "bei automationen sollen die nicht sehen." Dahinter liegen + Sicherungen, die KI und der Zustand des Servers -- das ist Betrieb, + nicht Betreuung. */ + "/workspace/automation.html": ["admin"], + /* Die gemeinsame Lage des Teams -- nur die DogFather-Rolle, also + Filipe und VanVan (10.09.2026). Fuer alle anderen leitet die + Schranke auf die Startseite; die Schnittstelle dahinter antwortet + zusaetzlich mit 404. Zwei Schloesser, absichtlich: Faellt eines + weg, haelt das andere. */ + /* Der Eingang: DogFather und seine rechte Hand. Blueprint 3.1 -- + "Kann im Alltag vertretend Entscheidungen treffen." Genau das + passiert auf dieser Seite. */ + "/workspace/teamlage.html": ["admin", "hand"], +}; + +/* Die Zugangswaende. Sie haben keine Rolle, weil sich dort noch + niemand angemeldet hat -- sie sind der Ort, an dem das passiert. + Sie stehen hier nur, damit die Pruefung "jede Datei hat einen + Eintrag" sie nicht als Luecke meldet. */ +export const OHNE_ANMELDUNG = [ + "/workspace/index.html", + "/workspace/crew-index.html", +]; + +/* ===================================================================== + DIE EINE FRAGE, DIE ALLES BEANTWORTET + ===================================================================== */ + +/** + * Darf diese Person diese Seite oeffnen? + * + * Zwei Antworten, keine dritte: + * - Die Seite steht in der Tafel UND die Rolle steht darin -> ja. + * - Alles andere -> nein. Auch eine Seite, die es in der Tafel gar + * nicht gibt. Das ist die eigentliche Aenderung. + * + * @param {{rolle?: string}|null} person + * @param {string} pfad z. B. "/workspace/start.html" + */ +export function darfSeite(person, pfad) { + const rolle = person?.rolle; + if (!rolle) return false; + const erlaubt = SEITEN[String(pfad || "")]; + /* Kein Eintrag heisst NEIN. Vorher hiess es ja -- und genau darin + lag das Loch, das man einer neuen Seite nicht ansieht. */ + if (!Array.isArray(erlaubt)) return false; + return erlaubt.includes(rolle); +} + +/** + * Die Tafel aus der anderen Richtung: Rolle -> Seiten. + * + * Fuer die Uebersichtsseite ("Wer sieht was") und fuer Pruefungen. + * ABGELEITET, nicht zweitgeschrieben -- eine zweite Liste waere die + * Stelle, an der die beiden auseinanderlaufen. + */ +export function seitenFuer(rolle) { + return Object.entries(SEITEN) + .filter(([, r]) => r.includes(rolle)) + .map(([p]) => p) + .sort(); +} + +/** Alle Seiten der Tafel -- fuer die Pruefung gegen die Platte. */ +export function alleSeiten() { + return Object.keys(SEITEN).sort(); +} diff --git a/server/workspace.js b/server/workspace.js index f17e13cc..2e555a00 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -27,6 +27,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { mkdirSync } from "node:fs"; import { createRequire } from "node:module"; +import { darfSeite } from "./rechte.js"; import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; /* Weitergereicht, damit die Fachmodule sie wie alles andere aus @@ -3104,169 +3105,20 @@ function sitzungSetzen(res, person, req) { export const workspaceRouter = express.Router(); -/* Schutz der angemeldeten Seiten. Serverseitig, nicht nur im Browser -- - sonst könnte man die Seite einfach direkt aufrufen. */ -/* Pfad -> erlaubte Rollen. `null` heisst: jede angemeldete Rolle. +/* DIE ZUGANGSTABELLE STEHT SEIT DEM 11.09.2026 IN rechte.js. - Die Rolle wird hier mitgeprueft und nicht nur in der Schnittstelle. - Sonst bekommt z. B. ein Creator die Verwaltungsseite zwar ausgeliefert - (HTTP 200) und sieht ihr Geruest, auch wenn sie danach leer bleibt und - das Skript ihn wegschickt. Sichtbar sein soll sie gar nicht. */ -const GESCHUETZT = { - "/workspace/start.html": null, - "/workspace/uebersicht.html": null, - /* AUSDRUECKLICH OHNE 'modi' (10.09.2026). Vorher stand hier `null` - ("jede angemeldete Rolle") -- und mit der neuen Rolle hiess das - auch: sie. Die Seite laedt dann zwar, ihre Schnittstelle antwortet - ihm aber mit 404, weil `content` nicht zu seinen Bereichen gehoert. - Eine Seite, die man oeffnen kann und die dann leer bleibt, sieht - aus wie ein Fehler -- und ist einer. + Sie ist dorthin UMGEZOGEN, nicht kopiert -- samt jeder + Begruendung, die hier ueber den Jahren entstanden ist. Zwei + Tabellen waeren zwei Wahrheiten, und die zweite haette niemand + gepflegt. - Das ist die Kehrseite von "geschuetzt ist die Regel, nicht die - Ausnahme": Eine NEUE ROLLE erbt jedes `null` automatisch. Wer eine - Rolle hinzufuegt, muss diese Liste durchgehen -- der Kachel-Durchgang - in pruef-rollen findet die Faelle, die dabei auffallen. */ - "/workspace/content.html": ["spicy", "admin", "manager", "scout", "creator"], - "/workspace/aufgaben.html": null, - /* Chat (06.09.2026): jede Rolle. WEN man erreicht, entscheidet - schreibbareIds() -- nicht der Zugang zur Seite. Ein Creator, der - die Seite nicht öffnen dürfte, könnte auch nicht antworten, und - genau das Antworten ist der Zweck. */ - "/workspace/chat.html": null, - /* NUR DogFather (07.09.2026). Wunsch Filipe: "nimm die kategorie - zahlen bei jedem weg, nur die rolle dogfather soll die sehen." + Der Grund fuer den Umzug: `null` hiess "jede angemeldete Rolle", + und ein FEHLENDER Eintrag hiess dasselbe. Beides ist jetzt + "niemand". Was nicht in der Tafel steht, ist verboten. + pruef-rechtetafel.mjs macht daraus eine Garantie: eine Rolle + ohne Eintrag und eine Seite ohne Eintrag machen den Prueflauf + rot. */ - Bis heute stand hier `null` -- jede Rolle durfte die Seite oeffnen, - und WESSEN Zahlen jemand sah, entschied sichtbareCreatorIds(). - Die Begruendung dafuer war gut (es sind SEINE Zahlen), sie ist - jetzt aber nicht mehr meine Entscheidung. - - WICHTIG: Die Kachel wegzunehmen haette NICHT gereicht. Eine Kachel - ist ein Weg, keine Schranke -- wer die Adresse kennt oder ein altes - Lesezeichen hat, waere weiterhin hineingekommen. Beides gehoert - zusammen: bereiche.js zeigt sie nur noch DogFather, diese Zeile - laesst nur ihn hinein. */ - /* JETZT WIEDER FUER ALLE (11.09.2026). Filipe, mit der Kachel im - Bild: "jetzt soll jede rolle diese kachel sehen. spicy, dogfather, - manager, scout und creator. aber die creator kriegen dan quasi nur - ihre daten zu sehen und der manager scout dogfather oder spicy - koennen eintragen." - - Die Zeile stand seit dem 07.09.2026 auf ["admin"] -- erst "nimm - die kategorie zahlen bei jedem weg", dann die Praezisierung, dass - auch Spicy Media sie nicht sieht. Beides ist damit aufgehoben. - - AUSDRUECKLICH ALS LISTE, NICHT ALS `null`. `null` hiesse "jede - angemeldete Rolle" -- und damit automatisch auch jede Rolle, die - es noch gar nicht gibt. Genau das ist am 10.09. bei content.html - schiefgegangen. Hier stehen die fuenf, die Filipe genannt hat. - - WER WAS DARF, steht NICHT hier. Diese Zeile oeffnet nur die Tuer. - Dahinter gilt weiterhin: sichtbareCreatorIds() entscheidet, WESSEN - Zahlen jemand sieht (ein Creator sieht genau sich selbst), und - darfEintragen() entscheidet, wer schreiben darf (Leitung + Scout, - also NICHT der Creator). Beides war schon vorher so gebaut und - bleibt unangetastet -- es war nur niemand da, der es benutzen - konnte. */ - "/workspace/leistung.html": ["spicy", "admin", "manager", "scout", "creator"], - /* NUR DogFather (02.09.2026). Die Rollenauswahl verspricht das seit - jeher ("Manager -- dieselben Rechte, ausser der - Personenverwaltung"), die Schranke hielt sich nur nicht daran. - Wunsch: "die manager sollen diese kategorien garnicht sehen". */ - /* PERSONEN & ZUGAENGE: jetzt auch fuer Manager und Spicy Media - (07.09.2026). - - Filipe: "die manager sehen immer noch nicht die kachel personen & - zugangscode, wo sie dan die neuen creator hinzufuegen koennen ... - und die sollen in der kategorie wo die jetzt sehen werden auch noch - das mit den hinzufuegen sehen, alles andere auf der seite sollen die - weiterhin nicht sehen." - - DIE SEITE OEFFNET SICH, DIE SCHNITTSTELLEN NICHT. Das ist der ganze - Trick und der Grund, warum das hier ungefaehrlich ist: Alles unter - /workspace/api/verwaltung haengt weiterhin an `nurAdmin` -- Rollen - aendern, Codes neu setzen, sperren, loeschen, Protokoll lesen - bleiben zu und antworten mit 404. Wer die Seite oeffnet, bekommt - also genau die Teile zu sehen, fuer die es auch einen Weg gibt. - - Eine Seite ist kein Schutz, sie ist ein Weg. Der Schutz steht in den - Schnittstellen, und der ist unveraendert. */ - /* 'hand' kam am 10.09.2026 dazu. Filipe: "ich will dass die rechte - Hand auch alle sieht." - - ES WAR DIE DRITTE SCHICHT, DIE ZUSTIMMEN MUSSTE. Die Kachel stand - da, die Schnittstelle liess sie lesen -- und die Seite selbst warf - sie auf die Startseite zurueck. Gefunden hat das nicht das Auge, - sondern pruef-rollen: Sie geht jede Kachel jeder Rolle ab und - schaut, wo man landet ("Rechte Hand Kachel personen.html LANDET - AUF start.html"). - - Dass die Seite aufgeht, gibt ihr nichts, was die Schnittstellen ihr - nicht ohnehin geben: Alles unter /workspace/api/verwaltung ausser - der Liste antwortet ihr weiterhin mit 404. Eine Seite ist kein - Schutz, sie ist ein Weg. */ - "/workspace/personen.html": ["spicy", "admin", "manager", "hand"], - "/workspace/profil.html": ["spicy", "admin", "manager", "scout", "creator"], - /* Der eigene Steckbrief -- jede Rolle hat einen. Ein Creator wird - dort nicht hingeschickt (er sieht seinen auf profil.html), darf die - Seite aber aufrufen: Sie zeigt ihm dasselbe, nur ohne die Akte. */ - /* 'modi' kam am 10.09.2026 dazu. Der Steckbrief ist die EIGENE Seite - ("Dein Bild und deine Kanäle"); profil.html daneben ist der - Entwicklungsplan eines Creators und fuer ihn sinnlos. Der erste - Anlauf hatte genau das verwechselt -- die Kachel hiess "Mein - Profil" und fuehrte auf eine Seite, deren Schnittstelle mit 404 - antwortet, weil sie ausdruecklich nur Creator kennt. Gefunden vom - neuen Kachel-Durchgang in pruef-rollen. */ - "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"], - "/workspace/kalender.html": null, - "/workspace/dateien.html": null, - /* 'modi' kam am 10.09.2026 dazu -- und es war ein echter Fund: Ohne - diese Zeile leiteten VIER seiner Kacheln (Live-Ablauf, Community, - Technik, Ideen-Board) wortlos auf die Startseite zurueck. Der - Rollen-Rundgang meldete sie trotzdem als "ok": Eine Umleitung ist - kein Fehler, die Seite laedt ja -- nur eben eine andere. Seither - prueft pruef-rollen fuer JEDE Rolle, dass jede Kachel, die sie - bekommt, auch wirklich dort ankommt. - - WELCHE Bereiche ein Modi oeffnen darf, entscheidet die Liste - seiner Kacheln (MODI_BEREICHE_ERLAUBT) -- sonst kaeme er ueber die - Adresszeile auch in die Agentur-Ablage. */ - "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"], - "/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"], - "/workspace/scouting.html": ["spicy", "admin", "manager", "scout"], - /* DIE ARBEITSLAGE DES TEAMS (09.09.2026). - - Filipe: "so eine kategorie wie ueber die creator will ich dass nur - fuer die spicy und dogfather rolle auch ueber manager und scouts - gibt." - - Nur diese beiden Rollen. Ein Manager sieht seine eigene Zeile hier - ausdruecklich NICHT -- das ist kein Versehen: Eine Uebersicht ueber - die Arbeit der Kollegen gehoert der Leitung, nicht der Reihe. Der - Schutz steht ohnehin in der Schnittstelle (workspace-team.js); - diese Zeile sorgt nur dafuer, dass niemand vor einer leeren Seite - steht. */ - "/workspace/team.html": ["spicy", "admin"], - "/workspace/calls.html": null, - "/workspace/startcheck.html": null, - "/workspace/wissen.html": null, - /* Ebenfalls nur DogFather: Was von allein passiert, bestimmt, was - allen anderen zugeschoben wird. */ - /* AUTOMATIONEN BLEIBEN BEI DogFather (07.09.2026). Ausdruecklich: - "bei automationen sollen die nicht sehen." Dahinter liegen - Sicherungen, die KI und der Zustand des Servers -- das ist Betrieb, - nicht Betreuung. */ - "/workspace/automation.html": ["admin"], - /* Die gemeinsame Lage des Teams -- nur die DogFather-Rolle, also - Filipe und VanVan (10.09.2026). Fuer alle anderen leitet die - Schranke auf die Startseite; die Schnittstelle dahinter antwortet - zusaetzlich mit 404. Zwei Schloesser, absichtlich: Faellt eines - weg, haelt das andere. */ - /* Der Eingang: DogFather und seine rechte Hand. Blueprint 3.1 -- - "Kann im Alltag vertretend Entscheidungen treffen." Genau das - passiert auf dieser Seite. */ - "/workspace/teamlage.html": ["admin", "hand"], -}; /* Die einzige Seite unter /workspace, die offen sein MUSS -- man kann sich schlecht anmelden, wenn die Anmeldeseite eine Anmeldung verlangt. */ @@ -3327,10 +3179,19 @@ function schrankenPfad(roh) { damit offen ins Netz, ohne dass irgendwo etwas rot wird. Genau diese Sorte Fehler faellt erst auf, wenn jemand danach sucht. - Jetzt gilt: JEDE .html unter /workspace ist geschuetzt, ausser den - ausdruecklich offenen. Die Liste oben sagt nur noch, WELCHE ROLLEN - zusaetzlich eingeschraenkt sind. Eine neue Seite ist dadurch von - selbst mindestens "nur fuer Angemeldete" -- der sichere Rueckfall. */ + Seit dem 06.09.2026 gilt: JEDE .html unter /workspace ist + geschuetzt, ausser den ausdruecklich offenen. + + UND SEIT DEM 11.09.2026 AUCH FUER DIE ROLLE. Bis dahin sagte die + Liste nur, welche Rollen ZUSAETZLICH eingeschraenkt sind -- eine + neue Seite war damit "nur fuer Angemeldete", also fuer JEDE Rolle. + Das war der sichere Rueckfall gegen Fremde, aber keiner gegen eine + Rolle, die man gerade erst angelegt hat. + + Jetzt ist der Rueckfall "fuer niemanden". Eine neue Seite ohne + Eintrag in rechte.js oeffnet sich fuer keine Rolle, auch nicht fuer + DogFather -- und pruef-rechtetafel meldet sie, statt dass man es + beim Ausprobieren merkt. */ workspaceRouter.use((req, res, next) => { const pfad = schrankenPfad(req.path); if (!pfad.startsWith("/workspace/") || !pfad.endsWith(".html")) return next(); @@ -3338,8 +3199,15 @@ workspaceRouter.use((req, res, next) => { const person = sitzungLesen(req); if (!person) return res.redirect(302, "/workspace/"); - const erlaubt = GESCHUETZT[pfad]; - if (erlaubt && !erlaubt.includes(person.rolle)) { + /* EIN FEHLENDER EINTRAG HEISST JETZT NEIN. + + Vorher stand hier `if (erlaubt && ...)` -- und `erlaubt` war + `undefined`, sobald eine Seite gar nicht in der Tabelle stand. + Die Bedingung fiel dann durch, und die Seite war fuer jede + angemeldete Rolle offen. Am 11.09.2026 betraf das keine einzige + Seite (nachgemessen: 20 in der Tabelle, 2 Zugangswaende, 22 + Dateien) -- aber jede NEUE waere so entstanden. */ + if (!darfSeite(person, pfad)) { return res.redirect(302, "/workspace/start.html"); } return next(); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index f1d61de6..25948ab9 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609111554", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609111617", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609111554", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609111617", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111554", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111617", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111554", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111617", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index d1df15fd..8905a186 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -252,10 +252,10 @@ - - - - - + + + + +