|
|
|
@@ -0,0 +1,202 @@
|
|
|
|
|
/* =====================================================================
|
|
|
|
|
DIE FREMDE SICHT — und die Regel, die sie zusammenhält.
|
|
|
|
|
|
|
|
|
|
DogFather kann die Sicht einer anderen Person übernehmen. Dabei gilt
|
|
|
|
|
eine Regel, die in workspace.js ausdrücklich aufgeschrieben ist:
|
|
|
|
|
|
|
|
|
|
"Die Oberflaeche braucht beides: WER BIN ICH (fuer alles, was
|
|
|
|
|
SCHREIBT) und WESSEN ARBEITSPLATZ SEHE ICH (fuer das, was gezeigt
|
|
|
|
|
wird). Die beiden zu vermischen waere der sichere Weg dazu, dass
|
|
|
|
|
irgendwann etwas unter fremdem Namen gespeichert wird."
|
|
|
|
|
|
|
|
|
|
ANZEIGEN folgt also der Sicht, SCHREIBEN dem Angemeldeten.
|
|
|
|
|
|
|
|
|
|
---------------------------------------------------------------------
|
|
|
|
|
WARUM ES DIESE DATEI GIBT
|
|
|
|
|
|
|
|
|
|
Am 11.09.2026 meldete Filipe, DogFather sehe "die Creator nicht
|
|
|
|
|
mehr". Nichts war kaputt -- er war seit dem Vortag in einer fremden
|
|
|
|
|
Sicht. Beim Nachmessen fiel aber etwas anderes auf: Die Sicht kam nur
|
|
|
|
|
auf der HALBEN Seite an. Die Zahlen einer Seite folgten ihr, die
|
|
|
|
|
Creator-Auswahl daneben nicht -- drei Endpunkte lasen `req.person`,
|
|
|
|
|
wo ihre eigenen Datenwege zwanzig Zeilen weiter `req.sicht ||
|
|
|
|
|
req.person` lesen.
|
|
|
|
|
|
|
|
|
|
Zwei Antworten auf eine Frage, und beide sahen für sich richtig aus.
|
|
|
|
|
Genau deshalb steht die Regel jetzt hier als Messung und nicht nur
|
|
|
|
|
als Absatz.
|
|
|
|
|
|
|
|
|
|
Aufruf: node server/pruef-fremde-sicht.mjs
|
|
|
|
|
===================================================================== */
|
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
|
|
|
import { tmpdir } from "node:os";
|
|
|
|
|
import { join, dirname } from "node:path";
|
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
|
import { spawn } from "node:child_process";
|
|
|
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
|
|
|
|
|
|
notbremse(240_000, "pruef-fremde-sicht");
|
|
|
|
|
|
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
const PORT = 4393;
|
|
|
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-sicht-"));
|
|
|
|
|
const DB = join(ordner, "workspace.db");
|
|
|
|
|
const jetzt = new Date().toISOString();
|
|
|
|
|
|
|
|
|
|
let gemacht = 0, fehler = 0;
|
|
|
|
|
const ok = (b, text) => {
|
|
|
|
|
gemacht++;
|
|
|
|
|
if (b) console.log(` ok ${text}`);
|
|
|
|
|
else { fehler++; console.log(` FEHL ${text}`); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let kind = null;
|
|
|
|
|
async function starten() {
|
|
|
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
|
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
|
|
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
|
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
|
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
|
|
|
let aus = "";
|
|
|
|
|
kind.stdout.on("data", (d) => { aus += d; });
|
|
|
|
|
kind.stderr.on("data", (d) => { aus += d; });
|
|
|
|
|
for (let i = 0; i < 80; i++) {
|
|
|
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
|
|
|
if (kind.exitCode !== null) break;
|
|
|
|
|
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
|
|
|
|
if (a.status < 500) return; } catch { /* noch nicht oben */ }
|
|
|
|
|
}
|
|
|
|
|
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
|
|
|
|
}
|
|
|
|
|
await starten();
|
|
|
|
|
kind.kill();
|
|
|
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
|
|
|
|
|
|
|
|
let idNova, idKiro, idLumi;
|
|
|
|
|
{
|
|
|
|
|
const d = new DatabaseSync(DB);
|
|
|
|
|
const anlegen = (name, rolle, code) => {
|
|
|
|
|
const salz = randomBytes(16).toString("hex");
|
|
|
|
|
const hash = scryptSync(code, salz, 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, salz, 32768, jetzt);
|
|
|
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
|
|
|
};
|
|
|
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
|
|
|
/* DREI Creator, nicht einer. Mit einem einzigen waere jede Liste
|
|
|
|
|
zufaellig richtig -- "alle" und "nur dieser" saehen gleich aus. */
|
|
|
|
|
idNova = anlegen("Nova", "creator", "CODE-NOVA-0001");
|
|
|
|
|
idKiro = anlegen("Kiro", "creator", "CODE-KIRO-0001");
|
|
|
|
|
idLumi = anlegen("Lumi", "creator", "CODE-LUMI-0001");
|
|
|
|
|
d.close();
|
|
|
|
|
}
|
|
|
|
|
await starten();
|
|
|
|
|
|
|
|
|
|
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 }) });
|
|
|
|
|
const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
|
|
|
|
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
|
|
|
|
return keks;
|
|
|
|
|
};
|
|
|
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
|
|
|
|
|
|
|
|
/* Die Sicht kommt als Abfrageteil an den Weg -- genauso, wie der
|
|
|
|
|
Browser sie schickt (siehe index.js, `req.sicht`). */
|
|
|
|
|
const hole = async (weg, sicht) => {
|
|
|
|
|
const trenner = weg.includes("?") ? "&" : "?";
|
|
|
|
|
const a = await fetch(BASIS + weg + (sicht ? `${trenner}sicht=${sicht}` : ""), {
|
|
|
|
|
headers: { Cookie: keksDogi } });
|
|
|
|
|
let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ }
|
|
|
|
|
return { status: a.status, daten };
|
|
|
|
|
};
|
|
|
|
|
const namen = (d) => (d?.creator || d?.personen || [])
|
|
|
|
|
.map((x) => x.name).filter(Boolean).sort();
|
|
|
|
|
|
|
|
|
|
/* =======================================================================
|
|
|
|
|
1. In der EIGENEN Sicht sieht DogFather alle
|
|
|
|
|
======================================================================= */
|
|
|
|
|
console.log("=== Eigene Sicht: alle drei ===");
|
|
|
|
|
for (const weg of ["/workspace/api/uebersicht/creator", "/workspace/api/report/creator",
|
|
|
|
|
"/workspace/api/startcheck/creator"]) {
|
|
|
|
|
const { daten } = await hole(weg);
|
|
|
|
|
const n = namen(daten);
|
|
|
|
|
ok(n.length === 3 && n.includes("Nova") && n.includes("Kiro") && n.includes("Lumi"),
|
|
|
|
|
`${weg.replace("/workspace/api/", "")}: ${n.join(", ") || "nichts"}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* =======================================================================
|
|
|
|
|
2. In der Sicht von Nova sieht er NUR Nova
|
|
|
|
|
======================================================================= */
|
|
|
|
|
console.log("\n=== Sicht von Nova: nur Nova ===");
|
|
|
|
|
for (const weg of ["/workspace/api/uebersicht/creator", "/workspace/api/report/creator",
|
|
|
|
|
"/workspace/api/startcheck/creator"]) {
|
|
|
|
|
const { daten } = await hole(weg, idNova);
|
|
|
|
|
const n = namen(daten);
|
|
|
|
|
ok(n.length === 1 && n[0] === "Nova",
|
|
|
|
|
`${weg.replace("/workspace/api/", "")}: ${n.join(", ") || "nichts"}`);
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
/* Der Start-Check sagt zusaetzlich, dass es die EIGENE Sicht der
|
|
|
|
|
angesehenen Person ist -- daran erkennt die Seite, dass sie den
|
|
|
|
|
Umschalter gar nicht erst zeigen muss. */
|
|
|
|
|
const { daten } = await hole("/workspace/api/startcheck/creator", idNova);
|
|
|
|
|
ok(daten?.eigen === true, `startcheck meldet "eigen" (${daten?.eigen})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* =======================================================================
|
|
|
|
|
3. UND SCHREIBEN FOLGT NICHT DER SICHT
|
|
|
|
|
======================================================================= */
|
|
|
|
|
console.log("\n=== Was zum Schreiben da ist, bleibt beim Angemeldeten ===");
|
|
|
|
|
{
|
|
|
|
|
/* /api/personen fuellt die Auswahl "zu wem gehoert dieser Eintrag".
|
|
|
|
|
Das ist eine SCHREIB-Auswahl. Wuerde sie der Sicht folgen, koennte
|
|
|
|
|
DogFather in Novas Sicht nur noch Eintraege fuer Nova anlegen --
|
|
|
|
|
und das waere keine Vereinfachung, sondern ein stiller Verlust von
|
|
|
|
|
Handlungsfaehigkeit an einer Stelle, an der man es nicht vermutet.
|
|
|
|
|
|
|
|
|
|
Die Regel steht in workspace.js: "WER BIN ICH (fuer alles, was
|
|
|
|
|
SCHREIBT)". Diese Pruefung haelt sie fest. */
|
|
|
|
|
const ohne = namen((await hole("/workspace/api/personen")).daten);
|
|
|
|
|
const mit = namen((await hole("/workspace/api/personen", idNova)).daten);
|
|
|
|
|
ok(ohne.length >= 4, `ohne Sicht stehen alle zur Auswahl (${ohne.length})`);
|
|
|
|
|
ok(JSON.stringify(ohne) === JSON.stringify(mit),
|
|
|
|
|
`mit Sicht steht dieselbe Auswahl da (${mit.length})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* =======================================================================
|
|
|
|
|
4. Gegenprobe: Die Messung erkennt den Unterschied wirklich
|
|
|
|
|
======================================================================= */
|
|
|
|
|
console.log("\n=== Gegenproben ===");
|
|
|
|
|
{
|
|
|
|
|
/* Ohne diese Probe hiesse "nur Nova" auch dann ok, wenn die Listen
|
|
|
|
|
grundsaetzlich einelementig waeren. */
|
|
|
|
|
const drei = namen((await hole("/workspace/api/report/creator")).daten);
|
|
|
|
|
const eins = namen((await hole("/workspace/api/report/creator", idNova)).daten);
|
|
|
|
|
ok(drei.length !== eins.length,
|
|
|
|
|
`dieselbe Abfrage liefert mit und ohne Sicht Verschiedenes (${drei.length} gegen ${eins.length})`);
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
/* Und eine Sicht auf jemand anderen muss auch jemand anderen zeigen
|
|
|
|
|
-- sonst waere "Nova" nur zufaellig der erste Eintrag. */
|
|
|
|
|
const n = namen((await hole("/workspace/api/report/creator", idLumi)).daten);
|
|
|
|
|
ok(n.length === 1 && n[0] === "Lumi", `Sicht auf Lumi zeigt Lumi (${n.join(", ")})`);
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
/* Eine erfundene Nummer darf keine Sicht oeffnen. */
|
|
|
|
|
const n = namen((await hole("/workspace/api/report/creator", 999999)).daten);
|
|
|
|
|
ok(n.length === 3, `eine erfundene Nummer aendert nichts (${n.length})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
kind.kill();
|
|
|
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
|
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
|
|
|
|
|
|
|
|
|
console.log(`\n${gemacht} Pruefungen`);
|
|
|
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
|
|
|
|
process.exit(fehler ? 1 : 0);
|