diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs new file mode 100644 index 00000000..650241bf --- /dev/null +++ b/server/pruef-spicy.mjs @@ -0,0 +1,365 @@ +/* ===================================================================== + DIE ROLLE "SPICY MEDIA" + + Wunsch Filipe (07.09.2026): "eine neue rolle ... soll die gleichen + rechte haben wie dogfather ausser, bei automationen sollen die nicht + sehen und die kategorie personen und zugaenge sollen die nur leute + hinzufuegen koennen ... aber die sollen meine privaten chats und so + nicht sehen." + + --------------------------------------------------------------------- + ZWEI DINGE WERDEN HIER GEPRUEFT, UND DAS ZWEITE IST DAS WICHTIGERE + + 1. DER UMBAU DER PERSONENTABELLE. Eine Rolle ist ein erlaubter Wert in + einer Spalte, und der steckt in einem CHECK. Ein CHECK laesst sich + nicht aendern -- die Tabelle muss neu gebaut werden. Auf `personen` + zeigen ZWEIUNDFUENFZIG Fremdschluessel; jede Sitzung, jede Aufgabe, + jeder Termin haengt daran. Deshalb wird der Weg hier nicht "im + Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die Pruefung baut + eine Datenbank im alten Stand nach, fuellt sie mit Personen UND + abhaengigen Zeilen, laesst die echte Anwendung darueberlaufen und + zaehlt danach jede Zeile und jedes Feld nach. + + 2. DIE GRENZEN DER ROLLE. Zu jedem "darf" steht das "darf nicht" + daneben. Eine Rolle, von der nur geprueft ist, was sie kann, ist + keine Rolle, sondern eine Vermutung. + + Aufruf: node server/pruef-spicy.mjs + ===================================================================== */ +import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname, basename } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { DatabaseSync } from "node:sqlite"; +import { scryptSync, randomBytes } from "node:crypto"; +import { notbremse } from "./helfer-notbremse.mjs"; + +notbremse(300_000, "die Spicy-Pruefung"); + +const HIER = dirname(fileURLToPath(import.meta.url)); +const { portMussFreiSein } = await import("./helfer-port.mjs"); +const PORT = 4351; +await portMussFreiSein(PORT, "die Spicy-Pruefung"); +const BASIS = `http://127.0.0.1:${PORT}`; + +const ordner = mkdtempSync(join(tmpdir(), "ws-spicy-")); +const DB = join(ordner, "workspace.db"); + +let fehler = 0, gemacht = 0; +const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +let kind = null; +async function starten() { + kind = spawn(process.execPath, [join(HIER, "index.js")], { + env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT), + SITE_ACCESS_SECRET: "lokaler-test", + SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" }, + stdio: ["ignore", "pipe", "pipe"] }); + let aus = ""; + kind.stdout.on("data", (d) => { aus += d; }); + kind.stderr.on("data", (d) => { aus += d; }); + for (let i = 0; i < 100; i++) { + await new Promise((r) => setTimeout(r, 250)); + if (kind.exitCode !== null) break; + try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); + if (a.status < 500) return () => aus; } catch { /* noch nicht oben */ } + } + throw new Error("Server startete nicht:\n" + aus.slice(-1500)); +} +async function stoppen() { + if (!kind) return; + kind.kill(); kind = null; + await new Promise((r) => setTimeout(r, 700)); +} + +/* ======================================================================= + 1. Eine Datenbank im ALTEN Stand -- mit Personen und allem, was daran + haengt. Ohne die abhaengigen Zeilen pruefte der Umbau nur eine + Tabelle statt eines Netzes. + ======================================================================= */ +console.log("=== Der alte Stand ==="); +await starten(); +await stoppen(); + +const jetzt = new Date().toISOString(); +const anlegen = (d, name, rolle, code) => { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salz, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +}; + +let idDogi, idMax, idLuna, spaltenVorher, personenVorher; +{ + const d = new DatabaseSync(DB); + /* Die Tabelle auf den alten CHECK zuruecksetzen -- WOERTLICH so, wie + sie vor heute aussah, aber mit allen Spalten, die inzwischen + dazugekommen sind. Genau das ist der Fall, der gefaehrlich ist. */ + d.exec("PRAGMA foreign_keys = OFF"); + const alt = d.prepare( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='personen'").get().sql; + const zurueck = alt + /* Auch hier muss `IF NOT EXISTS` ins Muster -- sonst waere der + "alte Stand" gar keiner: Die Ersetzung liefe ins Leere, `CREATE + TABLE IF NOT EXISTS personen` waere wegen des vorhandenen Tisches + ein Leerlauf, und die Pruefung testete den heutigen Stand gegen + sich selbst. */ + .replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i, + "CREATE TABLE personen_alt") + .replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('admin','manager','scout','creator')"); + /* Wenn der Rueckbau nicht greift, ist alles danach wertlos -- dann + prueft diese Datei den heutigen Stand gegen sich selbst. Deshalb + hier abbrechen statt weitermachen. */ + const regelDanach = zurueck.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || ""; + if (!zurueck.includes("personen_alt") || regelDanach.includes("'spicy'")) { + console.log(" FEHL der alte Stand liess sich nicht nachbauen -- Muster passt nicht."); + process.exit(1); + } + d.exec("BEGIN"); + d.exec(zurueck); + const sp = d.prepare("PRAGMA table_info(personen)").all().map((z) => `"${z.name}"`).join(", "); + d.exec(`INSERT INTO personen_alt (${sp}) SELECT ${sp} FROM personen;`); + d.exec("DROP TABLE personen;"); + d.exec("ALTER TABLE personen_alt RENAME TO personen;"); + d.exec("COMMIT"); + d.exec("PRAGMA foreign_keys = ON"); + + idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001"); + idMax = anlegen(d, "Max", "manager", "CODE-MAX-00001"); + idLuna = anlegen(d, "Luna", "creator", "CODE-LUNA-0001"); + /* Ein Profilbild und ein Feld aus einer SPAETER ergaenzten Spalte -- + genau die verliert man beim Abschreiben einer Spaltenliste. */ + d.prepare("UPDATE personen SET bild = ?, ueber_mich = ?, tiktok = ? WHERE id = ?") + .run("abcdef0123456789abcdef0123456789.jpg", "Ich streame seit 2024.", "luna_live", idLuna); + /* Abhaengige Zeilen aus verschiedenen Tabellen. */ + d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)") + .run(idLuna, idMax, jetzt); + d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von) + VALUES ('technik','problem','Ton bricht ab',?,?,?,?)`) + .run(jetzt.slice(0, 10), idLuna, jetzt, idMax); + + spaltenVorher = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); + personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + + /* GEGENPROBE: Der ALTE Stand muss 'spicy' wirklich ablehnen -- sonst + gaebe es nichts umzustellen, und alles danach bewiese nichts. */ + let abgelehnt = false; + try { + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'spicy','x','y',1,1,?)") + .run("Darf noch nicht", jetzt); + } catch { abgelehnt = true; } + d.close(); + ok(abgelehnt, "der alte Stand lehnt die Rolle 'spicy' wirklich ab"); + ok(personenVorher === 3, `${personenVorher} Personen und abhaengige Zeilen angelegt`); + ok(spaltenVorher.includes("bild") && spaltenVorher.includes("tiktok"), + `die Tabelle hat ${spaltenVorher.length} Spalten, auch die spaeter ergaenzten`); +} + +/* ======================================================================= + 2. Die Anwendung laeuft darueber + ======================================================================= */ +console.log("\n=== Die Umstellung ==="); +const ausgabe = await starten(); +/* Die Umstellung laeuft beim ersten Datenbankzugriff, nicht beim + Zuhoeren am Anschluss -- ihre Meldung kann also erst kurz nach dem + Start in der Ausgabe stehen. Eine Anfrage stellen und kurz warten. */ +await fetch(BASIS + '/workspace/', { redirect: 'manual' }).catch(() => {}); +await new Promise((r) => setTimeout(r, 900)); +console.log("--- Serverausgabe ---"); +for (const z of ausgabe().split(String.fromCharCode(10))) if (z.includes("workspace]")) console.log(" " + z); +{ + ok(/Rolle 'spicy' freigeschaltet/.test(ausgabe()), + "die Umstellung meldet sich im Protokoll"); + ok(/Sicherung vor der Umstellung/.test(ausgabe()), + "und hat vorher eine Sicherung geschrieben"); + const sicherungen = readdirSync(ordner).filter((n) => n.includes("vor-spicy")); + ok(sicherungen.length === 1, `die Sicherungsdatei liegt da (${sicherungen[0] || "keine"})`); +} +{ + const d = new DatabaseSync(DB, { readOnly: true }); + const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); + const fehlend = spaltenVorher.filter((n) => !spalten.includes(n)); + ok(fehlend.length === 0, + fehlend.length ? `nach der Umstellung fehlen Spalten: ${fehlend.join(", ")}` + : `alle ${spalten.length} Spalten sind noch da`); + + const n = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + ok(n === personenVorher, `alle ${n} Personen sind noch da (vorher ${personenVorher})`); + + const luna = d.prepare("SELECT * FROM personen WHERE id = ?").get(idLuna); + ok(luna?.bild === "abcdef0123456789abcdef0123456789.jpg", + `das Profilbild hat den Umbau ueberlebt (${luna?.bild ? "ja" : "WEG"})`); + ok(luna?.ueber_mich === "Ich streame seit 2024." && luna?.tiktok === "luna_live", + "auch die spaeter ergaenzten Textspalten"); + + const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(idLuna); + ok(b?.betreuer_id === idMax, `die Betreuung zeigt noch auf Max (${b?.betreuer_id})`); + const e = d.prepare("SELECT creator_id FROM eintraege WHERE titel = 'Ton bricht ab'").get(); + ok(e?.creator_id === idLuna, `der Technik-Eintrag noch auf Luna (${e?.creator_id})`); + const kaputt = d.prepare("PRAGMA foreign_key_check").all(); + ok(kaputt.length === 0, `keine verwaisten Verweise (${kaputt.length})`); + + /* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine + Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein + Erfolg und ist der schlimmere Ausgang. */ + d.close(); + const s = new DatabaseSync(DB); + let erfunden = false; + try { + s.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'quatsch','x','y',1,1,?)") + .run("Erfundene Rolle", jetzt); + } catch { erfunden = true; } + ok(erfunden, "die Schranke beisst noch -- eine erfundene Rolle wird abgelehnt"); + const idSpicy = anlegen(s, "Agentur", "spicy", "CODE-SPICY-001"); + ok(Number.isInteger(idSpicy) && idSpicy > 0, "'spicy' laesst sich jetzt anlegen"); + s.close(); +} + +/* ======================================================================= + 3. Was die Rolle darf -- und was nicht + ======================================================================= */ +console.log("\n=== Rechte ==="); +const anmelden = async (rolle, code) => { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }) }); + const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); + if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); + return keks; +}; +const keksSpicy = await anmelden("spicy", "CODE-SPICY-001"); +const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); +const keksMax = await anmelden("manager", "CODE-MAX-00001"); + +const ruf = async (art, weg, keks, koerper) => { + const a = await fetch(BASIS + weg, { method: art, + headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS }, + body: koerper ? JSON.stringify(koerper) : undefined }); + let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ } + return { status: a.status, daten }; +}; + +{ + const { status, daten } = await ruf("GET", "/workspace/api/ich", keksSpicy); + ok(status === 200 && daten?.rolle === "spicy", `die Anmeldung klappt (${daten?.rolle})`); + ok(daten?.rolle_name === "Spicy Media", `und heisst in der Oberflaeche "${daten?.rolle_name}"`); +} +{ + /* SIEHT ALLES: alle Personen, alle Bereiche. */ + const { daten } = await ruf("GET", "/workspace/api/personen", keksSpicy); + const namen = (daten?.personen || []).map((p) => p.name).sort(); + ok(namen.includes("Luna") && namen.includes("Max") && namen.includes("Filipe"), + `sie sieht alle: ${namen.join(", ")}`); +} +{ + const { status } = await ruf("GET", "/workspace/api/uebersicht/creator", keksSpicy); + ok(status === 200, `das Dashboard steht ihr offen (HTTP ${status})`); +} +{ + /* NICHT: Automationen. */ + const a = await fetch(BASIS + "/workspace/automation.html", { + headers: { Cookie: keksSpicy }, redirect: "manual" }); + ok(a.status === 302 || a.status === 403 || a.status === 404, + `die Automationen-Seite bleibt zu (HTTP ${a.status})`); + const { status } = await ruf("GET", "/workspace/api/sicherung/zustand", keksSpicy); + ok(status === 404, `und die Sicherungs-Schnittstelle auch (HTTP ${status})`); +} +{ + /* NICHT: die Personenverwaltung. Anlegen ja, alles andere nein. */ + const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksSpicy); + ok(status === 404, `die Personenverwaltung bleibt zu (HTTP ${status})`); + const loeschen = await ruf("DELETE", `/workspace/api/verwaltung/personen/${idLuna}`, keksSpicy); + ok(loeschen.status === 404, `loeschen geht nicht (HTTP ${loeschen.status})`); + const code = await ruf("POST", `/workspace/api/verwaltung/personen/${idLuna}/code`, keksSpicy); + ok(code.status === 404, `einen Code neu setzen auch nicht (HTTP ${code.status})`); +} +{ + /* ABER: Leute anlegen -- Creator und Manager. */ + const c = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy, { name: "NeuerCreator" }); + ok(c.status === 201 && c.daten?.rolle === "creator", + `sie legt einen Creator an (HTTP ${c.status}, ${c.daten?.rolle})`); + const m = await ruf("POST", "/workspace/api/manager-anlegen", keksSpicy, { name: "NeuerManager" }); + ok(m.status === 201 && m.daten?.rolle === "manager", + `und einen Manager (HTTP ${m.status}, ${m.daten?.rolle})`); +} +{ + /* GEGENPROBE: Ein Manager kann KEINEN Manager anlegen -- sonst waere + die zweite Tuer nur Zierde. */ + const { status } = await ruf("POST", "/workspace/api/manager-anlegen", keksMax, { name: "VonMax" }); + ok(status === 404, `ein Manager kommt durch diese Tuer NICHT (HTTP ${status})`); +} +{ + /* NICHT: fremde Chats. + + Der Chat haengt ausschliesslich an der Teilnehmerliste -- Spicy + Media ist nirgends Teilnehmerin, also sieht sie nichts. Geprueft + wird mit einem ECHTEN Gespraech zwischen DogFather und Max. + + DER AUFBAU MUSS GELINGEN, SONST BEWEIST DER REST NICHTS. Beim + ersten Anlauf stand hier ein falscher Weg (/chat/raum statt + /chat/direkt). Das Gespraech entstand gar nicht -- und "Spicy Media + sieht 0 Gespraeche" war trotzdem gruen, weil es ueberhaupt keine + gab. Genau die Sorte gruener Haken, die nichts geprueft hat. + Deshalb ist das Anlegen jetzt selbst eine Pruefung, und die + Raumnummer wird nachgehalten. */ + const raum = await ruf("POST", "/workspace/api/chat/direkt", keksDogi, { mit: idMax }); + const raumId = raum.daten?.raum_id; + ok(raum.status === 200 && Number.isInteger(raumId), + `DogFather und Max haben ein Gespraech (HTTP ${raum.status}, Raum ${raumId})`); + const nachricht = await ruf("POST", `/workspace/api/chat/raeume/${raumId}/nachrichten`, + keksDogi, { text: "Streng geheim." }); + ok(nachricht.status === 200 || nachricht.status === 201, + `und eine Nachricht darin (HTTP ${nachricht.status})`); + + /* GEGENPROBE: Max SIEHT das Gespraech -- sonst pruefte die Zeile + darunter nur, dass die Liste immer leer ist. */ + const beiMax = await ruf("GET", "/workspace/api/chat/raeume", keksMax); + ok((beiMax.daten?.raeume || []).length === 1, + `Max sieht es (${(beiMax.daten?.raeume || []).length})`); + + const meine = await ruf("GET", "/workspace/api/chat/raeume", keksSpicy); + const raeume = meine.daten?.raeume || []; + ok(raeume.length === 0, `Spicy Media sieht 0 Gespraeche (${raeume.length})`); + const rein = await ruf("GET", `/workspace/api/chat/raeume/${raumId}/nachrichten`, keksSpicy); + ok(rein.status === 403 || rein.status === 404, + `und kommt auch mit der Raumnummer nicht hinein (HTTP ${rein.status})`); +} +{ + /* Und DogFather bleibt der Einzige mit dem letzten Wort. */ + const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi); + ok(status === 200, `DogFather kommt weiterhin in die Verwaltung (HTTP ${status})`); +} + +/* ======================================================================= + 4. Die Anmeldeseite kennt die Rolle + ======================================================================= */ +console.log("\n=== Die Anmeldeseite ==="); +{ + const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await pw.chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 950 } }); + const seite = await ctx.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); + const rollen = await seite.$$eval(".rolle", (k) => k.map((x) => x.dataset.rolle)); + ok(rollen[0] === "spicy", `sie steht ganz oben (${rollen.join(", ")})`); + await seite.click('.rolle[data-rolle="spicy"]'); + await seite.fill("#code", "CODE-SPICY-001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 25000 }); + await seite.waitForTimeout(1500); + const kacheln = await seite.$$eval(".kachel__name", (k) => k.map((x) => x.textContent.trim())); + ok(kacheln.includes("Personen & Zugänge"), + `sie sieht die Personen-Kachel (${kacheln.length} Kacheln)`); + ok(!kacheln.includes("Automationen"), "und die Automationen-Kachel NICHT"); + await ctx.close(); + await browser.close(); +} + +await stoppen(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } + +console.log(`\n${gemacht} Pruefungen`); +console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 7f5a3625..472dc29e 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -16,7 +16,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, + db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, externPruefen, externSql, } from "./workspace.js"; @@ -262,7 +262,7 @@ export function sichtbar(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; /* "ODER ich habe ihn selbst angelegt" kam am 02.09.2026 dazu -- aus demselben Grund wie bei den Aufgaben: Ein Eintrag ohne creator_id diff --git a/server/workspace-calls.js b/server/workspace-calls.js index 7765c3a3..57a47abc 100644 --- a/server/workspace-calls.js +++ b/server/workspace-calls.js @@ -18,7 +18,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, einstellung, einstellungSetzen, istLeitung, istDogFather, + db, protokolliere, echteIp, sitzungLesen, einstellung, einstellungSetzen, istLeitung, istDogFather, siehtAlles, } from "./workspace.js"; import { sichtbar } from "./workspace-kalender.js"; @@ -203,7 +203,7 @@ callRouter.put("/workspace/api/call-link", gleicheHerkunft, (req, res) => { CASCADE auf der Zwischentabelle), die Aufgaben selbst nicht. */ callRouter.delete("/workspace/api/calls/:id/protokoll", gleicheHerkunft, (req, res) => { try { - if (!istDogFather(req.person)) { + if (!siehtAlles(req.person)) { return res.status(403).json({ fehler: "Nur DogFather darf ein Protokoll löschen." }); } const id = Number(req.params.id); diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js index 01a1ff55..25b171cf 100644 --- a/server/workspace-checkliste.js +++ b/server/workspace-checkliste.js @@ -34,7 +34,7 @@ import express from "express"; import { - db, sitzungLesen, istLeitung, istDogFather, darfCreator, betreuteIds, + db, sitzungLesen, istLeitung, istDogFather, siehtAlles, darfCreator, betreuteIds, protokolliere, echteIp, } from "./workspace.js"; import { @@ -207,7 +207,7 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { if (req.person.rolle !== "creator") { /* Nur DogFather waehlt aus allen Creatorn; ein Manager aus denen, die ihm zugeteilt sind (01.09.2026). */ - const ids = istDogFather(req.person) + const ids = siehtAlles(req.person) ? db().prepare("SELECT id FROM personen WHERE rolle = 'creator' AND aktiv = 1") .all().map((z) => z.id) : betreuteIds(req.person); diff --git a/server/workspace-content.js b/server/workspace-content.js index 5298f2d2..c24bd5f1 100644 --- a/server/workspace-content.js +++ b/server/workspace-content.js @@ -26,7 +26,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, - istLeitung, istDogFather, tagLokal, + istLeitung, istDogFather, siehtAlles, tagLokal, } from "./workspace.js"; export const contentRouter = express.Router(); @@ -85,7 +85,7 @@ function sichtbareCreator(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; if (person.rolle === "creator") return [person.id]; return betreuteIds(person); } diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index 6c8f91cb..253665b9 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -29,7 +29,7 @@ import { randomBytes } from "node:crypto"; import { join, extname, basename } from "node:path"; import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from "node:fs"; import { - db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, ROLLEN_SORTIERUNG, + db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, externSql, EXTERN_MAX, sichtbarePersonenIds, } from "./workspace.js"; @@ -79,7 +79,7 @@ export function sichtbar(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; /* Ausdrueckliche Freigabe -- gilt fuer jede Rolle gleich. Wer in der Liste steht, sieht die Datei, egal ob Creator oder Scout. */ diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js index 2d0da0a4..bd331b35 100644 --- a/server/workspace-hinweise.js +++ b/server/workspace-hinweise.js @@ -24,7 +24,7 @@ import express from "express"; import { - db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds, + db, sitzungLesen, betreuteIds, istLeitung, istDogFather, siehtAlles, pipelineIds, } from "./workspace.js"; import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js"; import { sichtbar as sichtbarTermine } from "./workspace-kalender.js"; @@ -222,8 +222,8 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => { Scouts -- dieselbe Regel wie in der Pipeline selbst, aus derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der mehr verraet als die Seite dahinter, waere ein Leck. */ - if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") { - const ids = istDogFather(person) ? null : pipelineIds(person); + if (siehtAlles(person) || person.rolle === "scout" || person.rolle === "manager") { + const ids = siehtAlles(person) ? null : pipelineIds(person); const nur = ids === null ? "1=1" : ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1"; const werte = ids === null ? [] : ids; diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index c78df009..1dc613c2 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -13,7 +13,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, betreutWo, termineSichtbar, heuteLokal, + db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, siehtAlles, betreutWo, termineSichtbar, heuteLokal, externPruefen, externSql, betreuteIds, einladbareIds, ROLLEN_SORTIERUNG, } from "./workspace.js"; import { @@ -282,7 +282,7 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { const eigenA = "(a.creator_id = ? OR a.verantwortlich_id = ? OR a.erstellt_von = ?)"; const werteA = [sicht.id, sicht.id, sicht.id]; const bA = betreutWo(sicht, "a.creator_id"); - const aufgabenWo = istDogFather(sicht) + const aufgabenWo = siehtAlles(sicht) ? { wo: "1=1", werte: [] } : bA ? { wo: `(${eigenA} OR ${bA.wo})`, werte: [...werteA, ...bA.werte] } diff --git a/server/workspace-personen.js b/server/workspace-personen.js index dd32a3d7..ddfce9ca 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -15,7 +15,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -368,6 +368,45 @@ personenRouter.post("/workspace/api/creator-anlegen", gleicheHerkunft, nurLeitun } }); +/* ZWEITE TUER: EINEN MANAGER ANLEGEN -- nur Spicy Media und DogFather + (07.09.2026). + + Filipe: "die agentur manager und creator und die dan auch einteilen." + + Warum nicht einfach ein Feld `rolle` an der Tuer daneben? Weil damit + genau die Eigenschaft verloren ginge, die sie sicher macht: Sie KANN + nichts anderes, als einen Creator anzulegen. Sobald eine Abfrage + entscheidet, welche Rolle erlaubt ist, ist eine vergessene Abfrage ein + zweiter Zugang mit vollen Rechten. + + Also zwei Tueren, jede mit genau einer Aufgabe. Die Rolle steht auch + hier nicht im Aufruf. + + EINEN MANAGER LEGT MAN NICHT NEBENBEI AN: Er sieht danach jeden + Creator, den er betreut, und legt selbst welche an. Deshalb bleibt + diese Tuer Spicy Media und DogFather vorbehalten -- ein Manager kann + keinen zweiten Manager schaffen. */ +personenRouter.post("/workspace/api/manager-anlegen", gleicheHerkunft, (req, res) => { + try { + const person = sitzungLesen(req); + if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); + if (!siehtAlles(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const name = String(req.body?.name ?? "").trim(); + if (name.length < 2) return res.status(400).json({ fehler: "Name fehlt." }); + if (name.length > NAME_MAX) return res.status(400).json({ fehler: "Name ist zu lang." }); + const vorhanden = db().prepare( + "SELECT 1 FROM personen WHERE lower(name) = lower(?) AND aktiv = 1").get(name); + if (vorhanden) return res.status(409).json({ fehler: "Diesen Namen gibt es schon." }); + + const neu = personAnlegen(name, "manager", { ...person, ip: echteIp(req) }); + res.status(201).json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code }); + } catch (fehler) { + console.error("[workspace] Manager anlegen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + /* Welche Scouts kann ich einem neuen Creator gleich mitgeben? Dieselbe Regel wie oben, nur lesend -- damit die Oberflaeche genau die Liste anbietet, die der Server auch annimmt. Zwei getrennte diff --git a/server/workspace-profil.js b/server/workspace-profil.js index a2e29eb4..b0ac16ba 100644 --- a/server/workspace-profil.js +++ b/server/workspace-profil.js @@ -14,7 +14,7 @@ import express from "express"; import { sichtbareCreatorIds, db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds, istLeitung, - istDogFather, + istDogFather, siehtAlles, } from "./workspace.js"; export const profilRouter = express.Router(); @@ -71,7 +71,7 @@ function darf(person, zielId) { Ein Manager behaelt volle Rechte an SEINEN Creators: darfCreator prueft die Zuteilung, und "voll" bleibt es dort auch -- er soll die Verwaltungsfelder seiner eigenen Leute pflegen koennen. */ - if (istDogFather(person)) return "voll"; + if (siehtAlles(person)) return "voll"; if (person.rolle === "creator") return person.id === zielId ? "eigen" : null; if (person.rolle === "manager") return darfCreator(person, zielId) ? "voll" : null; if (person.rolle === "scout") return darfCreator(person, zielId) ? "betreuend" : null; diff --git a/server/workspace-reports.js b/server/workspace-reports.js index ffbb593b..c305b50d 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -20,7 +20,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, sichtbareCreatorIds, + db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds, } from "./workspace.js"; import { BEREICHE } from "./workspace-bereiche.js"; @@ -69,7 +69,7 @@ function zielCreator(req) { bei ihm "alle MEINE". Vorher lieferte gewuenscht=null eine Auswertung ueber den gesamten Bestand -- die Zahlen darin stammten zum Teil von Creators, die ihm nie zugeteilt waren. (03.09.2026) */ - if (istDogFather(req.person)) return gewuenscht; + if (siehtAlles(req.person)) return gewuenscht; /* Scout: darf nur ueber die eigenen Creator einen Report ziehen. Ein fremder Wunsch wird nicht abgelehnt, sondern still auf den ersten diff --git a/server/workspace-schulung.js b/server/workspace-schulung.js index ea8a6199..27755094 100644 --- a/server/workspace-schulung.js +++ b/server/workspace-schulung.js @@ -44,7 +44,7 @@ import { import { join } from "node:path"; import { readFileSync, existsSync } from "node:fs"; import { - db, sitzungLesen, istLeitung, istDogFather, darfCreator, betreuteIds, + db, sitzungLesen, istLeitung, istDogFather, siehtAlles, darfCreator, betreuteIds, protokolliere, echteIp, DATEN_ORDNER, sichtbareCreatorIds, } from "./workspace.js"; @@ -406,7 +406,7 @@ schulungRouter.delete("/workspace/api/schulung/:id(\\d+)/bestaetigen", gleicheHe anzutasten. */ schulungRouter.delete("/workspace/api/schulung/:id(\\d+)", gleicheHerkunft, (req, res) => { - if (!istDogFather(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" }); + if (!siehtAlles(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" }); try { db().prepare("UPDATE schulungen SET aktiv = 0 WHERE id = ?").run(Number(req.params.id)); protokolliere("schulung_abgeschaltet", req.person, echteIp(req), `Schulung ${req.params.id}`); diff --git a/server/workspace-scouts.js b/server/workspace-scouts.js index e1ad4a91..47cad658 100644 --- a/server/workspace-scouts.js +++ b/server/workspace-scouts.js @@ -17,7 +17,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, pipelineIds, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, siehtAlles, pipelineIds, } from "./workspace.js"; export const scoutRouter = express.Router(); @@ -70,7 +70,7 @@ scoutRouter.use("/workspace/api/leads", angemeldet); dreimal abgeschrieben waere dreimal Gelegenheit, sie einmal falsch zu aendern. */ function sichtbar(person) { - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; const ids = pipelineIds(person); if (!ids.length) return { wo: "0=1", werte: [] }; return { wo: `l.scout_id IN (${ids.map(() => "?").join(",")})`, werte: ids }; diff --git a/server/workspace-suche.js b/server/workspace-suche.js index 72395b5f..dec0738f 100644 --- a/server/workspace-suche.js +++ b/server/workspace-suche.js @@ -24,7 +24,7 @@ import express from "express"; import { - db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds, + db, sitzungLesen, betreuteIds, istLeitung, istDogFather, siehtAlles, pipelineIds, sichtbareCreatorIds, } from "./workspace.js"; import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js"; @@ -198,8 +198,8 @@ sucheRouter.get("/workspace/api/suche", (req, res) => { Scouts -- dieselbe Regel wie in der Pipeline selbst, aus derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der mehr verraet als die Seite dahinter, waere ein Leck. */ - if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") { - const ids = istDogFather(person) ? null : pipelineIds(person); + if (siehtAlles(person) || person.rolle === "scout" || person.rolle === "manager") { + const ids = siehtAlles(person) ? null : pipelineIds(person); const nur = ids === null ? "1=1" : ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1"; const werte = ids === null ? [] : ids; diff --git a/server/workspace.js b/server/workspace.js index 731e0a8e..47fa1249 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -57,17 +57,19 @@ const COOKIE = "dfw_sitzung"; const SITZUNG_STUNDEN = 12; const VERSUCHE_MAX = 8; // pro IP const VERSUCHE_FENSTER_MIN = 10; -const ROLLEN = new Set(["admin", "manager", "scout", "creator"]); +const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator"]); -/* Die Reihenfolge, in der Rollen ueberall erscheinen: DogFather zuerst, - dann Manager, dann Scout, dann Creator. Steht hier einmal, damit keine - Liste eine eigene Reihenfolge erfindet. */ -export const ROLLEN_REIHE = ["admin", "manager", "scout", "creator"]; +/* Die Reihenfolge, in der Rollen ueberall erscheinen: Spicy Media + zuerst, dann DogFather, Manager, Scout, Creator. Steht hier einmal, + damit keine Liste eine eigene Reihenfolge erfindet. */ +export const ROLLEN_REIHE = ["spicy", "admin", "manager", "scout", "creator"]; /* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch - (admin, creator, manager, scout) -- also fast genau falsch herum. */ + (admin, creator, manager, scout, spicy) -- also fast genau falsch + herum. */ export const ROLLEN_SORTIERUNG = - "CASE rolle WHEN 'admin' THEN 0 WHEN 'manager' THEN 1 WHEN 'scout' THEN 2 ELSE 3 END"; + "CASE rolle WHEN 'spicy' THEN 0 WHEN 'admin' THEN 1 WHEN 'manager' THEN 2" + + " WHEN 'scout' THEN 3 ELSE 4 END"; /* LEITUNG = DogFather und Manager. Ein Manager darf alles, was DogFather darf -- mit genau zwei Ausnahmen, die in @@ -75,10 +77,44 @@ export const ROLLEN_SORTIERUNG = Leitung veraendern. Sonst koennte er sich selbst zum DogFather machen oder den echten aussperren. "Nur DogFather hat alle endgueltigen Rechte" heisst genau das. */ -const LEITUNG = new Set(["admin", "manager"]); +const LEITUNG = new Set(["spicy", "admin", "manager"]); export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle); export const istDogFather = (person) => !!person && person.rolle === "admin"; +/* ======================================================================= + SPICY MEDIA (07.09.2026) + + Wunsch Filipe: "eine neue rolle ... die den namen traegt, spicy media, + und soll die gleichen rechte haben wie dogfather ausser, bei + automationen sollen die nicht sehen und die kategorie personen und + zugaenge sollen die nur leute hinzufuegen koennen ... aber die sollen + meine privaten chats und so nicht sehen." + + ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS -- und genau daran haette + man diesen Umbau kaputtmachen koennen: + + WER SIEHT ALLES? -> siehtAlles() = DogFather ODER Spicy Media + WER ENTSCHEIDET? -> istDogFather() = nur DogFather + + Die erste Frage stellt sich bei Listen, Uebersichten und Auswertungen: + Spicy Media soll alle Manager, Scouts und Creator sehen. Die zweite + bei allem Endgueltigen: loeschen, Rollen aendern, Codes neu setzen, + Sicherungen, die KI abschalten. Da bleibt DogFather allein. + + Es waere viel weniger Arbeit gewesen, istDogFather() einfach um + "spicy" zu erweitern -- und genau das waere der Fehler: Spicy Media + koennte dann DogFather loeschen. Zwei Namen, zwei Bedeutungen, und an + jeder Stelle steht sichtbar, welche gemeint ist. + + DER CHAT BRAUCHTE NICHTS. Er haengt ausschliesslich an der + Teilnehmerliste (chat_teilnehmer) und kennt kein "das Management sieht + alles". Spicy Media sieht fremde Gespraeche also nicht, weil es dafuer + gar keinen Weg gibt -- nicht, weil eine Abfrage es verbietet. + ======================================================================= */ +export const istSpicy = (person) => !!person && person.rolle === "spicy"; +export const siehtAlles = (person) => !!person + && (person.rolle === "admin" || person.rolle === "spicy"); + /* Der Rollenschluessel bleibt "admin" -- er steckt in der CHECK-Regel der Datenbank, in jeder Sitzung und in jeder Rechteabfrage. Umbenannt wird nur, was man LIEST. Diese Zuordnung ist die einzige Stelle dafuer: @@ -89,6 +125,7 @@ export const istDogFather = (person) => !!person && person.rolle === "admin"; Rolle "admin" weiterhin "das Management". Gemeint ist dieselbe Rolle, die in der Oberflaeche "DogFather" heisst. */ export const ROLLEN_NAME = { + spicy: "Spicy Media", admin: "DogFather", manager: "Manager", scout: "Scout", @@ -120,7 +157,21 @@ let _dbFehler = null; waehrend laufender Schreibvorgaenge). ===================================================================== */ function umstellungen(d) { - const jetztStempel = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, ""); + /* MIT SEKUNDEN, nicht nur mit Minuten (07.09.2026). + + `VACUUM INTO` weigert sich, eine vorhandene Datei zu ueberschreiben + ("output file already exists") -- zu Recht, eine Sicherung darf + nichts wegwerfen. Der Stempel hatte aber nur Minutenaufloesung: + Zwei Umstellungen innerhalb derselben Minute wollten in dieselbe + Datei, die zweite scheiterte, und weil ohne Sicherung nicht + umgestellt wird, blieb sie einfach aus. + + Das ist genau die unangenehme Sorte Fehler: Es sieht nach "die + Umstellung lief" aus (die Datei liegt ja da) und war doch keine. + Gefunden von pruef-spicy, das den Server zweimal in derselben + Minute startet -- im Betrieb genuegt dafuer ein Neustart im + falschen Moment. */ + const jetztStempel = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, ""); /* ---- Zeitpunkt des Hochladens in der Bibliothek ---- Eine neue Spalte laesst SQLite anstandslos anhaengen -- anders als @@ -635,6 +686,131 @@ function umstellungen(d) { d.exec("PRAGMA foreign_keys = ON"); } } + + /* ===================================================================== + ROLLE "spicy" (Spicy Media) FREISCHALTEN — 07.09.2026 + + Wunsch Filipe: "ich will dass ueber dogfather auch eine kategorie, + eine neue rolle entsteht die den namen traegt, spicy media." + + --------------------------------------------------------------------- + DAS IST DER RISKANTESTE UMBAU IM GANZEN HAUS + + Eine Rolle ist ein erlaubter Wert in einer Spalte, und der steckt in + SQLite in einem CHECK. Ein CHECK laesst sich nicht aendern -- die + Tabelle muss neu gebaut werden. Bei `eintraege` war das schon + unangenehm; hier geht es um `personen`, und auf die zeigen + ZWEIUNDFUENFZIG Fremdschluessel aus einem Dutzend Tabellen. Jede + Sitzung, jede Aufgabe, jeder Termin, jede Datei haengt daran. + + DESHALB WIRD DER BAUPLAN NICHT ABGESCHRIEBEN, SONDERN GELESEN. + + Die beiden Umstellungen darueber schreiben ihre Spaltenliste von + Hand ab. Das ging gut, solange die Liste stimmte -- aber `personen` + hat seit damals SIEBEN Spalten dazubekommen (bild, ueber_mich, + tiktok, instagram, youtube, twitch und weitere, siehe den + Spalten-Nachtrag oben). Wer hier eine Liste abschreibt, verliert + beim naechsten Mal genau die Spalte, die jemand letzte Woche + ergaenzt hat -- samt allen Profilbildern. + + Also: Der vorhandene CREATE-Text wird aus sqlite_master geholt und + darin AUSSCHLIESSLICH die Rollenliste ersetzt. Alles andere -- jede + Spalte, jeder Typ, jede Vorgabe -- bleibt woertlich stehen. Und die + Kopierliste kommt aus PRAGMA table_info, also aus der Tabelle + selbst. + + WENN DIE ERSETZUNG NICHT GREIFT, WIRD NICHT UMGESTELLT. Ein + `replace`, das nichts findet, gibt den Text unveraendert zurueck -- + die Umstellung liefe dann durch, baute dieselbe Tabelle noch einmal + und meldete Erfolg. Genau die Sorte gruener Haken, die nichts + geprueft hat. Deshalb wird danach nachgesehen, ob 'spicy' wirklich + drinsteht. + ===================================================================== */ + const rollenPlan = d.prepare( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || ""; + /* GEPRUEFT WIRD DIE REGEL, NICHT DER TEXT (07.09.2026). + + Hier stand `rollenPlan.includes("'spicy'")`. Das ist eine Suche im + ganzen gespeicherten Bauplan -- und SQLite hebt den woertlich auf, + mitsamt allen Kommentaren darin. Ein erklaerender Satz mit dem Wort + 'spicy' genuegte, und die Umstellung hielt die Tabelle fuer schon + umgestellt, obwohl die CHECK-Regel noch die alte war. Beim Bauen + genau so passiert. + + Jetzt wird die Regel selbst herausgeschnitten und NUR darin gesucht. + Findet sich keine, gibt es nichts umzustellen. */ + const regel = rollenPlan.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || ""; + if (rollenPlan && regel && !regel.includes("'spicy'")) { + const sicherung = `${DB_PFAD}.vor-spicy-${jetztStempel}`; + try { + d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`); + console.log("[workspace] Sicherung vor der Umstellung:", sicherung); + } catch (fehler) { + console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message); + return; + } + + /* Nur die Rollenliste ersetzen -- und pruefen, dass es geklappt hat. */ + const neuerPlan = rollenPlan + /* `IF NOT EXISTS` MUSS MIT INS MUSTER (07.09.2026, gefunden von + pruef-spicy). SQLite hebt den CREATE-Text WOERTLICH auf -- die + Tabelle wurde mit `CREATE TABLE IF NOT EXISTS personen` angelegt, + also steht das auch in sqlite_master. Ohne diese drei Woerter im + Muster griff die Ersetzung nicht, der Bauplan blieb unveraendert, + die Schutzabfrage darunter schlug an -- und die Umstellung waere + auf dem echten Server NIE gelaufen. Sie haette es gesagt (das ist + der Wert der Abfrage), aber sie waere nie gelaufen. */ + .replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i, + "CREATE TABLE personen_neu") + .replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('spicy','admin','manager','scout','creator')"); + if (!neuerPlan.includes("'spicy'") || !neuerPlan.includes("personen_neu")) { + console.error("[workspace] Umstellung abgebrochen: Der Bauplan liess sich nicht " + + "umschreiben. Steht die CHECK-Regel noch so da wie erwartet?"); + return; + } + + /* Die Spaltenliste kommt aus der Tabelle, nicht aus dem Gedaechtnis. */ + const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); + if (!spalten.length) { + console.error("[workspace] Umstellung abgebrochen: keine Spalten gefunden."); + return; + } + const liste = spalten.map((n) => `"${n}"`).join(", "); + + d.exec("PRAGMA foreign_keys = OFF"); + try { + const vorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + d.exec("BEGIN"); + d.exec(neuerPlan); + d.exec(`INSERT INTO personen_neu (${liste}) SELECT ${liste} FROM personen;`); + const nachher = d.prepare("SELECT COUNT(*) AS n FROM personen_neu").get().n; + /* Die Zaehlung steht INNERHALB der Transaktion -- stimmt sie nicht, + wird zurueckgerollt und die alte Tabelle bleibt unberuehrt. */ + if (nachher !== vorher) { + d.exec("ROLLBACK"); + console.error(`[workspace] Umstellung abgebrochen: ${vorher} Personen vorher, ` + + `${nachher} nachher. Sicherung: ${sicherung}`); + } else { + d.exec("DROP TABLE personen;"); + d.exec("ALTER TABLE personen_neu RENAME TO personen;"); + d.exec("COMMIT"); + const kaputt = d.prepare("PRAGMA foreign_key_check").all(); + if (kaputt.length) { + console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length, + "verwaiste Verweise. Sicherung liegt unter", sicherung); + } else { + console.log(`[workspace] Rolle 'spicy' freigeschaltet, ${nachher} Personen, ` + + `${spalten.length} Spalten uebernommen, Verweise geprueft.`); + } + } + } catch (fehler) { + try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ } + console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message, + "-- Sicherung:", sicherung); + } finally { + d.exec("PRAGMA foreign_keys = ON"); + } + } } export function db() { @@ -648,10 +824,23 @@ export function db() { PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; + /* ROLLENLISTE: 'spicy' steht hier mit drin, nicht nur in der + Umstellung (07.09.2026, gefunden von pruef-spicy). Vorher legte + eine frische Datenbank die alte Liste an, und die Umstellung + lief beim allerersten Start sofort hinterher -- Tabelle neu + bauen, Sicherung schreiben, umbenennen, fuer nichts. Ein + Bauplan, der sofort umgebaut werden muss, ist der falsche. + + KEIN KOMMENTAR INNERHALB DIESES CREATE-TEXTES. SQLite hebt ihn + woertlich in sqlite_master auf -- samt Kommentaren. Ein Satz mit + dem Wort 'spicy' darin haette bedeutet, dass die Umstellung die + Tabelle fuer bereits umgestellt haelt, obwohl die CHECK-Regel + noch die alte ist. Genau das ist beim Bauen passiert: Der + Bauplan enthielt das Wort, die Regel nicht. */ CREATE TABLE IF NOT EXISTS personen ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - rolle TEXT NOT NULL CHECK (rolle IN ('admin','manager','scout','creator')), + rolle TEXT NOT NULL CHECK (rolle IN ('spicy','admin','manager','scout','creator')), code_hash TEXT NOT NULL, code_salt TEXT NOT NULL, code_n INTEGER NOT NULL, @@ -1456,27 +1645,49 @@ const GESCHUETZT = { Lesezeichen hat, waere weiterhin hineingekommen. Beides gehoert zusammen: bereiche.js zeigt sie nur noch DogFather, diese Zeile laesst nur ihn hinein. */ - "/workspace/leistung.html": ["admin"], + "/workspace/leistung.html": ["spicy", "admin"], /* 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". */ - "/workspace/personen.html": ["admin"], - "/workspace/profil.html": ["admin", "manager", "scout", "creator"], + /* 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. */ + "/workspace/personen.html": ["spicy", "admin", "manager"], + "/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. */ - "/workspace/steckbrief.html": ["admin", "manager", "scout", "creator"], + "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator"], "/workspace/kalender.html": null, "/workspace/dateien.html": null, - "/workspace/bereich.html": ["admin", "manager", "scout", "creator"], - "/workspace/report.html": ["admin", "manager", "scout", "creator"], - "/workspace/scouting.html": ["admin", "manager", "scout"], + "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator"], + "/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"], + "/workspace/scouting.html": ["spicy", "admin", "manager", "scout"], "/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"], }; @@ -1870,7 +2081,7 @@ export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) { * null = alle (nur DogFather). */ export function sichtbareCreatorIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; if (person.rolle === "creator") return [person.id]; return betreuteIds(person); // Manager: eigene + die seiner Scouts } @@ -1883,7 +2094,7 @@ export function sichtbareCreatorIds(person) { * seine Scouts dazu -- er führt sie, er muss sie eintragen können. */ export function sichtbarePersonenIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; const creator = sichtbareCreatorIds(person) || []; const scouts = person.rolle === "manager" ? scoutsVon(person.id) : []; /* DOGFATHER IST FUER ALLE SICHTBAR (07.09.2026, Wunsch Filipe: @@ -1968,7 +2179,7 @@ export function betreuerIds(person) { * null = alle (nur DogFather), wie überall in dieser Datei. */ export function einladbareIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; const sichtbar = sichtbarePersonenIds(person) || []; /* DogFather gehört immer dazu: Er ist der Einzige, mit dem jede Rolle zu tun hat, und ihn nicht einladen zu können wäre der erste @@ -2001,7 +2212,7 @@ export function einladbareIds(person) { * null = alle (nur DogFather). */ export function schreibbareIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; const basis = new Set(einladbareIds(person) || []); @@ -2118,7 +2329,7 @@ export const externSql = (personSpalte, externSpalte) => spaeter auseinandergelaufen -- und dann haette eine Wiederholung jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */ export function termineSichtbar(person, praefix = "t") { - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; const p = praefix; /* MITGEZAEHLT WIRD AUCH DIE TEILNEHMERLISTE (05.09.2026). @@ -2236,7 +2447,7 @@ export function darfCreator(person, creatorId) { Ein Manager faellt jetzt in dieselbe Zeile wie ein Scout -- die Kette "eigene plus die meiner Scouts" steckt in betreuteIds. */ - if (istDogFather(person)) return true; + if (siehtAlles(person)) return true; if (person.rolle === "creator") return person.id === Number(creatorId); return betreuteIds(person).includes(Number(creatorId)); } diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index 87c93636..ca754260 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -1543,6 +1543,9 @@ body.gate { Scout Grün -- Schild Creator Blau -- Person Alle gedeckt, keine davon neon: augenschonend bleibt Vorgabe. */ + /* Spicy Media traegt das Rot der Marke -- dieselbe Farbe wie ihr Name + im Kopf der Karte darueber. */ + .tafel .rolle[data-rolle="spicy"] { --rton: #e9848c; } .tafel .rolle[data-rolle="admin"] { --rton: var(--k-gold); } .tafel .rolle[data-rolle="manager"] { --rton: var(--k-mittel); } .tafel .rolle[data-rolle="scout"] { --rton: var(--k-gut); } diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index 891fb23c..b8bd334f 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -301,3 +301,19 @@ .rollenwahl__knopf::after { border-color: CanvasText; } .rollenwahl__knopf[data-an="ja"]::after { background: Highlight; color: HighlightText; } } + +/* ===================================================================== + ZWEI AUSBAUSTUFEN DERSELBEN SEITE (07.09.2026) + + DogFather sieht diese Seite wie bisher. Manager und Spicy Media sehen + nur das Anlegen -- alles andere hier haengt am Server an `nurAdmin` + und antwortet ihnen mit 404. + + DAS AUSBLENDEN IST NICHT DIE SICHERUNG, sondern die Zusage, dass + niemand vor leeren Kaesten sitzt, die sich nie fuellen. Der Schutz + steht in den Schnittstellen; wuerde diese Regel fehlen, saehen sie + Ladefehler, aber keine Daten. + ===================================================================== */ +body[data-nur-anlegen="ja"] #liste, +body[data-nur-anlegen="ja"] .protokoll-block, +body[data-nur-anlegen="ja"] #rest-schalter { display: none; } diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index b4f9aeb0..62bf53fd 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -310,22 +310,22 @@ unter: 'Was du sowieso jeden Tag aufmachst', bereiche: [ { name: 'Dashboard', unter: 'Alle Creator auf einen Blick', zeichen: 'dashboard', ton: 1, - gross: true, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'uebersicht.html', szene: 'showbuehne' }, + gross: true, rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'uebersicht.html', szene: 'showbuehne' }, { name: 'Aufgaben', unter: 'Offen, in Arbeit, Review', zeichen: 'aufgaben', ton: 13, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'aufgaben.html', szene: 'garage' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'aufgaben.html', szene: 'garage' }, { name: 'Kalender', unter: 'Termine und Calls', zeichen: 'kalender', ton: 8, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'kalender.html', szene: 'skyline' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'kalender.html', szene: 'skyline' }, { name: 'Calls & Protokolle', unter: 'Gespräche mit To-dos', zeichen: 'calls', ton: 15, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'calls.html', szene: 'lounge' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'calls.html', szene: 'lounge' }, /* CHAT (06.09.2026). Steht neben "Calls & Protokolle", weil beides Gespraech ist -- das eine geplant und protokolliert, das andere zwischendurch. Ton 14 (Gruen), weil die taegliche Gruppe sonst 3, 11, 16, 9 und 2 traegt und sich der Chat davon abheben soll. */ { name: 'Chat', unter: 'Nachrichten mit deinem Team', zeichen: 'chat', ton: 4, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'chat.html', szene: 'lounge' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'chat.html', szene: 'lounge' }, { name: 'Dateien', unter: 'Ablage und Freigaben', zeichen: 'dateien', ton: 19, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'dateien.html', szene: 'arena' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'dateien.html', szene: 'arena' }, ], }, { @@ -370,9 +370,9 @@ erst wer jemand IST, dann was er vorhat, dann was dabei herauskommt. Die Zahlen zuerst zu zeigen dreht das um. */ { name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 10, - rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' }, { name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 20, - rollen: ['admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' }, /* KEINE eigene Steckbrief-Kachel fuer Creator, und das ist Absicht: Ihr Steckbrief samt Profilbild steht bereits auf `profil.html` (der Block mit `data-nur-creator="ja"`). Eine @@ -389,19 +389,19 @@ wegzunehmen, ohne die Seite zu schliessen, waere nur ein versteckter Weg und keine Regel. */ { name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2, - rollen: ['admin'], ziel: 'leistung.html', szene: 'arena' }, + rollen: ['spicy', 'admin'], ziel: 'leistung.html', szene: 'arena' }, { name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 16, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' }, { name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 3, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=live', szene: 'portal' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=live', szene: 'portal' }, { name: 'Content-Ideen', unter: 'Idee bis Upload', zeichen: 'content', ton: 12, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'content.html', szene: 'portal' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'content.html', szene: 'portal' }, { name: 'Community', unter: 'Moderation und Konflikte', zeichen: 'community', ton: 6, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=community', szene: 'lounge' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=community', szene: 'lounge' }, { name: 'Technik', unter: 'Setup, Fehler, Lösungen', zeichen: 'technik', ton: 14, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=technik', szene: 'garage' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=technik', szene: 'garage' }, { name: 'Schutz & Regeln', unter: 'Richtlinien und Vorfälle', zeichen: 'schutz', ton: 5, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=schutz', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=schutz', szene: 'halle' }, /* DER SECHSTE BEREICH (06.09.2026). Kernprinzip 04 des Konzepts lautet "Die Agentur bleibt angebunden" -- und dafuer gab es bis heute nichts. Fuenf Bereiche standen, dieser fehlte. @@ -415,9 +415,9 @@ von pruef-start-ansicht. Ein Untertitel, der mitten im Wort endet, ist schlechter als ein kuerzerer. */ { name: 'Agentur', unter: 'Kampagnen und offizielle Wege', zeichen: 'agentur', ton: 17, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=agentur', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=agentur', szene: 'halle' }, { name: 'Reports & Review', unter: 'Woche und Entscheidung', zeichen: 'reports', ton: 7, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' }, ], }, { @@ -443,11 +443,17 @@ einzeln: Man weiss danach nicht mehr, welcher von beiden man glauben soll. */ { name: 'Personen & Zugänge', unter: 'Rollen und Codes', zeichen: 'personen', ton: 11, - rollen: ['admin'], ziel: 'personen.html', szene: 'halle' }, + /* MANAGER SEHEN DIESE KACHEL JETZT AUCH (07.09.2026). + Filipe: "die manager sehen immer noch nicht die kachel + personen & zugangscode, wo sie dan die neuen creator + hinzufuegen koennen." Die SEITE zeigt ihnen nur das + Anlegen -- Codes, Sperren, Loeschen und das Protokoll + haengen weiterhin an DogFather (siehe personen.js). */ + rollen: ['spicy', 'admin', 'manager'], ziel: 'personen.html', szene: 'halle' }, { name: 'Scout-Pipeline', unter: 'Entdeckt bis übergeben', zeichen: 'scouting', ton: 21, - rollen: ['admin', 'manager', 'scout'], ziel: 'scouting.html', szene: 'wald' }, + rollen: ['spicy', 'admin', 'manager', 'scout'], ziel: 'scouting.html', szene: 'wald' }, { name: 'Wissens-Bibliothek', unter: 'Anleitungen und PDFs', zeichen: 'wissen', ton: 9, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'wissen.html', szene: 'arena' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'wissen.html', szene: 'arena' }, /* Ebenfalls nur DogFather (02.09.2026). Was von allein passiert, legt fest, was ALLEN anderen automatisch zugeschoben wird -- das gehört in dieselbe Hand wie die Rollenvergabe. */ diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 28b7877d..78374f58 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -15,10 +15,15 @@ /* Reihenfolge und Erklaerung an EINER Stelle. Sie gilt fuer die Rollenwahl im Formular und fuer alles, was sonst Rollen auflistet. */ const ROLLEN = [ + { wert: 'spicy', name: 'Spicy Media', symbol: 'chili', + text: 'Überblick über alle – ohne Automationen, ohne fremde Chats. Legt Leute an.' }, { wert: 'admin', name: 'DogFather', symbol: 'krone', - text: 'Alle Rechte, endgültig. Nur DogFather legt DogFather und Manager an.' }, + text: 'Alle Rechte, endgültig. Nur DogFather legt DogFather, Spicy Media und Manager an.' }, + /* Der Satz stimmte bis zum 07.09.2026 -- seither legen Manager + Creator an, und ein Text, der etwas anderes verspricht als die + Oberflaeche tut, ist schlimmer als gar keiner. */ { wert: 'manager', name: 'Manager', symbol: 'stern', - text: 'Dieselben Rechte wie DogFather – außer der Personenverwaltung.' }, + text: 'Dieselben Rechte wie DogFather, ohne Personenverwaltung – legt aber Creator an.' }, { wert: 'scout', name: 'Scout', symbol: 'schutz', text: 'Eigene Pipeline und die Creator, die ihm zugeteilt sind.' }, { wert: 'creator', name: 'Creator', symbol: 'person', @@ -32,7 +37,19 @@ /* DogFather und Manager kann nur DogFather vergeben -- der Server lehnt es ohnehin ab, aber ein Knopf, der immer scheitert, gehoert gar nicht erst hin. */ - for (const r of ROLLEN) { + /* WER WELCHE ROLLE VERGEBEN DARF (07.09.2026). + + DogFather: alle. Spicy Media: Manager und Creator. Manager: nur + Creator. Der Server setzt dasselbe durch (drei getrennte Wege, + siehe workspace-personen.js) -- hier geht es nur darum, niemandem + einen Knopf hinzustellen, der eine Absage bringt. + + Aus derselben Liste wie die Beschreibungen, damit die beiden nicht + auseinanderlaufen. */ + const darf = ich?.rolle === 'admin' ? ['spicy', 'admin', 'manager', 'scout', 'creator'] + : ich?.rolle === 'spicy' ? ['manager', 'creator'] + : ['creator']; + for (const r of ROLLEN.filter((x) => darf.includes(x.wert))) { if ((r.wert === 'admin' || r.wert === 'manager') && ich.rolle !== 'admin') continue; const b = el('button', 'rollenwahl__knopf'); b.type = 'button'; @@ -627,10 +644,27 @@ const knopf = $('neu-speichern'); knopf.disabled = true; try { - const a = await hole('/workspace/api/verwaltung/personen', { + /* DREI TUEREN, EINE AUSWAHL (07.09.2026). + + DogFather geht durch die Verwaltungstuer und kann jede Rolle + vergeben. Manager und Spicy Media haben dort nichts zu suchen -- + fuer sie gibt es zwei enge Tueren, die je genau eine Rolle + anlegen und die Rolle gar nicht erst als Feld annehmen. + + Die Zuordnung steht hier und nicht in einer Abfrage im Server: + Der Server prueft ohnehin, WER durch welche Tuer darf. Diese + Zeilen sorgen nur dafuer, dass niemand gegen eine verschlossene + laeuft. */ + const rolle = gewaehlteRolle(); + const weg = ich.rolle === 'admin' ? '/workspace/api/verwaltung/personen' + : rolle === 'manager' ? '/workspace/api/manager-anlegen' + : '/workspace/api/creator-anlegen'; + const a = await hole(weg, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: $('f-name').value, rolle: gewaehlteRolle() }), + body: JSON.stringify(ich.rolle === 'admin' + ? { name: $('f-name').value, rolle } + : { name: $('f-name').value }), }); if (!a.ok) { $('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.'; @@ -653,6 +687,43 @@ } catch { return; } if (!LEITUNG.has(ich.rolle)) { location.assign('/workspace/start.html'); return; } window.werZeigen?.(ich); + + /* =================================================================== + DIE SEITE HAT ZWEI AUSBAUSTUFEN (07.09.2026) + + Filipe: "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." + + DogFather bekommt die Seite wie bisher: Liste, Codes, Sperren, + Loeschen, Protokoll. Manager und Spicy Media bekommen genau EINEN + Teil davon -- das Anlegen. + + DAS VERSTECKEN IST NICHT DIE SICHERUNG. Alles, was hier + ausgeblendet wird, haengt am Server an `nurAdmin` und antwortet + mit 404. Wuerde diese Zeile fehlen, saehen sie leere Kaesten und + Fehlermeldungen -- aber keine Daten. Das Ausblenden erspart ihnen + nur den Anblick von Dingen, die es fuer sie nicht gibt. + + Und deshalb wird auch `laden()` NICHT aufgerufen: Es fragt die + Verwaltungsschnittstelle ab, und die ist zu. Eine Abfrage, von der + man weiss, dass sie 404 gibt, sollte man nicht stellen -- sie + fuellt nur das Serverlog. */ + const nurAnlegen = ich.rolle !== 'admin'; + if (nurAnlegen) { + document.body.dataset.nurAnlegen = 'ja'; + $('unterzeile') && ($('unterzeile').textContent = + 'Hier legst du neue Leute an. Codes, Sperren und das Protokoll bleiben bei DogFather.'); + baueRollenwahl(); + /* Das Formular steht offen da -- es ist der einzige Grund, warum + diese Seite fuer sie ueberhaupt aufgeht. Ein Knopf, der eine + leere Seite oeffnet, waere ein Klick ohne Zweck. */ + $('neu').hidden = false; + $('neu-oeffnen').hidden = true; + $('neu-abbrechen').hidden = true; + return; + } + baueRollenwahl(); await laden(); })(); diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index bc992a5a..120d5bdf 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -27,8 +27,12 @@ 'use strict'; const ROLLENTEXT = { + spicy: 'Spicy Media · Überblick über alle, ohne Automationen', admin: 'DogFather · Gesamtübersicht und Freigaben', - manager: 'Manager · dieselben Rechte, ohne Personenverwaltung', + /* Der Satz stimmte bis zum 07.09.2026. Seither legen Manager + Creator an -- und ein Rollentext, der etwas anderes verspricht als + die Oberflaeche kann, ist schlimmer als gar keiner. */ + manager: 'Manager · dieselben Rechte, ohne Personenverwaltung – legt Creator an', creator: 'Creator · dein eigener Bereich', scout: 'Scout · deine Pipeline und die Creator, die du betreust', }; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index cd2a7ed8..0cdfa329 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,9 @@ - - - + + +
@@ -211,10 +211,10 @@ - - - - - + + + + +