Zustaendigkeit: DogFather und Manager zaehlen wie ein Scout

Filipe: "dogfather soll auch zaehlen wie bananastift und patrick."

Beim Nachsehen war das kein Wunsch, sondern ein Fehlerbericht. In der
Auswahl "Betreut von" stand "DogFather" -- aber als LEER-Wert, nicht als
Person. Es sah aus wie eine Zuordnung und war keine. Genau dieselben
Creator zaehlten deshalb gleichzeitig im Hinweis "Creator ohne
zustaendige Person". Zwei Aussagen ueber denselben Sachverhalt, beide auf
demselben Bildschirm, beide fuer sich stimmig.

Jetzt kann jede betreuende Rolle eingetragen werden -- DogFather, Manager
und Scouts, in der ueblichen Reihenfolge. Der Leer-Wert heisst, was er
ist: "— niemand —". Bei DogFather und Manager steht die Rolle in
Klammern dabei; bei aehnlichen Namen ist sonst nicht zu erkennen, wen man
eintraegt. Und "betreut N Creator" steht jetzt an jeder betreuenden
Person, nicht nur an Scouts.

DER WICHTIGE TEIL: An den RECHTEN aendert das nichts.

Die Zustaendigkeit steuert die Sichtbarkeit NUR beim Scout -- die Leitung
sieht ohnehin jeden Creator. Waere das anders, haette eine
Anzeigeeinstellung still Rechte vergeben. Der Test weist beide Richtungen
nach:

  * Tili auf DogFather eingetragen -> KEIN Scout sieht sie.
  * Tili auf Patrick eingetragen   -> nur Patrick sieht sie, BananaStift
                                      weiterhin nicht.
  * Zurueck auf DogFather          -> Patrick verliert die Sicht wieder.
  * DogFather sieht in allen drei Faellen unveraendert beide Creator.

Ein Creator kann nicht zustaendig sein -- das waere eine Rolle, die es
nicht gibt. Und ein Scout kann die Zustaendigkeit weiterhin nicht selbst
setzen (404), sonst haette er die Rechtevergabe in der Hand, die ihn
begrenzen soll.

20 Pruefungen, darunter die Gegenprobe zum Hinweis: Auf "niemand"
zurueckgesetzt MUSS er wiederkommen, sonst waere er wertlos.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-31 23:40:27 +02:00
co-authored by Claude Opus 5
parent 7673c12136
commit 24048ff0e0
20 changed files with 314 additions and 114 deletions
+169
View File
@@ -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);
+25 -6
View File
@@ -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) });