Filipe: "jeder soll sich selber aufgabe vergeben koennen aber nur die
rechte hand und dogfather aufgaben an andere verteilen." Und: "ich will
dass ich da auch die rollen der leute wechseln kann ohne dass ich denen
einen neuen account machen muss ... perfektionier das fuer die rolle
dogfather und rechte hand."
---- AUFGABEN VERTEILEN ---------------------------------------------
Die Regel gab es schon -- sie fragte `istLeitung`, und darin steht die
rechte Hand NICHT. Sie konnte also keine Aufgabe weitergeben.
`hand` DORT EINZUTRAGEN WAERE FAHRLAESSIG GEWESEN: `istLeitung` wird an
57 Stellen in 19 Dateien gefragt -- unter anderem im vertraulichen
Meldeweg, in der Personenverwaltung und in den Auswertungen ueber
Menschen. Das haette in einem Zug ueber Einsicht in fremde Meldungen
entschieden, und das hat niemand gewollt.
Stattdessen `darfAufgabenVerteilen` -- eine eigene Regel mit eigenem
Namen. Man sieht an der Aufrufstelle, worum es geht, und wer sie
spaeter aendert, aendert genau diese eine Sache.
Wer nicht verteilen darf, bekommt KEINE Absage: Die Aufgabe landet bei
ihm selbst. Das ist genau, was Filipe wollte ("jeder soll sich selber
aufgabe vergeben koennen") und freundlicher als ein Fehler.
---- ROLLEN WECHSELN ------------------------------------------------
Auch das gab es schon, samt dem wichtigen Satz in der Rueckfrage: "Der
Zugangscode bleibt derselbe." Es konnte nur DogFather (und Spicy
Media).
DREI GRENZEN, JEDE MIT GRUND:
* Niemand wird zu "admin" -- unveraendert seit dem 11.09.2026.
* Die rechte Hand ernennt keine zweite rechte Hand. Wer jemanden auf
die eigene Ebene hebt, vergibt Vertrauen, das ihm nicht gehoert.
Und wer eine Rolle UEBER sich aendern koennte, koennte sich selbst
befoerdern, indem er zuerst den anderen herabstuft.
* Niemand aendert die eigene Rolle -- sonst waere jede Grenze nur ein
Umweg.
SPICY MEDIA BEHAELT, WAS SIE HATTE. Beim ersten Anlauf waere sie durch
die neue Regel ausgesperrt gewesen -- ein Rueckschritt, den ich selbst
verursacht haette. Sie steht jetzt ausdruecklich in der Tabelle.
---- UND DIE OBERFLAECHE RECHNET NICHT MEHR SELBST ------------------
Die Rollenwahl nahm die Knoepfe des ANLEGE-Formulars. Fuer die rechte
Hand ist das leer -- sie legt niemanden an. Die Wahl waere leer
geblieben, das Recht unbenutzbar.
Der Server schickt jetzt zwei Listen mit: wozu ich machen darf
(`rollen_zum_aendern`) und wessen Rolle ich anfassen darf
(`rollen_anfassbar`). Beide aus denselben Funktionen wie die Schranke.
Damit kann die Seite weder zu streng sein (ein Recht, das niemand
findet) noch zu grosszuegig (ein Knopf, der eine Absage bringt) -- der
Fehler vom 10.09.2026, als zwei Listen drei Zeilen auseinander
einander widersprachen.
Dabei fast hineingelaufen: `ROLLEN_REIHE` ist die SORTIERUNG, und
darin fehlt "gast" mit Absicht. Wer sie fuer eine Aufzaehlung nimmt,
verliert die Community still -- genau diese Verwechslung hat am
17.09.2026 schon einmal verhindert, dass sich jemand zu "Community"
machen liess.
---- AUSSERDEM ------------------------------------------------------
Screen 2: "Unsere Seiten" und "Regeln & Hilfe" haben die Plaetze
getauscht -- nur diese zwei.
GEPRUEFT: pruef-verteilen 11/0 (neu, inkl. der Abgrenzung "darf
verteilen, ist aber NICHT Leitung"), pruef-rollen-anlegen 11/0,
pruef-personen-formular 36/0, pruef-personen-liste 33/0,
pruef-personen-loeschen 40/0, pruef-rechtetafel 19/0, pruef-treff 72/0.
Co-Authored-By: Claude Opus 5 <[email protected]>
175 lines
8.0 KiB
JavaScript
175 lines
8.0 KiB
JavaScript
/* =====================================================================
|
|
pruef-verteilen.mjs — Wer darf Aufgaben an andere geben?
|
|
|
|
Filipe, 20.09.2026: "jeder soll sich selber aufgabe vergeben koennen
|
|
aber nur die rechte hand und dogfather aufgaben an andere verteilen."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT
|
|
|
|
Die Regel hat ZWEI Seiten, und beide muessen dasselbe sagen:
|
|
|
|
* Der Server lehnt ab, wer nicht darf.
|
|
* Die Seite zeigt die Felder nur dem, der darf.
|
|
|
|
Stimmt nur die erste, sieht die rechte Hand Felder, die nichts
|
|
bewirken. Stimmt nur die zweite, kommt jeder durch, der die
|
|
Schnittstelle direkt anspricht. Beides ist schon vorgekommen.
|
|
|
|
UND DIE GEFAEHRLICHE STELLE: `hand` steht bewusst NICHT in
|
|
`istLeitung` -- diese Menge wird an 57 Stellen gefragt, unter
|
|
anderem im vertraulichen Meldeweg. Die Pruefung haelt fest, dass
|
|
das Verteilen eine EIGENE Regel ist und nicht versehentlich zur
|
|
Leitung wird.
|
|
|
|
AUFRUF: node server/pruef-verteilen.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { portMussFreiSein } from "./helfer-port.mjs";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4433, "pruef-verteilen");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-verteilen-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-verteilen");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const melde = (t) => console.log(t);
|
|
|
|
console.log("\n=== Wer darf Aufgaben an andere geben? ===");
|
|
|
|
/* ---- 1. Die Regel selbst, ohne Server ----------------------------- */
|
|
melde("\n=== 1. Die Regel ===");
|
|
{
|
|
const { darfAufgabenVerteilen, istLeitung } = await import("./workspace.js");
|
|
const darf = ["admin", "hand", "spicy", "manager"];
|
|
const nicht = ["modi", "scout", "creator", "gast"];
|
|
ok(darf.every((r) => darfAufgabenVerteilen({ rolle: r })),
|
|
`verteilen duerfen: ${darf.join(", ")}`);
|
|
ok(nicht.every((r) => !darfAufgabenVerteilen({ rolle: r })),
|
|
`und nicht: ${nicht.join(", ")}`);
|
|
/* DIE WICHTIGE ABGRENZUNG: Verteilen ist NICHT Leitung. Waere die
|
|
rechte Hand in `istLeitung` gerutscht, saehe sie damit auch
|
|
vertrauliche Meldungen ueber Menschen -- eine Entscheidung, die
|
|
niemand getroffen hat. */
|
|
ok(!istLeitung({ rolle: "hand" }),
|
|
"die rechte Hand darf verteilen, ist aber NICHT „Leitung“ — das bleibt getrennt");
|
|
ok(darfAufgabenVerteilen({ rolle: undefined }) === false
|
|
&& darfAufgabenVerteilen(null) === false,
|
|
"Gegenprobe: ohne Rolle darf niemand");
|
|
}
|
|
|
|
/* ---- 2. Durch die Schnittstelle ----------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const jetzt = new Date().toISOString();
|
|
let idDogi, idHand, idModi, idModi2;
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const k = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,NULL)"
|
|
+ " ON CONFLICT(schluessel) DO NOTHING").run("code_kennung_schluessel", k, jetzt);
|
|
const anlegen = (name, rolle, code) => {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768, createHmac("sha256", k).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
idDogi = anlegen("Filipe", "admin", "CODE-A");
|
|
idHand = anlegen("VanVan", "hand", "CODE-H");
|
|
idModi = anlegen("Diene", "modi", "CODE-M");
|
|
idModi2 = anlegen("Ghost", "modi", "CODE-M2");
|
|
d.close();
|
|
}
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
function ruf(pfad, keks, methode = "GET", koerper = null) {
|
|
return new Promise((fertig, schief) => {
|
|
const rumpf = koerper ? JSON.stringify(koerper) : null;
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
|
|
headers: {
|
|
Host: CREW, ...(keks ? { Cookie: keks } : {}),
|
|
...(rumpf ? { "Content-Type": "application/json", Origin: `https://${CREW}`,
|
|
"Content-Length": Buffer.byteLength(rumpf) } : {}),
|
|
},
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (x) => { text += x; });
|
|
antwort.on("end", () => fertig({
|
|
code: antwort.statusCode, text,
|
|
keks: [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "),
|
|
}));
|
|
});
|
|
a.on("error", schief);
|
|
if (rumpf) a.write(rumpf);
|
|
a.end();
|
|
});
|
|
}
|
|
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
|
|
const anmelden = async (rolle, code) =>
|
|
(await ruf("/workspace/api/anmelden", null, "POST", { rolle, code })).keks;
|
|
|
|
melde("\n=== 2. Durch die Schnittstelle ===");
|
|
const kDogi = await anmelden("admin", "CODE-A");
|
|
const kHand = await anmelden("hand", "CODE-H");
|
|
const kModi = await anmelden("modi", "CODE-M");
|
|
ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet");
|
|
|
|
/* Sagt der Server es auch der Seite? */
|
|
for (const [name, keks, soll] of [["DogFather", kDogi, true], ["rechte Hand", kHand, true], ["Modi", kModi, false]]) {
|
|
const ich = json(await ruf("/workspace/api/ich", keks));
|
|
ok(ich.darf_verteilen === soll,
|
|
`„${name}“ bekommt darf_verteilen = ${ich.darf_verteilen} (erwartet ${soll})`);
|
|
}
|
|
|
|
/* Und die Tat: eine Aufgabe FUER JEMAND ANDEREN. */
|
|
async function anlegenFuer(keks, zielId, titel) {
|
|
const a = await ruf("/workspace/api/aufgaben", keks, "POST",
|
|
{ titel, verantwortlich_id: zielId, prioritaet: "mittel" });
|
|
if (a.code !== 201) return { code: a.code, verant: null };
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const z = d.prepare("SELECT verantwortlich_id FROM aufgaben WHERE id = ?").get(json(a).id);
|
|
d.close();
|
|
return { code: a.code, verant: z?.verantwortlich_id ?? null };
|
|
}
|
|
|
|
const vonHand = await anlegenFuer(kHand, idModi, "Von der rechten Hand an Diene");
|
|
ok(vonHand.code === 201 && vonHand.verant === idModi,
|
|
`die rechte Hand gibt eine Aufgabe an Diene (${vonHand.code}, liegt bei ${vonHand.verant})`);
|
|
|
|
const vonDogi = await anlegenFuer(kDogi, idModi2, "Von DogFather an Ghost");
|
|
ok(vonDogi.code === 201 && vonDogi.verant === idModi2,
|
|
`DogFather ebenso (${vonDogi.code}, liegt bei ${vonDogi.verant})`);
|
|
|
|
/* DIE ENTSCHEIDENDE GEGENPROBE: Ein Modi versucht dasselbe. Die
|
|
Aufgabe wird NICHT abgelehnt -- sie landet bei ihm selbst. Das ist
|
|
freundlicher als ein Fehler und genau das, was Filipe wollte:
|
|
"jeder soll sich selber aufgabe vergeben koennen". */
|
|
const vonModi = await anlegenFuer(kModi, idModi2, "Versuch eines Modi");
|
|
ok(vonModi.code === 201 && vonModi.verant === idModi,
|
|
`Gegenprobe: ein Modi kann es versuchen — die Aufgabe landet bei ihm selbst `
|
|
+ `(${vonModi.verant}, nicht ${idModi2})`);
|
|
|
|
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|