diff --git a/server/pruef-an-alle.mjs b/server/pruef-an-alle.mjs new file mode 100644 index 00000000..58680c7e --- /dev/null +++ b/server/pruef-an-alle.mjs @@ -0,0 +1,231 @@ +/* ===================================================================== + „AN ALLE" BEI „AN WEN" (24.09.2026) + + Filipe: „bei an ween fehlt noch die option alle neben den namen + allen." + + DREI FRAGEN, und die dritte ist die, an der es schiefgehen kann: + + 1. Trifft „alle" wirklich JEDEN -- oder nur den ersten? + 2. Bleibt der Duplikat-Schutz erhalten? Er fragt, was EINE Person + schon hat; bei „alle" muss er je Person neu gestellt werden. + Sonst bekaeme nur die erste ihre Pruefung und alle anderen + die Vorlage doppelt -- genau so entstanden am 22.09. aus zwei + Klicks 112 Aufgaben. + 3. Kommt jemand darueber an Leute heran, die ihn nichts angehen? + „alle" darf nicht mehr treffen als die Namen daneben. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-aa-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +import { notbremse } from "./helfer-notbremse.mjs"; +const { eigenerPort } = await import("./helfer-port.mjs"); +const PORT = await eigenerPort(import.meta, "die An-alle-Pruefung"); + +process.env.PORT = `${PORT}`; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-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"); +notbremse(240_000, "pruef-an-alle"); +await new Promise((r) => setTimeout(r, 900)); + +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); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const ks = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", ks, jetzt); +function anlegen(name, rolle, code, aktiv = 1) { + 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 (?,?,?,?,?,?,?,?)") + .run(name, rolle, hash, salt, 32768, + createHmac("sha256", ks).update(code).digest("hex"), aktiv, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const modis = [ + anlegen("Miss", "modi", "CODE-MODI-0001"), + anlegen("Ghost", "modi", "CODE-MODI-0002"), + anlegen("Marina", "modi", "CODE-MODI-0003"), +]; +/* EIN GESPERRTER MODI. Er darf NICHT mitbekommen -- ein Zugang, der + nicht hereinkommt, braucht keine Aufgabenliste. */ +const gesperrt = anlegen("Diene", "modi", "CODE-MODI-0004", 0); +/* Und ein Creator: Er steht gar nicht in dieser Auswahl und darf + auch ueber „alle" nichts bekommen. */ +const creator = anlegen("Luna", "creator", "CODE-CREA-0001"); +d.close(); + +const CREW = "crew.dogfather-universe.com"; +const { request } = await import("node:http"); +function roh(pfad, keks, koerper, methode) { + return new Promise((fertig, schief) => { + const daten = koerper ? JSON.stringify(koerper) : null; + const a = request({ + host: "127.0.0.1", port: PORT, path: pfad, + method: methode || (daten ? "POST" : "GET"), + headers: { + Host: CREW, Origin: `https://${CREW}`, + ...(keks ? { Cookie: keks } : {}), + ...(daten ? { "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(daten) } : {}), + }, + }, (antwort) => { + let text = ""; + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text })); + }); + a.on("error", schief); + if (daten) a.write(daten); + a.end(); + }); +} +const json = (r) => { try { return JSON.parse(r.text); } catch { return {}; } }; + +async function anmelden(rolle, code) { + const r = await new Promise((fertig, schief) => { + const daten = JSON.stringify({ rolle, code, alter_ok: true }); + const a = request({ + host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", + method: "POST", + headers: { Host: CREW, Origin: `https://${CREW}`, + "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) }, + }, (antwort) => { + let text = ""; + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers })); + }); + a.on("error", schief); + a.write(daten); a.end(); + }); + if (r.code !== 200) return null; + return [].concat(r.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +} + +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +const kModi = await anmelden("modi", "CODE-MODI-0001"); +ok(!!kDogi && !!kModi, "DogFather und ein Modi sind angemeldet"); + +/* Wie viele Aufgaben hat wer? Direkt aus der Datenbank -- die Antwort + der Route zu glauben hiesse, sie mit sich selbst zu pruefen. */ +function zaehle(personId) { + const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); + const { n } = db2.prepare( + "SELECT COUNT(*) AS n FROM aufgaben WHERE verantwortlich_id = ? AND vorlage IS NOT NULL") + .get(personId); + db2.close(); + return n; +} + +/* Welche Kategorie gibt es ueberhaupt? Aus dem Katalog, den die Seite + auch bekommt -- eine erfundene Gruppe waere eine Pruefung, die + nichts trifft. */ +const katalog = json(await roh("/workspace/api/vorlagen", kDogi)); +const ersteGruppe = (katalog.katalog?.gruppen || [])[0]?.schluessel + || (katalog.katalog?.aufgaben || [])[0]?.gruppe || ""; +ok(!!ersteGruppe, `es gibt eine Kategorie zum Verteilen (${ersteGruppe})`); + +melde("\n=== 1. alle trifft jeden ==="); +{ + const vorher = modis.map(zaehle); + ok(vorher.every((x) => x === 0), `vorher hat niemand etwas (${vorher.join(", ")})`); + + const a = await roh("/workspace/api/vorlagen/uebernehmen", kDogi, + { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle" }); + ok(a.code === 200, `die Anfrage geht durch (${a.code})`); + const r = json(a); + ok((r.angelegt || 0) > 0, `es wurde etwas angelegt (${r.angelegt})`); + + const nachher = modis.map(zaehle); + ok(nachher.every((x) => x > 0), + `JEDER der drei Modis hat jetzt Aufgaben (${nachher.join(", ")})`); + ok(new Set(nachher).size === 1, + "und alle gleich viele -- sonst hat die Schleife jemanden ausgelassen"); + ok((r.angelegt || 0) === nachher[0] * modis.length, + `die Zahl in der Antwort passt zur Datenbank (${r.angelegt} = ${nachher[0]} x ${modis.length})`); +} + +melde("\n=== 2. Der Duplikat-Schutz gilt je Person ==="); +{ + const vorher = modis.map(zaehle); + const a = await roh("/workspace/api/vorlagen/uebernehmen", kDogi, + { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle" }); + ok(a.code === 200, `ein zweiter Klick geht durch (${a.code})`); + const r = json(a); + ok((r.angelegt || 0) === 0, + `und legt NICHTS mehr an (${r.angelegt}) -- genau das war der Fehler vom 22.09.`); + /* `uebersprungen` ist eine ZAHL, kein Feld -- nachgesehen in + workspace-vorlagen.js (`uebersprungen: uebersprungen.length`). + Mein erster Versuch rief `.length` darauf auf und bekam + `undefined`; ein Messfehler, der wie eine fehlende Antwort + aussah. */ + ok(Number(r.uebersprungen) > 0, + `stattdessen sagt die Antwort, wie viele schon lagen (${r.uebersprungen})`); + ok(Number(r.uebersprungen) === 36, + `und zwar alle 36 -- 12 Vorlagen mal 3 Modis (${r.uebersprungen})`); + const nachher = modis.map(zaehle); + ok(nachher.join() === vorher.join(), + `niemand hat etwas doppelt (${nachher.join(", ")})`); +} + +melde("\n=== 3. alle trifft nicht mehr als die Namen ==="); +{ + ok(zaehle(gesperrt) === 0, + `ein GESPERRTER Modi bekommt nichts (${zaehle(gesperrt)})`); + ok(zaehle(creator) === 0, + `ein Creator erst recht nicht (${zaehle(creator)})`); + + /* Und wer gar nicht verteilen darf, kommt auch ueber „alle" nicht + weiter -- sonst waere der neue Wert ein Weg an der Rechtefrage + vorbei. */ + const alsModi = await roh("/workspace/api/vorlagen/uebernehmen", kModi, + { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle" }); + ok(alsModi.code >= 400, + `ein Modi kann nicht an alle verteilen (${alsModi.code})`); +} + +melde("\n=== 4. Gegenproben ==="); +{ + /* EINE EINZELNE PERSON GEHT WEITERHIN -- sonst hiesse „alle + funktioniert" nur, dass der alte Weg kaputt ist. */ + const einzeln = await roh("/workspace/api/vorlagen/uebernehmen", kDogi, + { art: "katalog", nr: 0, gruppe: ersteGruppe, person_id: modis[0] }); + ok(einzeln.code === 200 || einzeln.code === 400, + `der Weg an EINE Person antwortet noch (${einzeln.code})`); + + /* Ein erfundener Wert ist kein "alle". */ + const quatsch = await roh("/workspace/api/vorlagen/uebernehmen", kDogi, + { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle-zusammen" }); + ok(quatsch.code >= 400, + `alle-zusammen ist nicht alle (${quatsch.code})`); + + /* Und ohne Person weiterhin die alte Absage. */ + const ohne = await roh("/workspace/api/vorlagen/uebernehmen", kDogi, + { art: "katalog_alle", gruppe: ersteGruppe, stufe: "" }); + ok(ohne.code === 400, `ohne Person immer noch 400 (${ohne.code})`); +} + +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + +console.log(""); +console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index 320e028a..d6509583 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -1903,16 +1903,44 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, if (!darfAufgabenVerteilen(req.person)) { return res.status(403).json({ fehler: "erst_bewerben" }); } - const zielId = Number(req.body.person_id) || null; - if (!zielId) return res.status(400).json({ fehler: "Bitte zuerst eine Person wählen." }); + /* ==== „AN ALLE" (24.09.2026) ================================ + + Filipe: „bei an ween fehlt noch die option alle neben den + namen allen." + + EINE ANFRAGE, NICHT SECHS. Der Browser koennte fuer jede + Person einzeln anfragen -- und beim dritten von sechs + Aufrufen die Verbindung verlieren. Dann haette die Haelfte + des Teams die Aufgabe und die andere nicht, und niemand + saehe, wo es abgebrochen ist. Hier laeuft alles in einem + Durchgang. + + DER DUPLIKAT-SCHUTZ GILT JE PERSON. Er steht weiter unten + und fragt, was diese eine Person schon hat -- bei „alle" + wird er deshalb pro Person neu gestellt. Wer die Vorlage + schon liegen hat, bekommt sie nicht doppelt; genau so + entstanden am 22.09. die 112 Aufgaben aus zwei Klicks. */ + const anAlle = String(req.body.person_id) === "alle"; + const zielId = anAlle ? null : (Number(req.body.person_id) || null); + if (!anAlle && !zielId) { + return res.status(400).json({ fehler: "Bitte zuerst eine Person wählen." }); + } /* Nur an jemanden, fuer den die Kategorien ueberhaupt gelten -- sonst landete eine Modi-Aufgabe samt Kategorie an einem Creator, und die Trennung waere von innen aufgeweicht. */ - const zielPerson = db().prepare( - "SELECT id, rolle FROM personen WHERE id = ? AND aktiv = 1").get(zielId); - if (!zielPerson || zielPerson.rolle !== "modi") { + const zielListe = anAlle + ? db().prepare( + "SELECT id, rolle FROM personen WHERE aktiv = 1 AND rolle = 'modi' ORDER BY id").all() + : [db().prepare( + "SELECT id, rolle FROM personen WHERE id = ? AND aktiv = 1").get(zielId)] + .filter(Boolean); + if (!zielListe.length || zielListe.some((z) => z.rolle !== "modi")) { return res.status(404).json({ fehler: "nicht_gefunden" }); } + /* Nur noch als Beleg, dass die Liste nicht leer ist -- gelesen + wird sie in der Schleife weiter unten. Ein ungenutzter Wert + waere Rauschen; dieser sagt etwas. */ + if (!zielListe[0]) return res.status(404).json({ fehler: "nicht_gefunden" }); /* AUS DEM KATALOG, DEN DIE SEITE AUCH BEKOMMEN HAT (16.09.2026). @@ -1961,19 +1989,30 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, Dieselbe Sperre steht seit dem 10.09. im Termin-Zweig weiter unten. Dass sie hier fehlte, ist nicht aufgefallen, weil niemand zweimal drueckt -- bis es jemand tat. */ - const liegtSchon = art === "katalog_alle" - ? new Set(db().prepare( - `SELECT vorlage FROM aufgaben - WHERE verantwortlich_id = ? - AND vorlage IS NOT NULL - AND status NOT IN ('erledigt', 'abgebrochen')`) - .all(zielPerson.id).map((z) => z.vorlage)) - : new Set(); + /* JE PERSON EINMAL (24.09.2026, fuer „An alle"). + + Die Duplikatsperre fragt, was DIESE Person schon liegen + hat -- deshalb steht sie INNERHALB der Schleife. Waere sie + davor, bekaeme bei „alle" nur die erste Person ihre + Pruefung, und alle anderen haetten die Vorlage doppelt. + + Bei einer einzelnen Person aendert sich nichts: Die + Schleife laeuft dann genau einmal. */ const lagSchonDa = []; - for (const v of auswahl) { - if (liegtSchon.has(v.schluessel)) { lagSchonDa.push(v.titel); continue; } - katalogAufgabeAnlegen(v, zielPerson.id, req.person.id); - angelegt.push(v.titel); + for (const ziel of zielListe) { + const liegtSchon = art === "katalog_alle" + ? new Set(db().prepare( + `SELECT vorlage FROM aufgaben + WHERE verantwortlich_id = ? + AND vorlage IS NOT NULL + AND status NOT IN ('erledigt', 'abgebrochen')`) + .all(ziel.id).map((z) => z.vorlage)) + : new Set(); + for (const v of auswahl) { + if (liegtSchon.has(v.schluessel)) { lagSchonDa.push(v.titel); continue; } + katalogAufgabeAnlegen(v, ziel.id, req.person.id); + angelegt.push(v.titel); + } } /* DIE ZAHL GEHOERT IN DIE ANTWORT, nicht nur in die Datenbank. Ein Knopf, der "12 uebernehmen" heisst und dann schweigend diff --git a/server/workspace.js b/server/workspace.js index 28f7108a..16f4c144 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -6268,6 +6268,23 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { weiterarbeiten. Fuer DogFather und die rechte Hand ist sie nicht leer, fuer alle anderen schon. */ darf_zugaenge_verwalten: darfAnlegen(person).length > 0, + /* IN WELCHEM HAUS BIN ICH GERADE? (24.09.2026) + + Filipe: „in dieser app gibt es keinen und wird es niemals einen + anderen creator geben wie mich." + + Die Aufgabenseite beschriftete eine Filterreihe mit „Alle + Creator" -- und auf der Team-Dogi-Adresse standen darunter + Modis. Es gibt dort keine Creator, und es wird nie welche + geben. Die Oberflaeche konnte das bisher nicht wissen: `haus` + steht an der Sitzung, wurde aber nie mitgeschickt. + + ABGELEITET, NICHT GERATEN. Eine Rollenliste im Browser + („hand, linke, modi heisst crew") waere die naechste zweite + Wahrheit -- und sie waere falsch fuer DogFather, der in beiden + Haeusern arbeitet. Hier entscheidet dieselbe Angabe, die auch + `hausBedingung` und `bereicheFuer` benutzen. */ + haus: person?.haus === "crew" ? "crew" : "agentur", /* UND LOESCHEN -- getrennt, weil es das Einzige ist, was sich nicht zuruecknehmen laesst. Heute dieselbe Antwort; sollte Filipe es spaeter wieder einschraenken, ist hier die Stelle. */ diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index ff5ee714..c44395e4 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +