Files
guyg1474andClaude Opus 5.5 819219fb2d 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>
2026-10-12 19:47:03 +02:00

111 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Benutzerverwaltung für den Login
//
// Die Konten sind gemeinsame Zugänge (z. B. "admin" und "hcrb" für alle Mitglieder), keine persönlichen.
//
// npm run user -- list alle Benutzer mit Rolle
// npm run user -- add <name> [admin|mitglied] Benutzer anlegen (Standard: mitglied, Passwort wird abgefragt)
// npm run user -- role <name> <admin|mitglied> Rolle ändern (meldet alle Sitzungen des Benutzers ab)
// npm run user -- passwd <name> Passwort ändern (meldet alle Sitzungen des Benutzers ab)
// npm run user -- remove <name> Benutzer löschen
import { createInterface } from 'node:readline';
import { hashPassword, readUsers, writeUsers, normalizeName, MIN_PASSWORD, USERS_FILE, ROLES, roleOf } from '../auth.js';
// Passwort verdeckt einlesen (bei umgeleiteter Eingabe zeilenweise)
let pipedLines;
function askPassword(prompt) {
return new Promise((resolve, reject) => {
const { stdin, stdout } = process;
stdout.write(prompt);
if (!stdin.isTTY) {
pipedLines ??= createInterface({ input: stdin })[Symbol.asyncIterator]();
pipedLines.next().then(({ value }) => { stdout.write('\n'); resolve(value ?? ''); }, reject);
return;
}
let input = '';
stdin.setRawMode(true);
stdin.setEncoding('utf8');
stdin.resume();
const onData = (chunk) => {
for (const ch of chunk) {
if (ch === '\r' || ch === '\n') {
stdin.setRawMode(false);
stdin.pause();
stdin.off('data', onData);
stdout.write('\n');
return resolve(input);
}
if (ch === '\u0003') { // Strg+C
stdin.setRawMode(false);
stdout.write('\n');
return reject(new Error('Abgebrochen.'));
}
if (ch === '\u007f' || ch === '\b') input = input.slice(0, -1);
else input += ch;
}
};
stdin.on('data', onData);
});
}
async function newPassword() {
const pw = await askPassword('Neues Passwort: ');
if (pw.length < MIN_PASSWORD) throw new Error(`Das Passwort muss mindestens ${MIN_PASSWORD} Zeichen haben.`);
if (await askPassword('Passwort wiederholen: ') !== pw) throw new Error('Die Passwörter stimmen nicht überein.');
return hashPassword(pw);
}
const [cmd, rawName, rawRole] = process.argv.slice(2);
const role = String(rawRole ?? '').trim().toLowerCase();
function checkRole(value) {
if (!ROLES.includes(value)) throw new Error(`Rolle muss ${ROLES.join(' oder ')} sein.`);
return value;
}
const name = normalizeName(rawName);
const users = readUsers();
const user = users.find((u) => u.name === name);
try {
if (cmd === 'list') {
console.log(users.length
? users.map((u) => `${u.name.padEnd(20)} ${roleOf(u)}${u.role ? '' : ' (noch ohne Rolle gespeichert)'}`).join('\n')
: 'Noch keine Benutzer angelegt.');
} else if (cmd === 'add' && name) {
if (!/^[a-z0-9._-]{2,32}$/.test(name)) {
throw new Error('Benutzername: 2–32 Zeichen, nur a–z, 0–9, Punkt, Minus, Unterstrich.');
}
if (user) throw new Error(`Benutzer „${name}“ gibt es schon.`);
const newRole = checkRole(role || 'mitglied');
users.push({ name, role: newRole, hash: await newPassword(), changedAt: Date.now() });
writeUsers(users);
console.log(`Benutzer „${name}“ als ${newRole} angelegt.`);
} else if (cmd === 'role' && name && role) {
if (!user) throw new Error(`Benutzer „${name}“ gibt es nicht.`);
user.role = checkRole(role);
if (user.role === 'mitglied' && !users.some((u) => roleOf(u) === 'admin')) {
throw new Error('Das ist der letzte Admin. Lege zuerst einen anderen Admin an.');
}
user.changedAt = Date.now();
writeUsers(users);
console.log(`„${name}“ ist jetzt ${user.role}. Bestehende Anmeldungen sind abgemeldet.`);
} else if (cmd === 'passwd' && name) {
if (!user) throw new Error(`Benutzer „${name}“ gibt es nicht.`);
user.hash = await newPassword();
user.changedAt = Date.now();
writeUsers(users);
console.log(`Passwort von „${name}“ geändert. Bestehende Anmeldungen sind abgemeldet.`);
} else if (cmd === 'remove' && name) {
if (!user) throw new Error(`Benutzer „${name}“ gibt es nicht.`);
writeUsers(users.filter((u) => u !== user));
console.log(`Benutzer „${name}“ gelöscht.`);
} else {
console.log('Benutzung: npm run user -- list | add <name> [admin|mitglied] | role <name> <admin|mitglied> | passwd <name> | remove <name>');
console.log(`Benutzerdatei: ${USERS_FILE}`);
process.exitCode = cmd ? 1 : 0;
}
} catch (err) {
console.error(err.message);
process.exitCode = 1;
}