/* ===================================================================== Auf der Adresse von Team Dogi gibt es die Agenturseiten nicht (15.09.2026) Filipe, mit dem Bildschirmfoto des Start-Checks auf crew.: „das gibt es in der app nicht, dass ist nur auf der workspace app aber nicht hier." --------------------------------------------------------------------- ES WAR GRÖSSER ALS DIESE EINE SEITE Die Haustrennung entscheidet, WAS jemand sieht — Daten und Kacheln. Sie entschied nie, welche SEITEN es auf einer Adresse gibt. Die Rechtetafel kennt nur Rollen, keine Adressen. Zwischen beidem lag das Loch: zehn Agenturseiten waren auf crew. erreichbar, drei davon sogar für einen Modi. --------------------------------------------------------------------- UND DIE GEGENPROBE IST HIER DAS WICHTIGSTE Eine Regel, die Seiten wegnimmt, muss beweisen, dass sie die RICHTIGEN wegnimmt — und vor allem, dass auf der Agenturadresse nichts fehlt. Sonst hätte ich Filipe eine Seite repariert und ihm zehn andere genommen, ohne es zu merken. Aufrufen mit: node server/pruef-haus-seiten.mjs ===================================================================== */ import { mkdtempSync, rmSync, readdirSync } 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"; const PORT = await portMussFreiSein(4422, "pruef-haus-seiten"); const ordner = mkdtempSync(join(tmpdir(), "ws-haus-")); 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 }); }; import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); notbremse(240_000, "pruef-haus-seiten"); await new Promise((r) => setTimeout(r, 700)); 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 melde = (t) => console.log(t); const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const CREW = "crew.dogfather-universe.com"; const HAUS = "workspace.dogfather-universe.com"; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const ws = await import("./workspace.js"); const rechte = await import("./rechte.js"); const jetzt = new Date().toISOString(); { const d = new DatabaseSync(process.env.WORKSPACE_DB); const s = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", s, 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", s).update(code).digest("hex"), jetzt); }; anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("Rieke", "hand", "CODE-HAND-0001"); anlegen("Frida", "modi", "CODE-MODI-0001"); anlegen("Mara", "manager", "CODE-MANA-0001"); d.close(); } function ruf(pfad, host, methode, kopf = {}, koerper = null) { return new Promise((fertig, schief) => { const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } }, (antwort) => { let text = ""; antwort.on("data", (s) => { text += s; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text, ort: antwort.headers.location || "", kopf: antwort.headers })); }); a.on("error", schief); if (koerper) a.write(JSON.stringify(koerper)); a.end(); }); } async function anmelden(rolle, code, host) { const r = await ruf("/workspace/api/anmelden", host, "POST", {}, { rolle, code }); return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); } const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS); const kDogiCrew = await anmelden("admin", "CODE-DOGI-0001", CREW); const kHand = await anmelden("hand", "CODE-HAND-0001", CREW); const kModi = await anmelden("modi", "CODE-MODI-0001", CREW); const kMana = await anmelden("manager", "CODE-MANA-0001", HAUS); ok(!!kDogi && !!kDogiCrew && !!kHand && !!kModi && !!kMana, "vier Menschen sind angemeldet, DogFather auf beiden Adressen"); /** Kommt man auf diese Seite? 200 ja, 302 nein. */ const seite = async (datei, host, keks) => ruf("/workspace/" + datei, host, "GET", { cookie: keks }); const SEITEN = readdirSync(new URL("../workspace/", import.meta.url)) .filter((n) => n.endsWith(".html")); /* ======================================================================= 1. DIE GEMELDETE SEITE ======================================================================= */ melde(""); melde("=== 1. Der Start-Check ==="); { const a = await seite("startcheck.html", CREW, kDogiCrew); ok(a.code === 302, `auf der Team-Adresse gibt es ihn nicht (${a.code})`); ok(/start\.html$/.test(a.ort), `man landet auf der Startseite (${a.ort})`); /* DIE GEGENPROBE, OHNE DIE DIE ZEILE DARÜBER NICHTS BEWEIST: Auf der Agenturadresse ist der Start-Check unverändert da. */ const b = await seite("startcheck.html", HAUS, kDogi); ok(b.code === 200, `auf der Agenturadresse ist er da (${b.code})`); ok(/Start-Check/.test(b.text), "und es ist wirklich die Seite"); /* UND EIN MODI KAM DA AUCH HIN -- das war der unangenehmste Teil. */ const m = await seite("startcheck.html", CREW, kModi); ok(m.code === 302, `auch für einen Modi ist er weg (${m.code})`); } /* ======================================================================= 2. ALLE AGENTURSEITEN, NICHT NUR DIESE EINE ======================================================================= */ melde(""); melde("=== 2. Die übrigen neun ==="); { const AGENTUR = ["automation.html", "calls.html", "content.html", "leistung.html", "profil.html", "report.html", "scouting.html", "team.html", "uebersicht.html"]; let zu = 0; for (const s of AGENTUR) { const a = await seite(s, CREW, kDogiCrew); if (a.code === 302) zu++; else ok(false, `${s} ist auf crew. noch offen (${a.code})`); } ok(zu === AGENTUR.length, `alle ${zu} Agenturseiten sind auf der Team-Adresse zu`); /* GEGENPROBE: auf der Agenturadresse ist jede einzelne noch da. */ let offen = 0; for (const s of AGENTUR) { const a = await seite(s, HAUS, kDogi); if (a.code === 200) offen++; else ok(false, `${s} fehlt jetzt auf workspace. (${a.code})`); } ok(offen === AGENTUR.length, `und auf der Agenturadresse sind alle ${offen} unverändert da`); /* UND FÜR EINE MANAGERIN, die es dort auch nur dort gibt. */ const mana = await seite("scouting.html", HAUS, kMana); ok(mana.code === 200, `eine Managerin kommt weiterhin ins Scouting (${mana.code})`); } /* ======================================================================= 3. WAS ZUM TEAM GEHÖRT, BLEIBT ======================================================================= */ melde(""); melde("=== 3. Und was bleibt ==="); { const TEAM = ["start.html", "aufgaben.html", "chat.html", "dateien.html", "kalender.html", "wissen.html", "bereich.html", "entwicklung.html", "befinden.html", "talente.html", "personen.html", "teamlage.html", "treff-regeln.html", "treff-moderation.html", "rechte.html"]; let da = 0; for (const s of TEAM) { const a = await seite(s, CREW, kDogiCrew); if (a.code === 200) da++; else ok(false, `${s} ist auf crew. verschwunden (${a.code})`); } ok(da === TEAM.length, `alle ${da} Seiten des Teams sind da`); /* EIN MODI BEHÄLT SEINE. Besonders die beiden, die keine Kachel haben und trotzdem ihm gehören. */ for (const s of ["entwicklung.html", "befinden.html", "treff-regeln.html", "aufgaben.html"]) { const a = await seite(s, CREW, kModi); ok(a.code === 200, `ein Modi kommt weiterhin auf ${s} (${a.code})`); } /* Aber nicht auf die der Leitung -- das war schon vorher so und muss es bleiben. */ const verwehrt = await seite("personen.html", CREW, kModi); ok(verwehrt.code === 302, `und nicht auf personen.html (${verwehrt.code})`); } /* ======================================================================= 4. DIE REGEL SELBST ======================================================================= */ melde(""); melde("=== 4. Die Regel ==="); { /* SIE IST ABGELEITET, NICHT GEPFLEGT: Was eine Kachel hat, ist erreichbar. Gemessen statt behauptet. */ const person = { id: 1, rolle: "modi", haus: "crew" }; const kacheln = [...(ws.bereicheFuer(person) || []), ...(ws.zusatzBereicheFuer(person) || [])] .map((k) => "/workspace/" + String(k.ziel || "").split("?")[0]); const alle = kacheln.every((p) => ws.gehoertAufDieseAdresse(person, p)); ok(alle, `alle ${kacheln.length} Kacheln eines Modis führen auf erreichbare Seiten`); /* EINE NEUE SEITE IST AUF crew. AUTOMATISCH ZU -- das ist die Richtung, in die diese Regel altern soll. */ ok(!ws.gehoertAufDieseAdresse(person, "/workspace/gibt-es-noch-nicht.html"), "eine Seite, die es noch nicht gibt, ist dort automatisch zu"); /* UND AUF DER AGENTURADRESSE ÄNDERT SIE GAR NICHTS. */ const agentur = { id: 2, rolle: "creator", haus: null }; ok(SEITEN.every((s) => ws.gehoertAufDieseAdresse(agentur, "/workspace/" + s)), `im anderen Haus lässt sie alle ${SEITEN.length} Seiten durch`); /* SIE ÄNDERT KEINE RECHTE. Was jemand darf, steht weiter allein in rechte.js -- die Tafel ist unberührt. */ ok((rechte.tafelJetzt()["/workspace/startcheck.html"] || []).includes("modi"), "in der Rechtetafel steht der Start-Check unverändert"); ok((await seite("startcheck.html", CREW, kModi)).code === 302, "erreichbar ist er auf crew. trotzdem nicht — zwei verschiedene Fragen"); } /* ======================================================================= 5. KEINE SEITE FÄLLT ZWISCHEN DIE STÜHLE ======================================================================= */ melde(""); melde("=== 5. Der Durchgang über alle Seiten ==="); { /* JEDE .html EINZELN, für jede der drei Rollen des Teams. Es geht nicht darum, ob sie offen oder zu ist -- sondern darum, dass KEINE davon eine Antwort gibt, die weder 200 noch 302 ist. Ein 500 an dieser Stelle wäre die Regel, die an einer Seite stolpert. */ let gezaehlt = 0, seltsam = 0; const zu = { admin: [], hand: [], modi: [] }; for (const [rolle, keks] of [["admin", kDogiCrew], ["hand", kHand], ["modi", kModi]]) { for (const s of SEITEN) { const a = await seite(s, CREW, keks); gezaehlt++; if (a.code !== 200 && a.code !== 302 && a.code !== 404) { seltsam++; ok(false, `${s} für ${rolle}: ${a.code}`); } if (a.code !== 200) zu[rolle].push(s); } } ok(gezaehlt === SEITEN.length * 3, `${gezaehlt} Aufrufe: ${SEITEN.length} Seiten × 3 Rollen`); ok(seltsam === 0, "keine einzige antwortet mit einem Fehler"); /* UND DIE ZAHLEN GEHÖREN IN DIE BEDINGUNG, nicht nur in den Text: Ein Durchgang, bei dem alles offen ist, hätte nichts geprüft. */ ok(zu.admin.length > 8, `für DogFather sind ${zu.admin.length} Seiten zu`); ok(zu.modi.length > zu.admin.length, `für einen Modi mehr als für DogFather (${zu.modi.length})`); } /* ------------------------------------------------------------------- */ melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } process.exit(fehler ? 1 : 0);