Rollen (Admin/Mitglied), Seite Kooperationen, Textanpassungen

- Gemeinsame Zugänge mit Rollen: Admin darf alles, Mitglied nutzt Kalender,
  Fahrtplaner, Treffpunkte und bearbeitet Mitglieder (kein Hinzufügen,
  Löschen, Import, keine Patches, keine Statistik-Korrektur)
- Server prüft die Rechte (403), Konten ohne Rolle gelten als Admin
- npm run user: add <name> [admin|mitglied], role <name> <rolle>
- Neue öffentliche Seite Kooperationen mit Titelbild, Anführer,
  Social-Media-Links und Beschreibung, Pflege durch Admin
- Kopfzeile bleibt bei mittlerer Breite einzeilig
- Texte auf Wunsch gekürzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-12 19:47:03 +02:00
1 parent f46016487d
commit 819219fb2d
18 files changed
+661 -106

No files matched your search

+23 -6
View File
@@ -2,6 +2,10 @@
//
// Passwörter werden nie im Klartext gespeichert, sondern mit scrypt (eigenes Salt pro Passwort) gehasht.
// Sitzungs-Tokens liegen nur im Cookie des Browsers; auf dem Server wird lediglich ihr SHA-256-Hash gespeichert.
//
// Rollen: Die Konten sind gemeinsame Zugänge, keine persönlichen.
// admin – alles, inkl. Mitglieder hinzufügen/löschen, Gruppe importieren, Patches, Statistik-Korrektur
// mitglied – Kalender, Fahrtplaner, Treffpunkte, Mitglieder bearbeiten (nicht hinzufügen/löschen)
import { scrypt, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, statSync } from 'node:fs';
@@ -17,6 +21,10 @@ const SESSION_DAYS = 30;
const MAX_FAILS = 10; // Fehlversuche pro IP …
const FAIL_WINDOW = 15 * 60 * 1000; // … innerhalb von 15 Minuten
export const MIN_PASSWORD = 8;
export const ROLES = ['admin', 'mitglied'];
// Konten aus der Zeit vor den Rollen wurden vom Betreiber angelegt → Admin, damit sich niemand aussperrt
export const roleOf = (user) => (ROLES.includes(user?.role) ? user.role : 'admin');
// ---------- Passwort-Hashing (scrypt) ----------
const SCRYPT = { N: 2 ** 15, r: 8, p: 1, keylen: 64 };
@@ -105,7 +113,7 @@ function setCookie(req, res, value, maxAgeSec) {
res.setHeader('Set-Cookie', attrs.join('; '));
}
// Liefert den angemeldeten Benutzernamen oder null
// Liefert den angemeldeten Benutzer ({ name, role }) oder null
function sessionUser(req) {
const token = readCookie(req, COOKIE);
if (!token) return null;
@@ -114,7 +122,7 @@ function sessionUser(req) {
// Benutzer gelöscht oder Passwort seit der Anmeldung geändert → Sitzung ungültig
const user = currentUsers().find((u) => u.name === s.user);
if (!user || s.created < (user.changedAt || 0)) return null;
return user.name;
return { name: user.name, role: roleOf(user) };
}
// ---------- Schutz vor Passwort-Raten ----------
@@ -156,7 +164,7 @@ export function authRoutes(app) {
});
saveSessions();
setCookie(req, res, token, SESSION_DAYS * 86400);
res.json({ user: user.name });
res.json({ user: user.name, role: roleOf(user) });
} catch (err) {
next(err);
}
@@ -170,15 +178,24 @@ export function authRoutes(app) {
});
app.get('/api/me', (req, res) => {
res.json({ user: sessionUser(req) });
const user = sessionUser(req);
res.json({ user: user?.name ?? null, role: user?.role ?? null });
});
}
// Alles, was nach dieser Middleware kommt, ist nur angemeldet erreichbar
export function requireAuth(req, res, next) {
req.user = sessionUser(req);
if (req.user) return next();
const user = sessionUser(req);
req.user = user?.name ?? null;
req.role = user?.role ?? null;
if (user) return next();
if (req.path.startsWith('/api/')) return res.status(401).json({ error: 'Bitte anmelden.' });
if (req.method === 'GET') return res.redirect(`/login?next=${encodeURIComponent(req.originalUrl)}`);
res.status(401).end();
}
// Nur für Admins (nach requireAuth verwenden)
export function requireAdmin(req, res, next) {
if (req.role === 'admin') return next();
res.status(403).json({ error: 'Das darf nur der Admin-Zugang.' });
}