/workspace/dateien.html -- Ablegen per Ziehen oder Auswaehlen, Entwurf -> Review -> Freigabe, Filter je Zustand. OHNE NEUE ABHAENGIGKEIT. Uploads laufen ueblicherweise ueber multer. Hier schickt der Browser die Datei roh im Rumpf, der Name steht im Kopf (URI-kodiert, damit Umlaute heil ankommen). express.raw() bringt den passenden Empfaenger schon mit -- kein Zerlegen von multipart/form-data von Hand, was gerade hier heikel waere. Die drei Punkte, an denen Dateiablagen typischerweise scheitern: 1. Die Dateien liegen AUSSERHALB des Repos (workspace-daten/dateien/). Lagen sie darin, wuerde express.static sie ungeprueft ans Netz geben. 2. Auf der Platte traegt jede Datei einen erzeugten Zufallsnamen, der Originalname steht nur in der Datenbank. Geprueft mit dem Dateinamen "../../etc/passwd": gespeichert wurde "passwd", auf der Platte ein Zufallsname IM Ordner -- nichts ist ausgebrochen. 3. Ausgeliefert wird immer als Download mit neutralem Typ, dazu nosniff und CSP sandbox. Geprueft mit einer hochgeladenen boese.html: kommt als application/octet-stream zurueck, kann also keinen Code im Namen der Domain ausfuehren. Freigabe nach Konzept: Der Zustand "freigegeben" ist eine Abnahme und bleibt dem Management vorbehalten. Geprueft -- Luna kann Entwurf -> Review, aber nicht freigeben (403); eine freigegebene Datei kann sie weder aendern noch loeschen. Sichtbarkeit wie ueberall: Chef 2 Dateien, Luna 1, Mika 0. Zugriff auf eine fremde Datei: 404, nicht 403. Ein Fehler beim Testen gefunden: Bei zu grosser Datei brach express.raw ab, BEVOR der eigene Code lief -- der Fehler landete im allgemeinen Behandler als HTTP 500. Fuer die Nutzerin sah das aus wie ein kaputter Server statt wie "Datei zu gross". Jetzt faengt ein eigener Fehlerbehandler am Ende des Routers das ab und antwortet mit 413 und einer verstaendlichen Meldung. Damit ist Phase 1 aus dem Konzept vollstaendig.
129 lines
4.9 KiB
JavaScript
129 lines
4.9 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: 'Personen & Zugänge', phase: 1, rollen: ['admin'], ziel: 'personen.html' },
|
|
{ name: 'Creator-Profile', phase: 1, rollen: ['admin'], ziel: 'profil.html' },
|
|
{ name: 'Mein Profil', phase: 1, rollen: ['creator'], ziel: 'profil.html' },
|
|
{ name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'aufgaben.html' },
|
|
{ name: 'Kalender', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'kalender.html' },
|
|
{ name: 'Dateien', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'dateien.html' },
|
|
{ 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);
|
|
if (b.ziel) li.dataset.fertig = 'ja';
|
|
|
|
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.ziel ? 'öffnen' : (b.phase === 1 ? 'als Nächstes' : `Phase ${b.phase}`);
|
|
|
|
if (b.ziel) {
|
|
/* Fertige Bereiche sind echte Verweise -- Tastatur und
|
|
Rechtsklick funktionieren damit wie gewohnt. */
|
|
const a = document.createElement('a');
|
|
a.className = 'bereich__link';
|
|
a.href = b.ziel;
|
|
a.append(name, stand);
|
|
li.append(a);
|
|
} else {
|
|
li.append(name, stand);
|
|
}
|
|
liste.append(li);
|
|
}
|
|
|
|
await zahlenLaden();
|
|
}
|
|
|
|
/* Die Zahlen kommen aus derselben Quelle wie das Brett und sind bereits
|
|
serverseitig auf die eigene Rolle eingegrenzt. */
|
|
const ZAHLEN = [
|
|
{ schluessel: 'ueberfaellig', name: 'Überfällig', warn: true },
|
|
{ schluessel: 'heute', name: 'Heute fällig' },
|
|
{ schluessel: 'offen', name: 'Offen' },
|
|
{ schluessel: 'arbeit', name: 'In Arbeit' },
|
|
{ schluessel: 'review', name: 'Review' },
|
|
{ schluessel: 'erledigt', name: 'Erledigt' },
|
|
];
|
|
|
|
async function zahlenLaden() {
|
|
let daten;
|
|
try {
|
|
const antwort = await fetch('/workspace/api/uebersicht', { credentials: 'same-origin' });
|
|
if (!antwort.ok) return;
|
|
daten = await antwort.json();
|
|
} catch { return; }
|
|
|
|
const liste = $('zahlen');
|
|
liste.textContent = '';
|
|
for (const z of ZAHLEN) {
|
|
const wert = daten[z.schluessel] ?? 0;
|
|
const li = document.createElement('li');
|
|
li.className = 'zahl';
|
|
if (z.warn && wert > 0) li.dataset.warn = 'ja';
|
|
|
|
const n = document.createElement('span');
|
|
n.className = 'zahl__wert';
|
|
n.textContent = String(wert);
|
|
|
|
const t = document.createElement('span');
|
|
t.className = 'zahl__name';
|
|
t.textContent = z.name;
|
|
|
|
li.append(n, t);
|
|
liste.append(li);
|
|
}
|
|
}
|
|
|
|
|
|
laden();
|
|
})();
|