Files
ProxLedger/scripts/benutzer.ts
T
florian.wildeandClaude Opus 5 7775b6211f feat(security): Haertung nach OWASP Top 10 (#024)
Drei echte Luecken gefunden und geschlossen:

1. Die 27 Server Actions verliessen sich allein auf proxy.ts. Jede prueft die
   Sitzung jetzt selbst und schlaegt gegen die Datenbank nach, ob das Konto
   noch gilt - damit lassen sich Sitzungen ueberhaupt erst widerrufen.
2. Der Anmelde-Endpunkt prueft die Herkunft des Formulars; vorher war
   Login-CSRF moeglich.
3. Content-Security-Policy mit Nonce je Anfrage und strict-dynamic.

Dazu: npm audit ist ohne Befund (mysql2 und deepmerge-ts per overrides
angehoben statt Prisma zurueckzustufen), eine Benutzerverwaltung fuer die
Kommandozeile und docs/sicherheit.md mit allem, was bewusst offen bleibt -
keine Rollen, keine Zwei-Faktor-Anmeldung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 23:45:35 +02:00

82 lines
2.8 KiB
TypeScript
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.
import "dotenv/config";
import { createInterface } from "node:readline/promises";
import { hash } from "@node-rs/argon2";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@/generated/prisma/client";
/**
* Benutzerverwaltung von der Kommandozeile – solange es dafür keine Oberfläche gibt.
*
* npm run benutzer -- liste
* npm run benutzer -- sperren <email> Konto sperren und alle Sitzungen sofort beenden
* npm run benutzer -- entsperren <email>
* npm run benutzer -- passwort <email> neues Passwort setzen, beendet alle Sitzungen
*/
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
async function passwortAbfragen(): Promise<string> {
const eingabe = createInterface({ input: process.stdin, output: process.stdout });
const passwort = await eingabe.question("Neues Passwort (mindestens 12 Zeichen): ");
eingabe.close();
if (passwort.length < 12) throw new Error("Zu kurz – mindestens 12 Zeichen.");
return passwort;
}
async function main() {
const [befehl, email] = process.argv.slice(2);
if (befehl === "liste") {
const alle = await prisma.benutzer.findMany({ orderBy: { email: "asc" } });
for (const b of alle) {
console.log(`${b.aktiv ? "aktiv " : "gesperrt"} ${b.email.padEnd(32)} ${b.name}`);
}
return;
}
if (!email) throw new Error("Bitte eine E-Mail-Adresse angeben.");
const benutzer = await prisma.benutzer.findUnique({ where: { email: email.toLowerCase().trim() } });
if (!benutzer) throw new Error(`Kein Benutzer mit der Adresse ${email}.`);
switch (befehl) {
case "sperren":
// Sperren allein genügt nicht: Das Sitzungs-Token gilt sonst bis zum Ablauf weiter.
await prisma.benutzer.update({
where: { id: benutzer.id },
data: { aktiv: false, sitzungenUngueltigAb: new Date() },
});
console.log(`${benutzer.email} ist gesperrt, alle Sitzungen sind beendet.`);
break;
case "entsperren":
await prisma.benutzer.update({ where: { id: benutzer.id }, data: { aktiv: true } });
console.log(`${benutzer.email} ist wieder freigegeben.`);
break;
case "passwort": {
const passwort = await passwortAbfragen();
await prisma.benutzer.update({
where: { id: benutzer.id },
data: {
passwortHash: await hash(passwort),
sitzungenUngueltigAb: new Date(),
},
});
console.log(`Passwort gesetzt; bestehende Sitzungen von ${benutzer.email} sind beendet.`);
break;
}
default:
throw new Error("Bekannte Befehle: liste, sperren, entsperren, passwort");
}
}
main()
.catch((fehler: unknown) => {
console.error(fehler instanceof Error ? fehler.message : fehler);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());