Workspace: Aufgaben bearbeiten, Personenverwaltung im Browser

Aufgaben:
- Bearbeiten-Dialog (Titel, Beschreibung, Prioritaet, Frist, Zuordnung).
  Als natives <dialog>: Fokusfang, Esc zum Schliessen und Abdunklung
  ohne eigenen Code.
- Loeschen nur fuer Management, mit Rueckfrage und Protokolleintrag.
  Das Konzept will, dass Erledigtes stehen bleibt -- Loeschen ist der
  Ausnahmefall fuer Fehleintraege, nicht der normale Abschluss.

Personen (/workspace/personen.html, nur Management):
- Anlegen, Code erneuern, sperren/entsperren, Protokollansicht
- Der Code wird genau einmal in der Antwort zurueckgegeben, nie
  gespeichert; beim Schliessen auch aus dem Dokument entfernt
- Selbstschutz: niemand kann sich selbst sperren, und das letzte aktive
  Management laesst sich nicht sperren -- sonst kaeme niemand mehr hinein
- Code fuer sich selbst tauschen nur mit ausdruecklicher Bestaetigung,
  weil es die eigene Sitzung sofort beendet

Zwei Fehler, die beim Testen aufgefallen sind:

1. Rollenpruefung fehlte beim Ausliefern der Seiten. Ein Creator bekam
   personen.html mit HTTP 200 -- die Schnittstellen wiesen ihn zwar ab,
   das Geruest der Seite war aber sichtbar. GESCHUETZT ist jetzt eine
   Zuordnung Pfad -> erlaubte Rollen statt einer blossen Liste.

2. Das Protokoll nannte den falschen Verursacher. personAnlegen trug die
   NEU ANGELEGTE Person als person_id ein, der Eintrag las sich also so,
   als haette sie sich selbst angelegt. Akteur und Betroffener sind jetzt
   getrennt: Akteur in person_id, Betroffener im Text. Ueber die
   Kommandozeile angelegte Personen zeigen korrekt keinen Akteur.
This commit is contained in:
2026-08-27 23:12:28 +02:00
parent 2eecb40537
commit 232a2003dd
11 changed files with 759 additions and 12 deletions
+39 -10
View File
@@ -247,13 +247,27 @@ export const workspaceRouter = express.Router();
/* Schutz der angemeldeten Seiten. Serverseitig, nicht nur im Browser --
sonst könnte man die Seite einfach direkt aufrufen. */
const GESCHUETZT = ["/workspace/start.html", "/workspace/aufgaben.html"];
/* Pfad -> erlaubte Rollen. `null` heisst: jede angemeldete Rolle.
Die Rolle wird hier mitgeprueft und nicht nur in der Schnittstelle.
Sonst bekommt z. B. ein Creator die Verwaltungsseite zwar ausgeliefert
(HTTP 200) und sieht ihr Geruest, auch wenn sie danach leer bleibt und
das Skript ihn wegschickt. Sichtbar sein soll sie gar nicht. */
const GESCHUETZT = {
"/workspace/start.html": null,
"/workspace/aufgaben.html": null,
"/workspace/personen.html": ["admin"],
};
workspaceRouter.use((req, res, next) => {
if (!GESCHUETZT.includes(req.path)) return next();
if (!Object.hasOwn(GESCHUETZT, req.path)) return next();
const person = sitzungLesen(req);
if (person) return next();
return res.redirect(302, "/workspace/");
if (!person) return res.redirect(302, "/workspace/");
const erlaubt = GESCHUETZT[req.path];
if (erlaubt && !erlaubt.includes(person.rolle)) {
return res.redirect(302, "/workspace/start.html");
}
return next();
});
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
@@ -343,7 +357,12 @@ export function codeErzeugen(gruppen = 4, laenge = 4) {
return aus;
}
export function personAnlegen(name, rolle) {
/* `akteur` ist WER die Aktion ausloest -- nicht, wen sie betrifft. Das
muss getrennt bleiben: Stand im Protokoll die neu angelegte Person als
person_id, las sich der Eintrag so, als haette sie sich selbst angelegt.
Wer betroffen ist, steht im Text. Ohne Akteur (Kommandozeile) bleibt
das Feld leer. */
export function personAnlegen(name, rolle, akteur = null) {
if (!ROLLEN.has(rolle)) throw new Error(`Unbekannte Rolle: ${rolle}`);
const code = codeErzeugen();
const salt = randomBytes(16).toString("hex");
@@ -351,11 +370,14 @@ export function personAnlegen(name, rolle) {
const { lastInsertRowid } = db().prepare(
"INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)"
).run(name, rolle, hash, salt, SCRYPT.N, jetzt());
protokolliere("person_angelegt", { personId: Number(lastInsertRowid), rolle, detail: name });
protokolliere("person_angelegt", {
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
ip: akteur?.ip ?? null, detail: `${name} (${rolle})`,
});
return { id: Number(lastInsertRowid), name, rolle, code };
}
export function codeNeu(id) {
export function codeNeu(id, akteur = null) {
const person = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
if (!person) throw new Error(`Keine Person mit Nummer ${id}`);
const code = codeErzeugen();
@@ -365,14 +387,21 @@ export function codeNeu(id) {
/* Alle offenen Sitzungen beenden -- ein neuer Code soll den alten Zugang
wirklich beenden, nicht nur die nächste Anmeldung betreffen. */
db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
protokolliere("code_erneuert", { personId: id, rolle: person.rolle, detail: person.name });
protokolliere("code_erneuert", {
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
ip: akteur?.ip ?? null, detail: `für ${person.name}`,
});
return { ...person, code };
}
export function personSperren(id, aktiv = 0) {
export function personSperren(id, aktiv = 0, akteur = null) {
const person = db().prepare("SELECT name FROM personen WHERE id = ?").get(id);
db().prepare("UPDATE personen SET aktiv = ? WHERE id = ?").run(aktiv ? 1 : 0, id);
if (!aktiv) db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
protokolliere(aktiv ? "person_entsperrt" : "person_gesperrt", { personId: id });
protokolliere(aktiv ? "person_entsperrt" : "person_gesperrt", {
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
ip: akteur?.ip ?? null, detail: person?.name ?? `#${id}`,
});
}
export function personenListe() {