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
+102
View File
@@ -0,0 +1,102 @@
/* ===================================================================
Startseite nach dem Anmelden. Baut auf den Farben und Bausteinen von
gate.css auf, damit beides wie eine Anwendung wirkt und nicht wie zwei
verschiedene Seiten.
=================================================================== */
/* Grundschrift und Grundfarbe stehen in gate.css auf `body.gate` und
greifen hier deshalb NICHT. Ohne diese Wiederholung fällt die Seite auf
die Browservorgaben zurück: schwarze Serifenschrift auf dunklem Grund,
praktisch unlesbar. */
body.start {
display: block;
min-height: 100svh;
margin: 0;
padding: 0;
color: var(--text);
font-family: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
line-height: 1.55;
letter-spacing: .01em;
background:
radial-gradient(120% 80% at 12% -10%, rgba(63, 189, 245, .10), transparent 58%),
radial-gradient(100% 70% at 92% 8%, rgba(138, 118, 255, .10), transparent 60%),
var(--tinte);
}
/* ---------- Kopfleiste ------------------------------------------------ */
.kopfleiste {
position: sticky; top: 0; z-index: 5;
display: flex; align-items: center; justify-content: space-between; gap: 16px;
padding: 14px clamp(16px, 4vw, 40px);
background: rgba(6, 9, 16, .82);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border-bottom: 1px solid var(--rand);
}
.kopfleiste .marke { margin: 0; font-size: .72rem; }
.kopfleiste__rechts { display: flex; align-items: center; gap: 12px; }
.wer {
font-size: .86rem; color: var(--text-leise);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46vw;
}
.abmelden {
padding: 7px 14px;
background: rgba(255, 255, 255, .045);
border: 1px solid var(--rand);
border-radius: 9px;
color: var(--text); font: inherit; font-size: .84rem; cursor: pointer;
transition: border-color var(--tempo), background var(--tempo);
}
.abmelden:hover { background: rgba(255, 255, 255, .08); border-color: var(--rand-hell); }
/* ---------- Inhalt ---------------------------------------------------- */
.inhalt {
max-width: 880px;
margin: 0 auto;
padding: clamp(28px, 5vw, 56px) clamp(16px, 4vw, 40px) 64px;
}
.willkommen { margin-bottom: 38px; }
.willkommen .titel { margin-bottom: 6px; }
.bereiche {
list-style: none; margin: 0; padding: 0;
display: grid; gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
}
.bereich {
display: flex; align-items: baseline; gap: 10px;
padding: 13px 15px;
background: rgba(255, 255, 255, .03);
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
}
.bereich__name { font-weight: 600; font-size: .95rem; }
.bereich__stand {
margin-left: auto;
font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
color: var(--text-still);
white-space: nowrap;
}
/* Phase 1 ist in Arbeit - das wird hervorgehoben, der Rest bleibt ruhig. */
.bereich[data-phase="1"] { border-color: rgba(63, 189, 245, .34); }
.bereich[data-phase="1"] .bereich__stand { color: var(--akzent); }
.inhalt .fuss { margin-top: 40px; text-align: left; }
@media (max-width: 520px) {
.kopfleiste { padding: 12px 16px; }
.kopfleiste .marke { font-size: .64rem; }
.wer { max-width: 34vw; }
}