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.
81 lines
3.3 KiB
JavaScript
81 lines
3.3 KiB
JavaScript
/* ===================================================================
|
|
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();
|
|
})();
|