Filipe: "ich sehe noch immer nicht dass dogfather oder die rechte hand wenn sie aufgaben verteilen die spezifisch an leute verteilen können. mach das endlich auch weil das ist seeehr wichtig damit wir arbeiten können mit den leuten." GEMESSEN, BEVOR GEBAUT WURDE -- und die Messung hat zwei verschiedene Antworten gegeben: DogFather Feld sichtbar, vier Menschen in der Auswahl Rechte Hand Feld VERSTECKT, Auswahl leer Fuer ihn gab es die Zuteilung also, fuer sie nicht. Beides war falsch, jedes auf seine Art. 1. DIE RECHTE HAND KAM NIE AN DIE FELDER Das Absenden fragte `ich.darf_verteilen` -- das sagt der Server: Leitung ODER rechte Hand (seit 20.09.). Das ANZEIGEN fragte eine andere Menge aus bereiche.js: spicy, admin, manager. Zwei Bedingungen fuer dieselbe Sache, und eine davon war nie nachgezogen worden. Der Server haette ihre Zuteilung angenommen; sie konnte sie nur nirgends eintragen. Vier Stellen betroffen, und eine davon war gefaehrlich: Beim BEARBEITEN wurde `verantwortlich_id` nicht mitgeschickt -- die rechte Hand haette durch blosses Speichern die Zuteilung einer Aufgabe stillschweigend geloescht. Kein Fehler, keine Meldung, die Aufgabe gehoert danach niemandem. Alle vier fragen jetzt den Server. `LEITUNG.has(ich.rolle)` bleibt nur noch als Rueckfall hinter `??`, fuer den Fall einer aelteren Serverfassung. 2. UND FUER DOGFATHER LAG ES AM WORT Das Feld hiess "Verantwortlich" und stand auf "—". Das liest sich wie eine Eigenschaft der Aufgabe, nicht wie eine Frage an einen selbst. Es heisst jetzt "Wer macht es?", der leere Wert "— noch niemand —", und darunter steht, was die Auswahl bewirkt: Die Aufgabe steht bei ihm unter "Nur meine", und er bekommt eine Meldung. Eine Frage beantwortet man. Ein Strich uebersieht man. WARUM KEINE PRUEFUNG DAS GEFUNDEN HAT: Alle fragten den Server, und der war in Ordnung -- Rechte da, Leute da, Zuteilung wird angenommen. Eine Erlaubnis, an die niemand herankommt, sieht von dort aus wie eine Erlaubnis. pruef-verteilen liest deshalb jetzt auch den Quelltext der Oberflaeche: dieselbe Frage an beiden Stellen, und die Beschriftung muss eine Frage sein. Kommentare zaehlen dabei nicht mit -- beim ersten Lauf hat die Pruefung meine eigene Begruendung als Befund gezaehlt. pruef-verteilen 16/0 · pruef-aufgabenbrett in Ordnung Co-Authored-By: Claude Opus 5 <[email protected]>
247 lines
12 KiB
JavaScript
247 lines
12 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 { eigenerPort } from "./helfer-port.mjs";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "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})`);
|
|
|
|
/* =======================================================================
|
|
DIE FELDER MUESSEN AUCH ZU SEHEN SEIN
|
|
=======================================================================
|
|
|
|
Filipe am 21.09.2026: "ich sehe noch immer nicht dass dogfather oder
|
|
die rechte hand wenn sie aufgaben verteilen die spezifisch an leute
|
|
verteilen können. mach das endlich auch weil das ist seeehr wichtig
|
|
damit wir arbeiten können mit den leuten."
|
|
|
|
ALLES DARUEBER WAR GRUEN -- und er hatte trotzdem recht. Der Server
|
|
nahm die Zuteilung an, die Rechte stimmten, die Leute standen in der
|
|
Auskunft. Nur die rechte Hand BEKAM DAS FELD NIE ZU SEHEN: Das
|
|
Absenden fragte `ich.darf_verteilen` (Server: Leitung ODER rechte
|
|
Hand), das Anzeigen eine andere Menge aus bereiche.js (spicy, admin,
|
|
manager). Zwei Bedingungen fuer dieselbe Sache, und eine davon
|
|
veraltet.
|
|
|
|
EINE PRUEFUNG, DIE NUR DEN SERVER FRAGT, FINDET SO ETWAS NIE. Sie
|
|
sieht eine Erlaubnis, die es gibt -- und uebersieht, dass niemand
|
|
an sie herankommt. Deshalb wird hier der Quelltext der Oberflaeche
|
|
mitgelesen: Es muss an BEIDEN Stellen dieselbe Frage stehen.
|
|
======================================================================= */
|
|
{
|
|
const { readFileSync } = await import("node:fs");
|
|
/* KOMMENTARE ZAEHLEN NICHT MIT.
|
|
|
|
Beim ersten Lauf meldete diese Pruefung fuenf Stellen -- zwei
|
|
davon waren mein eigener ERKLAERTEXT, in dem die alte Bedingung
|
|
woertlich zitiert steht. Eine Pruefung, die ihre eigene
|
|
Begruendung als Befund zaehlt, treibt einen dazu, die Begruendung
|
|
zu loeschen. Der Zerleger aus helfer-ohne-kommentar.mjs trennt
|
|
beides sauber. */
|
|
const { ohneKommentar } = await import("./helfer-ohne-kommentar.mjs");
|
|
const js = ohneKommentar(readFileSync(
|
|
new URL("../workspace/assets/js/aufgaben.js", import.meta.url), "utf8"), ".js");
|
|
|
|
/* Wie oft wird ueberhaupt entschieden, ob jemand verteilen darf? */
|
|
const mitServer = (js.match(/ich\.darf_verteilen/g) || []).length;
|
|
ok(mitServer >= 2,
|
|
`die Oberflaeche fragt ${mitServer}-mal den Server (darf_verteilen)`);
|
|
|
|
/* UND NIRGENDS MEHR ALLEIN DIE ROLLENMENGE. `LEITUNG.has(ich.rolle)`
|
|
darf nur noch als Rueckfall hinter `??` stehen -- also fuer den
|
|
Fall, dass eine aeltere Serverfassung das Feld gar nicht schickt.
|
|
Steht es irgendwo als eigene Bedingung, ist die zweite Wahrheit
|
|
zurueck. */
|
|
const ohneRueckfall = [...js.matchAll(/LEITUNG\.has\(ich\.rolle\)/g)]
|
|
.filter((m) => !/\?\?\s*$/.test(js.slice(Math.max(0, m.index - 24), m.index)));
|
|
ok(ohneRueckfall.length === 0,
|
|
ohneRueckfall.length
|
|
? `${ohneRueckfall.length}-mal entscheidet die Rollenmenge allein -- `
|
|
+ "der Server sagt es besser"
|
|
: "und nirgends entscheidet die Rollenmenge allein");
|
|
|
|
/* DIE BESCHRIFTUNG IST TEIL DER FUNKTION. "Verantwortlich" mit einem
|
|
Strich daneben liest sich wie eine Eigenschaft; gefunden hat
|
|
Filipe das Feld damit nicht. "Wer macht es?" ist eine Frage. */
|
|
const html = readFileSync(
|
|
new URL("../workspace/aufgaben.html", import.meta.url), "utf8");
|
|
const schilder = [...html.matchAll(/for="(?:f|b)-verant">([^<]+)</g)].map((m) => m[1].trim());
|
|
ok(schilder.length === 2,
|
|
`beide Formulare haben das Feld (${schilder.length})`);
|
|
ok(schilder.every((t) => /wer\s+macht/i.test(t)),
|
|
`und beide fragen dasselbe ("${schilder.join('" / "')}")`);
|
|
|
|
/* Und der leere Wert sagt, was er bedeutet. Ein Strich ist kein
|
|
Zustand, sondern ein fehlendes Wort -- wer ihn sieht, weiss nicht,
|
|
ob er etwas versaeumt hat. */
|
|
ok(/— noch niemand —/.test(html),
|
|
"der leere Wert heisst „noch niemand“ und nicht nur „—“");
|
|
}
|
|
|
|
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);
|