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:
@@ -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; }
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
laden(true);
|
||||
|
||||
try {
|
||||
const antwort = await fetch('/api/anmelden', {
|
||||
const antwort = await fetch('/workspace/api/anmelden', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rolle: gewaehlt, code: wert }),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/* ===================================================================
|
||||
Startseite nach dem Anmelden.
|
||||
Die Seite ist bereits serverseitig geschützt (workspace.js). Dieses
|
||||
Skript holt nur noch, WER angemeldet ist, und regelt das Abmelden.
|
||||
=================================================================== */
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const ROLLENTEXT = {
|
||||
admin: 'Management · Gesamtübersicht und Freigaben',
|
||||
creator: 'Creator · dein eigener Bereich',
|
||||
scout: 'Scout · deine Pipeline und Kontakte',
|
||||
};
|
||||
|
||||
/* Bereiche aus dem Konzept, mit der Phase aus dem Umsetzungsplan.
|
||||
`rollen` steuert, wem ein Bereich überhaupt angezeigt wird - schon
|
||||
jetzt, damit die Trennung von Anfang an sichtbar ist. */
|
||||
const BEREICHE = [
|
||||
{ name: 'Dashboard', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Creator-Profile', phase: 1, rollen: ['admin'] },
|
||||
{ name: 'Mein Profil', phase: 1, rollen: ['creator'] },
|
||||
{ name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Kalender', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Dateien', phase: 1, rollen: ['admin', 'creator'] },
|
||||
{ name: 'LIVE-Analyse', phase: 2, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Content-Planung', phase: 2, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Technik', phase: 2, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Community', phase: 2, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Schutz & Regeln', phase: 2, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Reports & Review', phase: 2, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Calls', phase: 3, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Scout-Pipeline', phase: 3, rollen: ['admin', 'scout'] },
|
||||
{ name: 'Automationen', phase: 4, rollen: ['admin'] },
|
||||
];
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
async function laden() {
|
||||
let ich;
|
||||
try {
|
||||
const antwort = await fetch('/workspace/api/ich', { credentials: 'same-origin' });
|
||||
if (!antwort.ok) { location.assign('/workspace/'); return; }
|
||||
ich = await antwort.json();
|
||||
} catch {
|
||||
location.assign('/workspace/');
|
||||
return;
|
||||
}
|
||||
|
||||
$('wer').textContent = `${ich.name} · ${ich.rolle}`;
|
||||
$('gruss').textContent = `Willkommen, ${ich.name}`;
|
||||
$('rollentext').textContent = ROLLENTEXT[ich.rolle] || ich.rolle;
|
||||
|
||||
const liste = $('bereiche');
|
||||
for (const b of BEREICHE.filter((b) => b.rollen.includes(ich.rolle))) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bereich';
|
||||
li.dataset.phase = String(b.phase);
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'bereich__name';
|
||||
name.textContent = b.name;
|
||||
|
||||
const stand = document.createElement('span');
|
||||
stand.className = 'bereich__stand';
|
||||
stand.textContent = b.phase === 1 ? 'als Nächstes' : `Phase ${b.phase}`;
|
||||
|
||||
li.append(name, stand);
|
||||
liste.append(li);
|
||||
}
|
||||
}
|
||||
|
||||
$('abmelden').addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' });
|
||||
} catch { /* Auch ohne Antwort weiterleiten - das Cookie ist dann spätestens abgelaufen. */ }
|
||||
location.assign('/workspace/');
|
||||
});
|
||||
|
||||
laden();
|
||||
})();
|
||||
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Dogfather Universe · Creator Workspace</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css" />
|
||||
<link rel="stylesheet" href="assets/css/start.css" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<header class="kopfleiste">
|
||||
<p class="marke">Dogfather Universe · Creator Workspace</p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt">
|
||||
<section class="willkommen">
|
||||
<p class="marke">Angemeldet</p>
|
||||
<h1 class="titel" id="gruss">Willkommen</h1>
|
||||
<p class="unterzeile" id="rollentext">…</p>
|
||||
</section>
|
||||
|
||||
<!-- Die Bereiche aus dem Konzept. Noch keiner ist gebaut - das steht hier
|
||||
ehrlich dran, statt Kacheln anzubieten, die ins Leere führen. -->
|
||||
<section>
|
||||
<p class="feldschild">Bereiche aus dem Konzept</p>
|
||||
<ul class="bereiche" id="bereiche"></ul>
|
||||
</section>
|
||||
|
||||
<p class="fuss">
|
||||
Anmeldung, Sitzung und Protokoll funktionieren bereits.
|
||||
Die Bereiche werden nach dem Phasenplan gebaut.
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/start.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user