Creator Workspace: Anmeldung, Sitzungen und Audit-Log

Erster funktionierender Login fuer /workspace. Bewusst OHNE neue
Abhaengigkeiten: Node 24 bringt node:sqlite mit, Hashing und Zufall
kommen aus node:crypto. Nichts zu kompilieren, keine fremde Lieferkette
an einer Stelle, an der es um Zugangsdaten geht.

Sicherheit:
- Codes liegen nur als scrypt-Hash (N=32768) mit eigenem Salt in der DB
- Vergleich in konstanter Zeit (timingSafeEqual)
- Sitzungstoken 32 Byte Zufall, in der DB nur als SHA-256
- Cookie httpOnly, SameSite=lax, Path=/workspace, secure abhaengig von
  req.secure (live immer an, nur der lokale http-Test kommt ohne aus)
- Sperre nach 8 Fehlversuchen je IP fuer 10 Minuten -- danach ist auch
  der richtige Code blockiert (geprueft)
- gleiche Fehlermeldung bei falscher Rolle und falschem Code
- Audit-Log fuer Anmeldung, Fehlversuch, Sperre, Abmeldung, Codewechsel

Die Datenbank liegt AUSSERHALB des Repos (../workspace-daten/): sonst
waere sie ueber express.static aus dem Netz erreichbar, und ein git pull
wuerde Nutzdaten anfassen.

workspace.js kann die Website nicht mitreissen: node:sqlite wird erst bei
Bedarf per createRequire geladen, jede Route faengt ihre Fehler selbst ab.
Faellt die DB aus, antwortet nur /workspace/api/* mit 503.

Codes werden ausschliesslich auf der Kommandozeile erzeugt
(server/workspace-code.js) und dort einmal angezeigt -- nie in Git.

Ausserdem: start.html als geschuetzte Seite nach dem Anmelden. Der Schutz
sitzt serverseitig vor express.static, nicht nur im Browser.
This commit is contained in:
2026-08-27 22:35:55 +02:00
parent 1f769c9de7
commit f3a55af80f
7 changed files with 688 additions and 1 deletions
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env node
/* =====================================================================
workspace-code.js — Zugangscodes für den Creator Workspace verwalten.
Läuft NUR auf der Kommandozeile des Servers. Der Code wird dabei einmal
angezeigt und danach nirgends mehr gespeichert -- in der Datenbank liegt
ausschließlich sein scrypt-Hash. Geht ein Code verloren, wird ein neuer
erzeugt; wiederherstellen lässt er sich nicht. Das ist Absicht.
Aufrufe:
node server/workspace-code.js liste
node server/workspace-code.js anlegen "Dogfather" admin
node server/workspace-code.js neu 3
node server/workspace-code.js sperren 3
node server/workspace-code.js entsperren 3
node server/workspace-code.js protokoll [anzahl]
===================================================================== */
import {
personAnlegen, personenListe, personSperren, codeNeu, protokollLesen,
} from "./workspace.js";
const [, , befehl, ...rest] = process.argv;
function zeigeCode(p) {
const linie = "─".repeat(52);
console.log(`\n${linie}`);
console.log(` ${p.name} · Rolle: ${p.rolle} · Nummer: ${p.id}`);
console.log(` Zugangscode: ${p.code}`);
console.log(`${linie}`);
console.log(" Dieser Code wird NUR JETZT angezeigt. Sicher weitergeben");
console.log(" (z. B. über Bitwarden) und hier nicht notieren.\n");
}
try {
switch (befehl) {
case "anlegen": {
const [name, rolle] = rest;
if (!name || !rolle) throw new Error('Aufruf: anlegen "Name" <admin|creator|scout>');
zeigeCode(personAnlegen(name, rolle));
break;
}
case "neu": {
const id = Number(rest[0]);
if (!id) throw new Error("Aufruf: neu <Nummer>");
zeigeCode(codeNeu(id));
console.log(" Alle offenen Sitzungen dieser Person wurden beendet.\n");
break;
}
case "sperren":
case "entsperren": {
const id = Number(rest[0]);
if (!id) throw new Error(`Aufruf: ${befehl} <Nummer>`);
personSperren(id, befehl === "entsperren");
console.log(`Person ${id} ist jetzt ${befehl === "entsperren" ? "aktiv" : "gesperrt"}.`);
break;
}
case "liste": {
const alle = personenListe();
if (!alle.length) { console.log("Noch keine Personen angelegt."); break; }
console.log("\n Nr Rolle Name Status Sitzungen Letzte Anmeldung");
console.log(" ".padEnd(78, "─"));
for (const p of alle) {
console.log(
` ${String(p.id).padStart(2)} ${p.rolle.padEnd(8)} ${p.name.slice(0, 20).padEnd(20)} `
+ `${(p.aktiv ? "aktiv" : "gesperrt").padEnd(9)} ${String(p.sitzungen).padStart(6)} `
+ `${p.letzter_login ? p.letzter_login.slice(0, 16).replace("T", " ") : "—"}`);
}
console.log("");
break;
}
case "protokoll": {
const eintraege = protokollLesen(Number(rest[0]) || 20);
if (!eintraege.length) { console.log("Protokoll ist leer."); break; }
console.log("");
for (const e of eintraege) {
console.log(` ${e.zeitpunkt.slice(0, 19).replace("T", " ")} ${(e.aktion || "").padEnd(24)} `
+ `${(e.rolle || "—").padEnd(8)} ${(e.detail || "").padEnd(18)} ${e.ip || ""}`);
}
console.log("");
break;
}
default:
console.log(`
Zugangscodes für den Creator Workspace
liste alle Personen anzeigen
anlegen "Name" <rolle> Person anlegen, Code wird einmal angezeigt
neu <Nr> neuen Code erzeugen (beendet alte Sitzungen)
sperren <Nr> / entsperren <Nr> Zugang sperren oder wieder freigeben
protokoll [Anzahl] letzte Einträge des Audit-Logs
Rollen: admin, creator, scout
`);
}
} catch (fehler) {
console.error(`\nFehler: ${fehler.message}\n`);
process.exit(1);
}