diff --git a/pruef-personen.png b/pruef-personen.png index 46ffde66..08cb4b2c 100644 Binary files a/pruef-personen.png and b/pruef-personen.png differ diff --git a/server/pruef-betreuung.mjs b/server/pruef-betreuung.mjs new file mode 100644 index 00000000..fd42a520 --- /dev/null +++ b/server/pruef-betreuung.mjs @@ -0,0 +1,169 @@ +/* =================================================================== + Pruefung: DogFather und Manager koennen als zustaendige Person + eingetragen werden -- genau wie ein Scout. + + Wunsch Filipe, 31.08.2026: "dogfather soll auch zaehlen wie + bananastift und patrick." + + Der springende Punkt ist der letzte Abschnitt: Ein Eintrag auf + DogFather darf an den RECHTEN nichts aendern. Nur beim Scout + entscheidet die Zustaendigkeit zusaetzlich ueber die Sichtbarkeit. + =================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-betreuung-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4189"; +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:4189"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); + +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(); +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001"); +const idManager = anlegen("Ein Manager", "manager", "CODE-MANA-0001"); +const idBanana = anlegen("BananaStift", "scout", "CODE-BANA-0001"); +const idPatrick = anlegen("Patrick", "scout", "CODE-PATR-0001"); +const idTili = anlegen("Tili", "creator", "CODE-TILI-0001"); +const idSpong = anlegen("SpongBob", "creator", "CODE-SPON-0001"); +d.close(); + +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 }), redirect: "manual" }); + return (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; "); +}; +const ruf = (pfad, keks, opt = {}) => fetch(BASIS + pfad, { + ...opt, headers: { cookie: keks, "Content-Type": "application/json", ...(opt.headers || {}) } }); +const jsn = async (a) => { try { return await a.json(); } catch { return {}; } }; +const setzen = (creator, betreuer, keks) => ruf(`/workspace/api/verwaltung/betreuung/${creator}`, keks, { + method: "PUT", body: JSON.stringify({ betreuer_id: betreuer }) }); + +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +const kBanana = await anmelden("scout", "CODE-BANA-0001"); +const kPatrick = await anmelden("scout", "CODE-PATR-0001"); + +console.log("\n1) Die Auswahl enthält jetzt alle betreuenden Rollen"); +{ + const { betreuer } = await jsn(await ruf("/workspace/api/verwaltung/personen", kDogi)); + console.log(" ", betreuer.map((b) => `${b.name} (${b.rolle})`).join(" · ")); + ok(betreuer.length === 4, `vier Auswahlmöglichkeiten (${betreuer.length})`); + ok(betreuer.map((b) => b.rolle).join(",") === "admin,manager,scout,scout", + `in der üblichen Reihenfolge (${betreuer.map((b) => b.rolle).join(", ")})`); + ok(betreuer[0].name === "Dogfather", `DogFather steht zuerst (${betreuer[0].name})`); + ok(!betreuer.some((b) => b.rolle === "creator"), "kein Creator in der Auswahl"); +} + +console.log("\n2) DogFather lässt sich eintragen wie ein Scout"); +{ + const a = await setzen(idTili, idDogi, kDogi); + const j = await jsn(a); + ok(a.ok, `DogFather als zuständig eingetragen (${a.status})`); + ok(j.betreuer_name === "Dogfather", `mit Namen zurückgemeldet (${j.betreuer_name})`); + + const m = await setzen(idSpong, idManager, kDogi); + ok(m.ok, `und ein Manager genauso (${m.status})`); + + const { personen } = await jsn(await ruf("/workspace/api/verwaltung/personen", kDogi)); + const tili = personen.find((p) => p.name === "Tili"); + ok(tili.betreuer_name === "Dogfather", `steht an der Person (${tili.betreuer_name})`); + + /* Und DogFather zaehlt jetzt als Betreuer -- genau wie ein Scout. */ + const dogi = personen.find((p) => p.name === "Dogfather"); + ok(dogi.betreut_anzahl === 1, `DogFather betreut 1 Creator (${dogi.betreut_anzahl})`); +} + +console.log("\n3) Der Hinweis über fehlende Zuständigkeit stimmt jetzt mit der Anzeige überein"); +{ + /* Das war der eigentliche Fehler: In der Auswahl stand "DogFather", + der Hinweis zaehlte dieselben Creator trotzdem als unbetreut. Zwei + Aussagen, ein Bildschirm. */ + const { hinweise } = await jsn(await ruf("/workspace/api/hinweise", kDogi)); + const ohne = hinweise.find((h) => h.art === "ohne_betreuung"); + ok(!ohne, `kein Creator mehr ohne zuständige Person (${ohne ? ohne.anzahl : 0})`); + + /* Gegenprobe: Zuruecksetzen auf "niemand" -- dann MUSS der Hinweis + wiederkommen, sonst waere er wertlos. */ + await setzen(idTili, null, kDogi); + const zweite = await jsn(await ruf("/workspace/api/hinweise", kDogi)); + const wieder = zweite.hinweise.find((h) => h.art === "ohne_betreuung"); + ok(wieder?.anzahl === 1, `nach dem Zurücksetzen wieder gemeldet (${wieder?.anzahl})`); + await setzen(idTili, idDogi, kDogi); +} + +console.log("\n4) Ein Creator kann nicht zuständig sein"); +{ + const a = await setzen(idTili, idSpong, kDogi); + ok(a.status === 400, `abgelehnt (${a.status})`); + console.log(" ->", (await jsn(a)).fehler); +} + +console.log("\n5) DAS WICHTIGSTE: an den Rechten ändert das nichts"); +{ + /* Tili ist auf DogFather eingetragen. Kein Scout darf sie dadurch + sehen -- die Zustaendigkeit steuert die Sichtbarkeit NUR beim + Scout. Waere das anders, haette eine Anzeigeeinstellung still + Rechte vergeben. */ + const b = await jsn(await ruf("/workspace/api/uebersicht/creator", kBanana)); + ok((b.creator || []).length === 0, + `BananaStift sieht weiterhin keinen Creator (${(b.creator || []).length})`); + + /* Und der Gegenbeweis: Auf einen Scout eingetragen, sieht genau DER + sie -- und nur er. */ + await setzen(idTili, idPatrick, kDogi); + const p = await jsn(await ruf("/workspace/api/uebersicht/creator", kPatrick)); + ok((p.creator || []).length === 1 && p.creator[0].name === "Tili", + `Patrick sieht Tili, sobald sie ihm zugeteilt ist (${(p.creator || []).map((c) => c.name).join(", ")})`); + const b2 = await jsn(await ruf("/workspace/api/uebersicht/creator", kBanana)); + ok((b2.creator || []).length === 0, + `BananaStift immer noch nicht (${(b2.creator || []).length})`); + + /* Zurueck auf DogFather -- Patrick verliert die Sicht wieder. */ + await setzen(idTili, idDogi, kDogi); + const p2 = await jsn(await ruf("/workspace/api/uebersicht/creator", kPatrick)); + ok((p2.creator || []).length === 0, + `und verliert sie wieder, sobald DogFather übernimmt (${(p2.creator || []).length})`); + + /* Die Leitung sah ohnehin immer alle -- das darf sich nicht geaendert + haben, in keine Richtung. */ + const dg = await jsn(await ruf("/workspace/api/uebersicht/creator", kDogi)); + ok((dg.creator || []).length === 2, `DogFather sieht unverändert beide (${(dg.creator || []).length})`); +} + +console.log("\n6) Ein Scout kann die Zuständigkeit nicht selbst setzen"); +{ + const a = await setzen(idSpong, idBanana, kBanana); + ok(a.status === 404, `Scout bekommt 404 (${a.status})`); +} + +console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler === 0 ? 0 : 1); diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 6ea1d056..bccf9b2c 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -102,9 +102,20 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => { FROM personen p LEFT JOIN betreuung b ON b.creator_id = p.id ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.aktiv DESC, p.name`).all(), - /* Wer ueberhaupt als zustaendig eingetragen werden kann. */ + /* Wer ueberhaupt als zustaendig eingetragen werden kann. + + Bis zum 31.08.2026 waren das nur Scouts. In der Auswahl stand + "DogFather" -- aber als LEER-Wert, nicht als Person. Das sah aus + wie eine Zuordnung und war keine: Dieselben Creator zaehlten + gleichzeitig im Hinweis "Creator ohne zustaendige Person". Zwei + Stellen, zwei Aussagen, beide auf demselben Bildschirm. + + Jetzt kann jede betreuende Rolle eingetragen werden -- DogFather + und Manager genauso wie Scouts. In der ueblichen Reihenfolge. */ betreuer: db().prepare( - "SELECT id, name FROM personen WHERE rolle = 'scout' AND aktiv = 1 ORDER BY name").all(), + `SELECT id, name, rolle FROM personen + WHERE rolle IN ('admin', 'manager', 'scout') AND aktiv = 1 + ORDER BY ${ROLLEN_SORTIERUNG}, name`).all(), }); } catch (fehler) { console.error("[workspace] Personen lesen:", fehler?.message); @@ -137,10 +148,18 @@ personenRouter.put("/workspace/api/verwaltung/betreuung/:id", gleicheHerkunft, ( if (!Number.isInteger(z) || z < 1) return res.status(400).json({ fehler: "Ungültige Auswahl." }); const betreuer = db().prepare( "SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(z); - /* Nur Scouts. Das Management sieht ohnehin alle Creator und braucht - keinen Eintrag -- einer waere irrefuehrend. */ - if (!betreuer || betreuer.rolle !== "scout") { - return res.status(400).json({ fehler: "Zuständig können nur aktive Scouts sein." }); + /* DogFather, Manager und Scouts. Ein Creator kann nicht fuer einen + anderen zustaendig sein -- das waere eine Rolle, die es nicht gibt. + + WICHTIG: Ein Eintrag auf DogFather oder Manager aendert an den + RECHTEN nichts. Die Leitung sieht ohnehin jeden Creator; der + Eintrag sagt nur, WER sich kuemmert. Nur beim Scout entscheidet er + zusaetzlich ueber die Sichtbarkeit -- deshalb steht die Regel + dafuer unveraendert in betreuteIds(). */ + if (!betreuer || !["admin", "manager", "scout"].includes(betreuer.rolle)) { + return res.status(400).json({ + fehler: "Zuständig können nur aktive DogFather, Manager oder Scouts sein.", + }); } betreuungSetzen(id, z, { ...req.person, ip: echteIp(req) }); diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 318f93cf..f3cc92ed 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -134,7 +134,7 @@ z.append(el('span', null, `letzte Anmeldung: ${datum(p.letzter_login)}`)); z.append(el('span', null, `offene Aufgaben: ${p.offene_aufgaben}`)); if (p.sitzungen) z.append(el('span', null, `${p.sitzungen} offene Sitzung(en)`)); - if (p.rolle === 'scout' && p.betreut_anzahl) { + if (p.betreut_anzahl) { z.append(el('span', null, `betreut ${p.betreut_anzahl} Creator`)); } links.append(z); @@ -147,12 +147,24 @@ s.htmlFor = 'betreuer-' + p.id; const w = el('select', 'betreuung__wahl'); w.id = 'betreuer-' + p.id; + /* Der Leer-Wert heisst jetzt, was er ist. Vorher stand dort + "DogFather" -- das sah aus wie eine Zuordnung, war aber keine, + und dieselben Creator tauchten gleichzeitig im Hinweis "Creator + ohne zustaendige Person" auf. Zwei Aussagen, ein Bildschirm. + + DogFather steht jetzt als echte Person in der Liste, gleichrangig + neben den Scouts. */ const leer = document.createElement('option'); - leer.value = ''; leer.textContent = 'DogFather'; + leer.value = ''; leer.textContent = '— niemand —'; w.append(leer); for (const s2 of betreuerListe) { const o = document.createElement('option'); - o.value = String(s2.id); o.textContent = s2.name; + o.value = String(s2.id); + /* Die Rolle mit dabei: Bei gleichen oder aehnlichen Namen ist + sonst nicht zu erkennen, wen man gerade einträgt. */ + o.textContent = s2.rolle && s2.rolle !== 'scout' + ? `${s2.name} (${ROLLENNAME[s2.rolle] || s2.rolle})` + : s2.name; w.append(o); } w.value = p.betreuer_id ? String(p.betreuer_id) : ''; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 0160ed90..46e59d7c 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -134,8 +134,8 @@ - - - + + + diff --git a/workspace/automation.html b/workspace/automation.html index fbbeee41..05dbde53 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -127,8 +127,8 @@ - - - + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index bad86275..e2b12f05 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -98,8 +98,8 @@ - - - + + + diff --git a/workspace/calls.html b/workspace/calls.html index a86ff9e1..5d212eac 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -49,9 +49,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index 290570a0..fa729dcf 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -153,8 +153,8 @@ - - - + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 00a87d19..9d7afce7 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -88,8 +88,8 @@ - - - + + + diff --git a/workspace/index.html b/workspace/index.html index 34b56d57..359715bd 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 602d4b14..c703bd32 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -94,8 +94,8 @@ - - - + + + diff --git a/workspace/personen.html b/workspace/personen.html index 2a2e6396..c8b3f286 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -116,8 +116,8 @@ - - - + + + diff --git a/workspace/profil.html b/workspace/profil.html index 982fd953..3d3bc523 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -127,8 +127,8 @@ - - - + + + diff --git a/workspace/report.html b/workspace/report.html index 474f89b5..c6895427 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -86,9 +86,9 @@ - - - - + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 6eeac639..d2cfaed1 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -124,8 +124,8 @@ - - - + + + diff --git a/workspace/start.html b/workspace/start.html index 06970507..6a2ea6d2 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -68,8 +68,8 @@

- - - + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 84db85f4..3ef4372d 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -65,9 +65,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index f1b3c963..5654e3d5 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -7,9 +7,9 @@ - - - + + + @@ -52,7 +52,7 @@ - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 419398d9..a9b52831 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -170,8 +170,8 @@ - - - + + +