diff --git a/server/pruef-ampel.png b/server/pruef-ampel.png index f5cb27a8..2ec06450 100644 Binary files a/server/pruef-ampel.png and b/server/pruef-ampel.png differ diff --git a/server/pruef-aufgabenbrett.png b/server/pruef-aufgabenbrett.png index 642a0130..9544dcab 100644 Binary files a/server/pruef-aufgabenbrett.png and b/server/pruef-aufgabenbrett.png differ diff --git a/server/pruef-code.png b/server/pruef-code.png index d5f9fe2e..62480d07 100644 Binary files a/server/pruef-code.png and b/server/pruef-code.png differ diff --git a/server/pruef-freie-namen.png b/server/pruef-freie-namen.png index fdf1867e..e43dadaa 100644 Binary files a/server/pruef-freie-namen.png and b/server/pruef-freie-namen.png differ diff --git a/server/pruef-handy.png b/server/pruef-handy.png index d6590c23..9fed8350 100644 Binary files a/server/pruef-handy.png and b/server/pruef-handy.png differ diff --git a/server/pruef-kalender-computer.png b/server/pruef-kalender-computer.png index a69a3f18..7713772e 100644 Binary files a/server/pruef-kalender-computer.png and b/server/pruef-kalender-computer.png differ diff --git a/server/pruef-kalender-handy.png b/server/pruef-kalender-handy.png index b065742f..94af3450 100644 Binary files a/server/pruef-kalender-handy.png and b/server/pruef-kalender-handy.png differ diff --git a/server/pruef-manager-sicht.mjs b/server/pruef-manager-sicht.mjs new file mode 100644 index 00000000..8c45ab02 --- /dev/null +++ b/server/pruef-manager-sicht.mjs @@ -0,0 +1,273 @@ +/* ===================================================================== + LECKSUCHE: Sieht eine Managerin wirklich nur ihre eigenen Creator? + + Wunsch vom 03.09.2026: "jeder manager soll auch immer nur seine und + die seiner scouts zugeteilten creator und creator daten sehen. und + nicht die der anderen." + + --------------------------------------------------------------------- + WARUM DAS NICHT MIT LESEN ZU BEANTWORTEN IST + + Die Kette "Manager -> seine Scouts -> deren Creator" ist gebaut und + steht an genau einer Stelle (betreuteIds). Die Frage ist eine andere: + Benutzt sie auch JEDER Weg, der Creator-Daten herausgibt? Es gibt + ueber zwanzig Stellen, an denen die Rolle geprueft wird statt der + Zuteilung -- und ein einziger vergessener Weg reicht. + + Deshalb wird hier nicht Quelltext gelesen, sondern GEMESSEN: + + * Zwei Managerinnen mit vollstaendig getrennten Creators. + * Bei der fremden Creatorin heisst ALLES "GEHEIM..." -- Aufgabe, + Termin, Bereichseintrag, Datei, Steckbrief, Notiz, Content. + * Danach wird jede Leseschnittstelle abgefragt und die ganze + Antwort nach "GEHEIM" durchsucht. + + Ein Leck faellt damit auf, egal an welcher Stelle es sitzt und egal, + ob ich es beim Lesen uebersehen haette. Das ist der Unterschied + zwischen "ich habe nichts gefunden" und "es ist nichts da". + + Die GEGENPROBE ist genauso wichtig: Dieselbe Managerin MUSS ihre + eigenen Creator sehen -- den direkt zugeteilten und den ueber ihren + Scout. Ohne sie waere jede Zeile oben auch dann gruen, wenn sie + ueberhaupt nichts mehr saehe. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-msicht-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4199"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4199"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const tag = (v) => new Date(Date.now() + v * 86400_000).toISOString().slice(0, 10); + +function person(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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} + +const idDogi = person("Dogfather", "admin", "CODE-DOGI-0001"); +/* Seite A -- Cigdem, ihr Scout Ben, und zwei Creator: einer direkt, + einer ueber den Scout. Genau die Kette aus dem Wunsch. */ +const idCigdem = person("Cigdem", "manager", "CODE-CIGDEM-01"); +const idBen = person("Ben", "scout", "CODE-BEN-00001"); +const idTili = person("Tili", "creator", "CODE-TILI-0001"); +const idLuna = person("Luna", "creator", "CODE-LUNA-0001"); +/* Seite B -- eine voellig fremde Managerin mit eigener Creatorin. */ +const idDana = person("Dana", "manager", "CODE-DANA-0001"); +const idZoe = person("GEHEIMZoe", "creator", "CODE-ZOE-00001"); + +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idCigdem, jetzt); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idBen, jetzt); +d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idBen, idCigdem, jetzt); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idZoe, idDana, jetzt); + +/* Alles von Zoe traegt das Wort GEHEIM. Taucht es irgendwo in einer + Antwort an Cigdem auf, ist genau dort ein Leck. */ +d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id, frist, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?,?,?)`) + .run("GEHEIM Aufgabe", "GEHEIM Beschreibung", "offen", "hoch", idZoe, idZoe, tag(1), jetzt, idDana); +d.prepare(`INSERT INTO termine (titel, beschreibung, art, beginn, dauer_min, creator_id, teilnehmer_id, erledigt, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?,0,?,?)`) + .run("GEHEIM Call", "GEHEIM Notiz", "call", tag(-1) + "T18:00", 30, idZoe, idZoe, jetzt, idDana); +for (const bereich of ["live", "content", "community", "technik", "schutz"]) { + d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, creator_id, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?,?,?)`) + .run(bereich, bereich === "content" ? "idee" : "vorbereitung", + "GEHEIM " + bereich, "GEHEIM Text", tag(0), "offen", idZoe, jetzt, idDana); +} +d.prepare(`INSERT INTO dateien (name_original, name_datei, groesse, typ, status, notiz, creator_id, hochgeladen_von, erstellt) + VALUES (?,?,?,?,?,?,?,?,?)`) + .run("GEHEIM.pdf", "x-geheim.pdf", 10, "application/pdf", "entwurf", "GEHEIM Notiz", idZoe, idDana, jetzt); +d.prepare("UPDATE personen SET ueber_mich = ?, tiktok = ? WHERE id = ?") + .run("GEHEIM Steckbrief", "GEHEIMhandle", idZoe); +d.prepare("INSERT INTO content_saeulen (creator_id, name, ziel, slot, erstellt) VALUES (?,?,?,?,?)") + .run(idZoe, "GEHEIM Saeule", 40, 1, jetzt); + +/* Und dasselbe harmlos fuer Cigdems eigene Creator -- fuer die + Gegenprobe. */ +d.prepare(`INSERT INTO aufgaben (titel, status, prioritaet, creator_id, verantwortlich_id, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?)`) + .run("Tili-Aufgabe", "offen", "mittel", idTili, idTili, jetzt, idDogi); +d.prepare(`INSERT INTO aufgaben (titel, status, prioritaet, creator_id, verantwortlich_id, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?)`) + .run("Luna-Aufgabe", "offen", "mittel", idLuna, idLuna, jetzt, idDogi); +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function anmelden(rolle, code) { + const kontext = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + return seite; +} +const roh = (seite, pfad) => seite.evaluate(async (p) => { + try { + const a = await fetch(p, { credentials: "same-origin" }); + let text = (await a.text()).slice(0, 60000); + /* Die Suche gibt den SUCHBEGRIFF in ihrer Antwort zurück + ("frage": "..."), damit die Seite ihn anzeigen kann. Sucht man + nach "GEHEIM", steht das Wort also zwangsläufig in der Antwort -- + auch bei null Treffern. + + Beim ersten Anlauf hat genau das einen Fehlalarm ausgelöst, und + ich hätte um ein Haar ein Leck "repariert", das es nie gab. Das + Echo wird deshalb entfernt, BEVOR gesucht wird -- gemessen wird + nur, was die Suche GEFUNDEN hat. */ + text = text.replace(/"frage"\s*:\s*"[^"]*"/g, '"frage":""'); + return { status: a.status, text }; + } catch (e) { return { status: 0, text: "" }; } +}, pfad); + +const cigdem = await anmelden("manager", "CODE-CIGDEM-01"); + +/* ---------- Jede Leseschnittstelle, die es gibt ------------------------- */ + +const WEGE = [ + "/workspace/api/ich", + "/workspace/api/aufgaben", + "/workspace/api/uebersicht", + "/workspace/api/uebersicht/creator", + "/workspace/api/personen", + `/workspace/api/termine?von=${tag(-30)}&tage=90`, + "/workspace/api/serien", + "/workspace/api/calls", + "/workspace/api/bereich/live", + "/workspace/api/bereich/content", + "/workspace/api/bereich/community", + "/workspace/api/bereich/technik", + "/workspace/api/bereich/schutz", + "/workspace/api/content/saeulen", + "/workspace/api/content/kennzahlen", + "/workspace/api/dateien", + "/workspace/api/hinweise", + "/workspace/api/suche?q=GEHEIM", + "/workspace/api/suche?q=Zoe", + "/workspace/api/report", + "/workspace/api/report/creator", + "/workspace/api/startcheck/creator", + "/workspace/api/steckbrief", + "/workspace/api/profil", + "/workspace/api/leads", + "/workspace/api/leads-scouts", + "/workspace/api/schulung", + "/workspace/api/wissen", + "/workspace/api/vorlagen", + "/workspace/api/eintrag/ampeln", + "/workspace/api/checkliste/live", +]; + +console.log("\n— Sickert etwas von der fremden Creatorin durch? —"); + +/* DIE STATUSPRÜFUNG IST KEIN BEIWERK. + + Beim Stopfen dieser Lecks ist mir genau das passiert: Ein Import + fehlte, der Weg warf einen Fehler, antwortete mit 503 -- und weil in + einer Fehlermeldung natürlich kein "GEHEIM" steht, meldete die + Prüfung brav "kein Leck". Sie war grün, weil der Weg KAPUTT war. + + Eine Prüfung, die einen Ausfall als Erfolg liest, ist schlimmer als + keine. Deshalb zählt hier beides: Es darf nichts durchsickern UND der + Weg muss antworten. */ +const lecks = []; +for (const weg of WEGE) { + const a = await roh(cigdem, weg); + const leck = /GEHEIM/i.test(a.text); + const kaputt = a.status === 0 || a.status >= 500; + if (leck) { + lecks.push(`${weg} (${a.status})`); + /* Bei einem Fund gleich zeigen, WO er steckt -- sonst sucht man + die Nadel anschliessend von Hand im ganzen Heuhaufen. */ + const stelle = a.text.search(/GEHEIM/i); + lecks.push(" …" + a.text.slice(Math.max(0, stelle - 120), stelle + 120) + "…"); + } + if (kaputt) lecks.push(`${weg} ANTWORTET NICHT (${a.status})`); + ok(!leck && !kaputt, + `${weg.replace("/workspace/api/", "").padEnd(34)} ${a.status}` + + `${leck ? " ← LECK" : ""}${kaputt ? " ← ANTWORTET NICHT" : ""}`); +} + +/* ---------- Auch direkte Zugriffe auf fremde Nummern -------------------- */ + +console.log("\n— Und wenn sie die Nummer der Fremden kennt? —"); + +for (const [weg, was] of [ + [`/workspace/api/steckbrief/${idZoe}`, "fremder Steckbrief"], + [`/workspace/api/profil/${idZoe}`, "fremdes Profil"], + [`/workspace/api/startcheck/${idZoe}`, "fremder Start-Check"], + [`/workspace/api/report?creator=${idZoe}`, "fremder Report"], + [`/workspace/api/content/kennzahlen?creator=${idZoe}`, "fremde Content-Zahlen"], + [`/workspace/api/uebersicht/creator?id=${idZoe}`, "fremde Übersicht"], +]) { + const a = await roh(cigdem, weg); + const leck = /GEHEIM/i.test(a.text); + ok(!leck, `${was.padEnd(24)} ${a.status}${leck ? " ← LECK" : ""}`); +} + +/* ---------- GEGENPROBE: ihre eigenen sieht sie sehr wohl ---------------- */ + +console.log("\n— Gegenprobe: ihre eigenen Creator —"); + +const eigene = await roh(cigdem, "/workspace/api/aufgaben"); +ok(/Tili-Aufgabe/.test(eigene.text), + "sie sieht den DIREKT zugeteilten Creator (Tili)"); +ok(/Luna-Aufgabe/.test(eigene.text), + "und den über ihren Scout (Luna) – die Kette Manager → Scout → Creator trägt"); + +const dana = await anmelden("manager", "CODE-DANA-0001"); +const beiDana = await roh(dana, "/workspace/api/aufgaben"); +ok(/GEHEIM/.test(beiDana.text), + "GEGENPROBE ANDERSHERUM: Dana sieht ihre eigene Creatorin sehr wohl"); +ok(!/Tili-Aufgabe/.test(beiDana.text) && !/Luna-Aufgabe/.test(beiDana.text), + "und umgekehrt keine von Cigdems – die Trennung gilt in beide Richtungen"); + +/* Und DogFather sieht weiterhin alles -- sonst waere aus der Trennung + eine Mauer geworden, die auch ihn aussperrt. */ +const dogi = await anmelden("admin", "CODE-DOGI-0001"); +const beiDogi = await roh(dogi, "/workspace/api/aufgaben"); +ok(/GEHEIM/.test(beiDogi.text) && /Tili-Aufgabe/.test(beiDogi.text), + "GEGENPROBE: DogFather sieht weiterhin beide Seiten"); + +if (lecks.length) { + console.log("\n--- LECKS ---"); + for (const l of lecks) console.log(" " + l); +} + +const ERWARTET = WEGE.length + 11; +console.log(""); +ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-personen-formular.png b/server/pruef-personen-formular.png index 9e3a3a63..53655395 100644 Binary files a/server/pruef-personen-formular.png and b/server/pruef-personen-formular.png differ diff --git a/server/pruef-personen.png b/server/pruef-personen.png index 78c9e6f1..65b48ce6 100644 Binary files a/server/pruef-personen.png and b/server/pruef-personen.png differ diff --git a/server/pruef-protokoll-loeschen.png b/server/pruef-protokoll-loeschen.png index 90451740..3767f1ef 100644 Binary files a/server/pruef-protokoll-loeschen.png and b/server/pruef-protokoll-loeschen.png differ diff --git a/server/pruef-rollen-admin.png b/server/pruef-rollen-admin.png index 1a6d4a4b..712749e4 100644 Binary files a/server/pruef-rollen-admin.png and b/server/pruef-rollen-admin.png differ diff --git a/server/pruef-rollen-creator.png b/server/pruef-rollen-creator.png index 27786159..c1550dca 100644 Binary files a/server/pruef-rollen-creator.png and b/server/pruef-rollen-creator.png differ diff --git a/server/pruef-rollen-manager.png b/server/pruef-rollen-manager.png index 6238a464..e1f4128f 100644 Binary files a/server/pruef-rollen-manager.png and b/server/pruef-rollen-manager.png differ diff --git a/server/pruef-rollen-scout.png b/server/pruef-rollen-scout.png index 1be40d4f..741b6a35 100644 Binary files a/server/pruef-rollen-scout.png and b/server/pruef-rollen-scout.png differ diff --git a/server/pruef-schulung.png b/server/pruef-schulung.png new file mode 100644 index 00000000..e1ea297f Binary files /dev/null and b/server/pruef-schulung.png differ diff --git a/server/pruef-scout-zuteilung.mjs b/server/pruef-scout-zuteilung.mjs index 03e4ba42..41c4e9a2 100644 --- a/server/pruef-scout-zuteilung.mjs +++ b/server/pruef-scout-zuteilung.mjs @@ -350,18 +350,33 @@ ok(bDogi.konsole.length === 0, await bDogi.seite.screenshot({ path: "pruef-scout-zuteilung.png", fullPage: false }); await bDogi.kontext.close(); -/* GEGENPROBE: Ein Manager darf die Personenliste sehen -- aber diese - Auswahl nicht. */ +/* GEGENPROBE, AM 02.09.2026 UMGEDREHT. + + Hier stand bis dahin: "ein Manager sieht die ganze Personenliste -- + die Personenverwaltung ist ihm nicht genommen worden". Das galt, war + aber ein Widerspruch zur eigenen Rollenbeschreibung ("Manager: + dieselben Rechte wie DogFather, AUSSER der Personenverwaltung"). + + Filipe hat den Widerspruch am 02.09.2026 zugunsten der Beschreibung + aufgeloest: "die manager sollen diese kategorien garnicht sehen." + Seitdem fuehrt personen.html fuer ihn zurueck zur Startseite. + + Die Pruefung wird deshalb nicht geloescht, sondern umgedreht -- eine + geloeschte Pruefung hinterlaesst keine Spur davon, dass hier einmal + etwas anderes galt. */ const bMara = await seiteAls("manager", "CODE-MANA-0001"); const rM = await bMara.seite.evaluate(() => ({ wahlen: document.querySelectorAll('select[id^="manager-"]').length, personen: document.querySelectorAll(".person").length, + wo: location.pathname, })); -ok(rM.personen >= 7, - `ein Manager sieht die ganze Personenliste (${rM.personen} Zeilen) – ` - + "die Personenverwaltung ist ihm nicht genommen worden"); +ok(rM.wo.endsWith("/start.html"), + `ein Manager landet auf der Startseite statt in der Personenverwaltung ` + + `(${rM.wo.split("/").pop()})`); +ok(rM.personen === 0, + `und sieht dort keine einzige Personenzeile (${rM.personen})`); ok(rM.wahlen === 0, - `Gegenprobe: aber KEINE Zuteilungs-Auswahl (${rM.wahlen}) – er koennte sich sonst ` + `erst recht keine Zuteilungs-Auswahl (${rM.wahlen}) – er koennte sich sonst ` + "seine eigene Sichtbarkeit vergeben"); await bMara.kontext.close(); diff --git a/server/pruef-scout-zuteilung.png b/server/pruef-scout-zuteilung.png new file mode 100644 index 00000000..33047bb2 Binary files /dev/null and b/server/pruef-scout-zuteilung.png differ diff --git a/server/pruef-serien-computer.png b/server/pruef-serien-computer.png index 049d774a..d440cc21 100644 Binary files a/server/pruef-serien-computer.png and b/server/pruef-serien-computer.png differ diff --git a/server/pruef-serien-handy.png b/server/pruef-serien-handy.png index 2997c0c6..11e904ad 100644 Binary files a/server/pruef-serien-handy.png and b/server/pruef-serien-handy.png differ diff --git a/server/pruef-serien-liste.png b/server/pruef-serien-liste.png index 6028c505..4d971aef 100644 Binary files a/server/pruef-serien-liste.png and b/server/pruef-serien-liste.png differ diff --git a/server/pruef-start-computer.png b/server/pruef-start-computer.png index 100dd72d..5ad2610f 100644 Binary files a/server/pruef-start-computer.png and b/server/pruef-start-computer.png differ diff --git a/server/pruef-start-creator.png b/server/pruef-start-creator.png index 1f2f93fb..065afb63 100644 Binary files a/server/pruef-start-creator.png and b/server/pruef-start-creator.png differ diff --git a/server/pruef-start-handy.png b/server/pruef-start-handy.png index 845a511a..e1e92b4a 100644 Binary files a/server/pruef-start-handy.png and b/server/pruef-start-handy.png differ diff --git a/server/pruef-steckbrief.png b/server/pruef-steckbrief.png new file mode 100644 index 00000000..7f99264a Binary files /dev/null and b/server/pruef-steckbrief.png differ diff --git a/server/pruef-team.png b/server/pruef-team.png index 2b4556d6..922e490f 100644 Binary files a/server/pruef-team.png and b/server/pruef-team.png differ diff --git a/server/pruef-uebersicht.mjs b/server/pruef-uebersicht.mjs index 8dc41849..c3bb0cb3 100644 --- a/server/pruef-uebersicht.mjs +++ b/server/pruef-uebersicht.mjs @@ -217,13 +217,51 @@ const kDogi = await anmelden("admin", "CODE-DOGI-0001"); ok(ben.hat_profil === 0, "Ben hat kein Profil"); } -console.log("\n3) Manager sieht dasselbe"); +/* AM 03.09.2026 UMGESCHRIEBEN. Hier stand "Manager sieht dasselbe" -- + also alle Creator, wie DogFather. + + Wunsch vom 03.09.2026: "jeder manager soll auch immer nur seine und + die seiner scouts zugeteilten creator und creator daten sehen. und + nicht die der anderen." Ein Manager OHNE Zuteilung sieht seitdem + keinen einzigen Creator -- und das ist kein Fehler, sondern die + Regel. + + Die Pruefung wird nicht geloescht, sondern gedreht: Erst der leere + Fall, dann derselbe Manager MIT Zuteilung. Sonst waere aus einer + Pruefung "sieht alles" eine Pruefung "sieht nichts" geworden -- und + die haette nicht bemerkt, wenn er auch seine eigenen verliert. */ +console.log("\n3) Manager sieht NUR seine zugeteilten Creator"); { const k = await anmelden("manager", "CODE-MANA-0001"); + const leer = await (await hole("/workspace/api/uebersicht/creator", k)).json(); + ok(leer.creator.length === 0, + `ohne Zuteilung kein einziger Creator (${leer.creator.length})`); + + /* Jetzt einen zuteilen -- und zwar mit denselben Mitteln wie die + Anwendung selbst, nicht per SQL am System vorbei. */ + /* WICHTIG: einen Creator nehmen, der noch KEINEN Betreuer hat. + `betreuung` haelt genau einen Betreuer je Creator -- ein + INSERT OR REPLACE auf einen bereits betreuten Creator nimmt dem + Scout seinen weg. Genau das ist beim ersten Anlauf passiert, und + Abschnitt 4 fiel daraufhin um: Die Pruefung hatte sich selbst die + Grundlage entzogen. */ + const dd = new DatabaseSync(process.env.WORKSPACE_DB); + const mara = dd.prepare("SELECT id FROM personen WHERE rolle = 'manager'").get().id; + const einer = dd.prepare(` + SELECT id, name FROM personen + WHERE rolle = 'creator' AND id NOT IN (SELECT creator_id FROM betreuung) + ORDER BY id`).get(); + if (!einer) throw new Error("Testaufbau: kein unbetreuter Creator vorhanden"); + dd.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)") + .run(einer.id, mara, new Date().toISOString()); + dd.close(); + const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json(); - ok(d2.creator.length === 2, `beide Creator (${d2.creator.length})`); - ok(d2.creator[0].naechster_review !== undefined - || d2.creator[1].naechster_review !== undefined, "Manager sieht Review-Termine"); + ok(d2.creator.length === 1, + `mit Zuteilung genau einer (${d2.creator.length}) – nicht alle, nicht keiner`); + ok(d2.creator[0]?.name === einer.name, + `und zwar der zugeteilte (${d2.creator[0]?.name})`); + ok(d2.creator[0]?.naechster_review !== undefined, "Manager sieht Review-Termine"); } console.log("\n4) Scout sieht nur die betreute Person"); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 01b5e845..81b9177a 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -11,7 +11,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, - externPruefen, externSql, + externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, } from "./workspace.js"; export const aufgabenRouter = express.Router(); @@ -163,13 +163,28 @@ aufgabenRouter.get("/workspace/api/uebersicht", (req, res) => { die Liste bewusst nicht -- sie sollen keine fremden Namen sehen. */ aufgabenRouter.get("/workspace/api/personen", (req, res) => { try { - if (istLeitung(req.person)) { + /* Nicht die ROLLE, sondern die ZUTEILUNG entscheidet (03.09.2026). + Hier stand eine Abfrage ueber ALLE -- eine Managerin bekam damit + auch Namen und Daten von Creators, die ihr nie zugeteilt waren. + sichtbarePersonenIds/sichtbareCreatorIds liefern null fuer + DogFather (= keine Einschraenkung) und sonst genau die erlaubten + Nummern. */ + const ids = sichtbarePersonenIds(req.person); + if (ids === null) { return res.json({ personen: db().prepare( "SELECT id, name, rolle FROM personen WHERE aktiv = 1 ORDER BY " + ROLLEN_SORTIERUNG + ", name").all(), }); } - res.json({ personen: [{ id: req.person.id, name: req.person.name, rolle: req.person.rolle }] }); + if (!ids.length) { + return res.json({ personen: [{ id: req.person.id, name: req.person.name, rolle: req.person.rolle }] }); + } + res.json({ + personen: db().prepare( + `SELECT id, name, rolle FROM personen WHERE aktiv = 1 + AND id IN (${ids.map(() => "?").join(",")}) + ORDER BY ` + ROLLEN_SORTIERUNG + ", name").all(...ids), + }); } catch { res.status(503).json({ fehler: "nicht_verfuegbar" }); } @@ -239,14 +254,16 @@ aufgabenRouter.get("/workspace/api/uebersicht/creator", (req, res) => { const person = req.person; let wo = "p.rolle = 'creator' AND p.aktiv = 1"; let werte = []; - if (person.rolle === "creator") { - wo += " AND p.id = ?"; - werte = [person.id]; - } else if (person.rolle === "scout") { - const ids = betreuteIds(person); - if (!ids.length) return res.json({ creator: [], eigen: false }); - wo += ` AND p.id IN (${ids.map(() => "?").join(",")})`; - werte = ids; + /* Zuteilung statt Rolle (03.09.2026): Ein Manager fiel hier durch + beide Zweige hindurch und bekam die Gesamtuebersicht ueber ALLE + Creator -- mit Namen, offenen Aufgaben, Terminen und dem Stand + ihres Start-Checks. sichtbareCreatorIds gibt allein DogFather + null (= keine Einschraenkung). */ + const erlaubt = sichtbareCreatorIds(person); + if (erlaubt !== null) { + if (!erlaubt.length) return res.json({ creator: [], eigen: person.rolle === "creator" }); + wo += ` AND p.id IN (${erlaubt.map(() => "?").join(",")})`; + werte = erlaubt; } /* ORTSZEIT: toISOString() liefert UTC und damit nachts den Vortag -- diff --git a/server/workspace-content.js b/server/workspace-content.js index 58945f70..5298f2d2 100644 --- a/server/workspace-content.js +++ b/server/workspace-content.js @@ -297,15 +297,35 @@ contentRouter.get("/workspace/api/content/kennzahlen", (req, res) => { laenger reagiert nicht mehr auf eine Kursaenderung. Gezaehlt wird nur VEROEFFENTLICHTES: Was auf dem Kanal steht, ist die Wahrheit ueber seine Ausrichtung, nicht was in der Ideenliste liegt. */ + /* Die Säulen brauchen dieselbe Einschränkung wie alles andere + (03.09.2026). Ohne sie zählte diese Abfrage die Säulen ALLER + Creator zusammen -- mit Namen. Eine Managerin bekam damit über + die Balance-Auswertung Themenschwerpunkte fremder Kanäle zu + sehen, ohne dass irgendwo ein Name gestanden hätte, den sie + öffnen könnte. Genau die Art Leck, die man beim Lesen übersieht: + Die Abfrage darüber (`wo`) war korrekt eingeschränkt, diese hier + hatte ihre eigene Bedingung. */ const vor90 = tagLokal(-90); + let saeulenWo = ""; + let saeulenWerte = []; + if (Number.isInteger(nur) && nur > 0) { + saeulenWo = "WHERE s.creator_id = ?"; + saeulenWerte = [nur]; + } else if (erlaubt !== null) { + if (!erlaubt.length) saeulenWo = "WHERE 0=1"; + else { + saeulenWo = `WHERE s.creator_id IN (${erlaubt.map(() => "?").join(",")})`; + saeulenWerte = erlaubt; + } + } const balance = d.prepare(` SELECT s.id, s.name, s.slot, s.ziel, COUNT(e.id) AS n FROM content_saeulen s LEFT JOIN eintraege e ON e.saeule_id = s.id AND e.bereich = 'content' AND e.art = 'veroeffentlicht' AND e.datum >= ? - ${Number.isInteger(nur) && nur > 0 ? "WHERE s.creator_id = ?" : ""} + ${saeulenWo} GROUP BY s.id ORDER BY s.slot`) - .all(...(Number.isInteger(nur) && nur > 0 ? [vor90, nur] : [vor90])); + .all(vor90, ...saeulenWerte); const ohneSaeule = d.prepare( `SELECT COUNT(*) AS n FROM eintraege e diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index bda93bd6..6c8f91cb 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -30,7 +30,7 @@ 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, - externSql, EXTERN_MAX, + externSql, EXTERN_MAX, sichtbarePersonenIds, } from "./workspace.js"; export const dateienRouter = express.Router(); @@ -119,11 +119,26 @@ export function freigabenZu(ids) { betreut -- sonst koennte er sich ueber eine Freigabe Zugang zu fremden Bereichen verschaffen. */ export function moeglicheEmpfaenger(person) { - if (istLeitung(person)) { + /* Nicht die ROLLE, sondern die ZUTEILUNG entscheidet (03.09.2026). + Hier stand eine Abfrage ueber ALLE -- eine Managerin bekam damit + auch Namen und Daten von Creators, die ihr nie zugeteilt waren. + sichtbarePersonenIds/sichtbareCreatorIds liefern null fuer + DogFather (= keine Einschraenkung) und sonst genau die erlaubten + Nummern. */ + const erlaubt = sichtbarePersonenIds(person); + if (erlaubt === null) { return db().prepare( "SELECT id, name, rolle FROM personen WHERE aktiv = 1 AND id <> ? ORDER BY " + ROLLEN_SORTIERUNG + ", name") .all(person.id); } + if (istLeitung(person)) { + const ohneMich = erlaubt.filter((x) => x !== person.id); + if (!ohneMich.length) return []; + return db().prepare( + `SELECT id, name, rolle FROM personen WHERE aktiv = 1 + AND id IN (${ohneMich.map(() => "?").join(",")}) + ORDER BY ` + ROLLEN_SORTIERUNG + ", name").all(...ohneMich); + } if (person.rolle === "scout") { const ids = betreuteIds(person); if (!ids.length) return []; diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index db2d285b..d8a2c735 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, termineSichtbar, heuteLokal, + db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, betreutWo, termineSichtbar, heuteLokal, externPruefen, externSql, } from "./workspace.js"; import { @@ -107,11 +107,26 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { /* Fristen aus den Aufgaben -- nur zum Anzeigen, nicht bearbeitbar. Die Sichtbarkeitsregel der Aufgaben gilt dabei unveraendert. */ - const aufgabenWo = istLeitung(req.person) + /* HIER STAND istLeitung -- und damit sah JEDE Managerin die Fristen + ALLER Aufgaben im Kalender, auch die von Creators, die ihr nie + zugeteilt waren. Gefunden bei der Lecksuche am 03.09.2026. + + Besonders unangenehm, weil es nicht wie ein Datenleck aussieht: + Im Kalender steht nur eine kleine orange Marke mit einem Titel. + Dass darin fremde Namen und Vorhaben stecken, faellt niemandem + auf, der nicht danach sucht. + + Jetzt gilt dieselbe Regel wie fuer die Termine daneben: eigene + plus die der zugeteilten Creator. */ + const sicht = req.sicht || req.person; + 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) ? { wo: "1=1", werte: [] } - : req.person.rolle === "creator" - ? { wo: "(a.creator_id = ? OR a.verantwortlich_id = ?)", werte: [req.person.id, req.person.id] } - : { wo: "a.verantwortlich_id = ?", werte: [req.person.id] }; + : bA + ? { wo: `(${eigenA} OR ${bA.wo})`, werte: [...werteA, ...bA.werte] } + : { wo: eigenA, werte: werteA }; const fristen = db().prepare(` SELECT a.id, a.titel, a.frist, a.status, a.prioritaet, pv.name AS verantwortlich_name diff --git a/server/workspace-profil.js b/server/workspace-profil.js index 6f5647d4..a2e29eb4 100644 --- a/server/workspace-profil.js +++ b/server/workspace-profil.js @@ -12,7 +12,9 @@ import express from "express"; import { + sichtbareCreatorIds, db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds, istLeitung, + istDogFather, } from "./workspace.js"; export const profilRouter = express.Router(); @@ -61,8 +63,17 @@ profilRouter.use("/workspace/api/profil", angemeldet); "eigen" = der Creator selbst. null = 404. */ function darf(person, zielId) { - if (istLeitung(person)) return "voll"; + /* "voll" bekommt allein DogFather (03.09.2026). Vorher stand hier + istLeitung -- und damit kam jede Managerin an JEDES Creator-Profil, + auch an das einer fremden Managerin, samt der internen Notiz. Es + reichte, die Nummer in die Adresse zu schreiben. + + 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 (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; return null; } @@ -109,14 +120,19 @@ profilRouter.get("/workspace/api/profil", (req, res) => { } /* Ein Scout bekommt genau die Creator, die er betreut -- und ohne Zuteilung eine leere Liste statt einer Fehlermeldung. */ + /* Zuteilung statt Rolle (03.09.2026): Eine Managerin sah hier die + Profile ALLER Creator. Jetzt bekommt sie genau ihre -- eigene und + die ihrer Scouts. */ let nur = ""; let werte = []; - if (req.person.rolle === "scout") { - const ids = betreuteIds(req.person); - if (!ids.length) return res.json({ creator: [], eigen: false, betreuend: true }); - nur = ` AND p.id IN (${ids.map(() => "?").join(",")})`; - werte = ids; - } else if (!istLeitung(req.person)) { + const erlaubt = sichtbareCreatorIds(req.person); + if (erlaubt !== null) { + if (!erlaubt.length) { + return res.json({ creator: [], eigen: false, betreuend: req.person.rolle !== "creator" }); + } + nur = ` AND p.id IN (${erlaubt.map(() => "?").join(",")})`; + werte = erlaubt; + } else if (false) { return res.status(404).json({ fehler: "nicht_gefunden" }); } res.json({ diff --git a/server/workspace-reports.js b/server/workspace-reports.js index 68d8f735..0ce44beb 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, + db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, sichtbareCreatorIds, } from "./workspace.js"; export const reportRouter = express.Router(); @@ -64,7 +64,11 @@ function zielCreator(req) { const gewuenscht = w && w !== "alle" && Number.isInteger(Number(w)) && Number(w) > 0 ? Number(w) : null; - if (istLeitung(req.person)) return gewuenscht; + /* Auch fuer einen Manager gilt jetzt die Zuteilung: "alle" bedeutet + 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; /* Scout: darf nur ueber die eigenen Creator einen Report ziehen. Ein fremder Wunsch wird nicht abgelehnt, sondern still auf den ersten @@ -211,18 +215,24 @@ reportRouter.get("/workspace/api/report/creator", (req, res) => { if (req.person.rolle === "creator") { return res.json({ creator: [{ id: req.person.id, name: req.person.name }] }); } - if (req.person.rolle === "scout") { - const ids = betreuteIds(req.person); - if (!ids.length) return res.json({ creator: [] }); + /* Nicht die ROLLE, sondern die ZUTEILUNG entscheidet (03.09.2026). + Hier stand eine Abfrage ueber ALLE -- eine Managerin bekam damit + auch Namen und Daten von Creators, die ihr nie zugeteilt waren. + sichtbarePersonenIds/sichtbareCreatorIds liefern null fuer + DogFather (= keine Einschraenkung) und sonst genau die erlaubten + Nummern. */ + const ids = sichtbareCreatorIds(req.person); + if (ids === null) { return res.json({ creator: db().prepare( - `SELECT id, name FROM personen WHERE id IN (${ids.map(() => "?").join(",")}) - AND aktiv = 1 ORDER BY name`).all(...ids), + "SELECT id, name FROM personen WHERE rolle = 'creator' AND aktiv = 1 ORDER BY name").all(), }); } + if (!ids.length) return res.json({ creator: [] }); res.json({ creator: db().prepare( - "SELECT id, name FROM personen WHERE rolle = 'creator' AND aktiv = 1 ORDER BY name").all(), + `SELECT id, name FROM personen WHERE id IN (${ids.map(() => "?").join(",")}) + AND aktiv = 1 ORDER BY name`).all(...ids), }); } catch { res.status(503).json({ fehler: "nicht_verfuegbar" }); diff --git a/server/workspace-schulung.js b/server/workspace-schulung.js index 72875f4d..ea8a6199 100644 --- a/server/workspace-schulung.js +++ b/server/workspace-schulung.js @@ -39,12 +39,14 @@ ===================================================================== */ import express from "express"; -import { createHash } from "node:crypto"; +import { + createHash } from "node:crypto"; import { join } from "node:path"; import { readFileSync, existsSync } from "node:fs"; import { db, sitzungLesen, istLeitung, istDogFather, darfCreator, betreuteIds, protokolliere, echteIp, DATEN_ORDNER, + sichtbareCreatorIds, } from "./workspace.js"; export const schulungRouter = express.Router(); @@ -190,6 +192,10 @@ schulungRouter.get("/workspace/api/schulung", (req, res) => { else if (istLeitung(req.person)) { creatorIds = db().prepare( "SELECT id FROM personen WHERE rolle = 'creator' AND aktiv = 1").all().map((z) => z.id); + /* Zuteilung statt Rolle (03.09.2026) -- eine Managerin sah sonst + den Schulungsstand aller Creator, auch fremder. */ + const erlaubtC = sichtbareCreatorIds(req.person); + if (erlaubtC !== null) creatorIds = creatorIds.filter((x) => erlaubtC.includes(x)); } else creatorIds = betreuteIds(req.person); const stand = creatorIds.length diff --git a/server/workspace-startcheck.js b/server/workspace-startcheck.js index 11b631ff..3d5ab8af 100644 --- a/server/workspace-startcheck.js +++ b/server/workspace-startcheck.js @@ -17,7 +17,8 @@ ===================================================================== */ import express from "express"; -import { db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds } from "./workspace.js"; +import { + sichtbareCreatorIds, db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds } from "./workspace.js"; export const startcheckRouter = express.Router(); @@ -118,13 +119,19 @@ startcheckRouter.get("/workspace/api/startcheck/creator", (req, res) => { if (person.rolle === "creator") { return res.json({ creator: [{ id: person.id, name: person.name }], eigen: true }); } + /* Nicht die ROLLE, sondern die ZUTEILUNG entscheidet (03.09.2026). + Hier stand eine Abfrage ueber ALLE -- eine Managerin bekam damit + auch Namen und Daten von Creators, die ihr nie zugeteilt waren. + sichtbarePersonenIds/sichtbareCreatorIds liefern null fuer + DogFather (= keine Einschraenkung) und sonst genau die erlaubten + Nummern. */ let nur = ""; let werte = []; - if (person.rolle === "scout") { - const ids = betreuteIds(person); - if (!ids.length) return res.json({ creator: [], eigen: false }); - nur = ` AND id IN (${ids.map(() => "?").join(",")})`; - werte = ids; + const erlaubt = sichtbareCreatorIds(person); + if (erlaubt !== null) { + if (!erlaubt.length) return res.json({ creator: [], eigen: false }); + nur = ` AND id IN (${erlaubt.map(() => "?").join(",")})`; + werte = erlaubt; } res.json({ creator: db().prepare( diff --git a/server/workspace-steckbrief.js b/server/workspace-steckbrief.js index 5c37c887..f19be566 100644 --- a/server/workspace-steckbrief.js +++ b/server/workspace-steckbrief.js @@ -43,12 +43,14 @@ ===================================================================== */ import express from "express"; -import { randomBytes } from "node:crypto"; +import { + randomBytes } from "node:crypto"; import { join } from "node:path"; import { mkdirSync, writeFileSync, unlinkSync, existsSync, createReadStream } from "node:fs"; import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, istLeitung, betreuteIds, ROLLEN_SORTIERUNG, ROLLEN_NAME, + sichtbarePersonenIds, } from "./workspace.js"; export const steckbriefRouter = express.Router(); @@ -100,12 +102,16 @@ steckbriefRouter.use("/workspace/api/steckbrief", angemeldet); Alles andere ist 404, nicht 403: Wer eine Person nicht sehen darf, soll auch nicht erfahren, dass es sie gibt. */ function sichtbareIds(person) { - const eigene = new Set([person.id]); - if (istLeitung(person)) return null; // null = alle - - if (person.rolle === "scout") { - for (const id of betreuteIds(person)) eigene.add(id); - } + /* NICHT mehr istLeitung (03.09.2026): Eine Managerin bekam damit die + Steckbriefe ALLER -- Bild, Kanaele, "ueber mich" -- auch von + Creators, die ihr nie zugeteilt waren. Jetzt gilt fuer sie dieselbe + Zuteilungsregel wie fuer einen Scout, nur mit der Kette ueber ihre + Scouts dazu (sichtbarePersonenIds). null bleibt "alle" und + bekommt allein DogFather. */ + const erlaubt = sichtbarePersonenIds(person); + if (erlaubt === null) return null; // null = alle + const eigene = new Set(erlaubt); + eigene.add(person.id); if (person.rolle === "creator") { /* Die eigenen Betreuer -- und die Leitung, die ohnehin für alle zuständig ist. */ diff --git a/server/workspace-suche.js b/server/workspace-suche.js index 28b7ad72..f42f2496 100644 --- a/server/workspace-suche.js +++ b/server/workspace-suche.js @@ -25,6 +25,7 @@ import express from "express"; import { db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds, + sichtbareCreatorIds, } from "./workspace.js"; import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js"; import { sichtbar as sichtbarTermine } from "./workspace-kalender.js"; @@ -219,14 +220,19 @@ sucheRouter.get("/workspace/api/suche", (req, res) => { Ansicht auf, die diese Felder gar nicht zeigen darf. Wer die Notiz lesen will, oeffnet das Profil. */ if (istLeitung(person) || person.rolle === "scout") { + /* Zuteilung statt Rolle (03.09.2026): Eine Managerin fand hier + die Profile ALLER Creator -- Nische, Kanäle, Sendezeiten, + Technik. Die Suche ist die unauffälligste Stelle für so ein + Leck: Man sucht nach etwas ganz anderem und bekommt fremde + Namen als Beifang, ohne dass jemand es je bemerkt. */ let nur = ""; let werte = []; - if (person.rolle === "scout") { - const ids = betreuteIds(person); - if (!ids.length) nur = null; + const erlaubt = sichtbareCreatorIds(person); + if (erlaubt !== null) { + if (!erlaubt.length) nur = null; else { - nur = ` AND p.id IN (${ids.map(() => "?").join(",")})`; - werte = ids; + nur = ` AND p.id IN (${erlaubt.map(() => "?").join(",")})`; + werte = erlaubt; } } if (nur !== null) { diff --git a/server/workspace.js b/server/workspace.js index 93041277..13c47377 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1178,6 +1178,64 @@ export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) { .run(scoutId, managerId, new Date().toISOString(), akteur); } +/* ===================================================================== + WEN DARF DIESE PERSON ÜBERHAUPT SEHEN? (03.09.2026) + + Wunsch: "jeder manager soll auch immer nur seine und die seiner + scouts zugeteilten creator und creator daten sehen. und nicht die der + anderen." + + Die Kette Manager -> Scout -> Creator gab es schon (betreuteIds). Was + fehlte, war ihre ANWENDUNG: Über zwanzig Stellen prüften die ROLLE + statt der ZUTEILUNG -- "ist Leitung? dann alles". Eine Lecksuche über + alle Leseschnittstellen (server/pruef-manager-sicht.mjs) fand am + 03.09.2026 dreizehn davon: der Kalender zeigte fremde Fristen, die + Personenlisten fremde Namen, Report, Steckbrief, Profil, Start-Check, + Schulung und die Suche jeweils alles. + + Deshalb stehen die beiden Antworten jetzt HIER, an einer Stelle, und + werden überall geholt statt jedes Mal neu formuliert. + + RÜCKGABE null HEISST "ALLE" -- und zwar nur für DogFather. Das ist + bewusst kein leeres Feld: Eine leere Liste bedeutet "niemand", und + die Verwechslung der beiden ist genau der Fehler, der aus einer + Sperre eine Freigabe macht. Wer null bekommt, lässt die Einschränkung + ganz weg; wer ein Feld bekommt, schränkt darauf ein -- auch wenn es + leer ist. + ===================================================================== */ + +/** Die Creator, deren Daten diese Person sehen darf. + * null = alle (nur DogFather). */ +export function sichtbareCreatorIds(person) { + if (!person) return []; + if (istDogFather(person)) return null; + if (person.rolle === "creator") return [person.id]; + return betreuteIds(person); // Manager: eigene + die seiner Scouts +} + +/** Die PERSONEN, die in Listen und Auswahlfeldern auftauchen dürfen -- + * Namen sind auch Daten. null = alle (nur DogFather). + * + * Enthält immer die Person selbst: Wer sich in einer Auswahl nicht + * findet, kann sich nichts selbst zuweisen. Bei einem Manager kommen + * 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; + const creator = sichtbareCreatorIds(person) || []; + const scouts = person.rolle === "manager" ? scoutsVon(person.id) : []; + return [...new Set([person.id, ...creator, ...scouts])]; +} + +/** SQL-Baustein daraus: "diese Spalte ist eine Person, die ich sehen + * darf". Gibt null zurück, wenn nicht eingeschränkt werden muss. */ +export function personenWo(person, spalte) { + const ids = sichtbarePersonenIds(person); + if (ids === null) return null; + if (!ids.length) return { wo: "0=1", werte: [] }; + return { wo: `${spalte} IN (${ids.map(() => "?").join(",")})`, werte: ids }; +} + /* SQL-Baustein "diese Spalte gehoert zu einem meiner Creator". Gibt null zurueck, wenn es nichts zu ergaenzen gibt -- eine leere IN-Liste waere ungueltiges SQL. */ @@ -1341,7 +1399,18 @@ export function sichtSetzen(req, res, next) { /* Darf diese Person den Bereich dieses Creators sehen und bearbeiten? */ export function darfCreator(person, creatorId) { if (!person || !creatorId) return false; - if (istLeitung(person)) return true; + /* NUR DogFather pauschal (03.09.2026). Hier stand istLeitung -- und + damit war jede Managerin fuer JEDEN Creator zustaendig, auch fuer + die einer fremden Managerin. + + Diese eine Zeile hing an mehreren Wegen gleichzeitig: Profil, + Start-Check und die Uebersicht je Creator liessen sich damit ueber + die blosse Kenntnis einer Nummer abfragen. Man musste nichts + umgehen, es reichte, eine Zahl in die Adresse zu schreiben. + + 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 (person.rolle === "creator") return person.id === Number(creatorId); return betreuteIds(person).includes(Number(creatorId)); } diff --git a/workspace/assets/js/uebersicht.js b/workspace/assets/js/uebersicht.js index 7a59df2b..cf38ca79 100644 --- a/workspace/assets/js/uebersicht.js +++ b/workspace/assets/js/uebersicht.js @@ -200,12 +200,26 @@ $('titel').textContent = 'Mein Stand'; $('unterzeile').textContent = 'Wo du gerade stehst – aus deinen Aufgaben, Terminen und dem Start-Check.'; } else if (!creator.length) { - $('unterzeile').textContent = ich.rolle === 'scout' + /* Ein Manager sah hier bis zum 03.09.2026 ALLE Creator und + landete deshalb nie in diesem Zweig. Seit die Zuteilung zählt, + steht er ohne zugeteilte Creator vor einer leeren Seite -- und + der Satz für den Scout ("DogFather legt fest, wen du betreust") + passte für ihn nur zur Hälfte: Er bekommt seine Creator auch + über seine Scouts. + + Ein leerer Bereich ohne Erklärung sieht aus wie ein Fehler. + Deshalb steht hier für jede Rolle der Satz, der wirklich + zutrifft. */ + const satz = ich.rolle === 'scout' ? 'Dir ist noch kein Creator zugeteilt.' - : 'Es gibt noch keinen Creator.'; - ziel.append(el('p', 'leer-hinweis', ich.rolle === 'scout' - ? 'DogFather legt unter „Personen & Zugänge“ fest, wen du betreust.' - : 'Lege unter „Personen & Zugänge“ den ersten Creator an.')); + : ich.rolle === 'manager' + ? 'Dir ist noch kein Creator zugeteilt – weder direkt noch über einen deiner Scouts.' + : 'Es gibt noch keinen Creator.'; + $('unterzeile').textContent = satz; + ziel.append(el('p', 'leer-hinweis', satz + ' ' + + (ich.rolle === 'admin' + ? 'Lege unter „Personen & Zugänge“ den ersten Creator an.' + : 'DogFather legt unter „Personen & Zugänge“ fest, wen du betreust.'))); return; } else { $('unterzeile').textContent = creator.length === 1 diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index f0d1e6fe..da2e0fe5 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + +
@@ -152,9 +152,9 @@ - - - - + + + +