import type { Benutzer } from "@/generated/prisma/client"; import { prisma } from "@/lib/db"; import { logger } from "@/lib/logger"; import { attrappenPruefung, passwortHashen, passwortPruefen, PASSWORT_MINDESTLAENGE } from "@/lib/passwort"; import { AnwendungsFehler } from "@/server/fehler"; import { protokollieren } from "@/server/services/protokoll"; /** * Sperre gegen Durchprobieren: höchstens 10 Fehlversuche je Benutzername und Adresse in 15 min. * Bewusst im Speicher: ProxDeploy läuft als eine Instanz, und ein Neustart, der die Zähler * leert, ist für einen Angreifer kein Gewinn – er müsste den Dienst dafür neu starten können. */ const FENSTER_MS = 15 * 60 * 1000; const HOECHSTENS = 10; const fehlversuche = new Map(); function versucheZaehlen(schluessel: string, jetzt: number): number { const liste = (fehlversuche.get(schluessel) ?? []).filter((t) => jetzt - t < FENSTER_MS); fehlversuche.set(schluessel, liste); return liste.length; } /** Nur für Tests. */ export function sperrenZuruecksetzen(): void { fehlversuche.clear(); } export async function mitPasswortAnmelden( benutzername: string, passwort: string, ip: string | null, ): Promise { const name = benutzername.trim().toLowerCase(); const schluessel = `${name}|${ip ?? "-"}`; const jetzt = Date.now(); if (versucheZaehlen(schluessel, jetzt) >= HOECHSTENS) { throw new AnwendungsFehler( 429, "zu_viele_versuche", "Zu viele Fehlversuche.", "In 15 Minuten erneut versuchen.", ); } const benutzer = await prisma.benutzer.findUnique({ where: { benutzername: name } }); const passt = benutzer?.passwortHash && benutzer.aktiv ? await passwortPruefen(benutzer.passwortHash, passwort) : await attrappenPruefung(passwort); if (!benutzer || !passt) { fehlversuche.get(schluessel)?.push(jetzt); await protokollieren({ benutzerId: benutzer?.id ?? null, quelle: "WEB", aktion: "Anmeldung fehlgeschlagen", ziel: `benutzer:${name}`, ip, }); // Eine Meldung für alle Fälle – sie verrät nicht, ob es den Benutzer gibt. throw new AnwendungsFehler(401, "anmeldung_fehlgeschlagen", "Benutzername oder Passwort ist falsch."); } fehlversuche.delete(schluessel); await protokollieren({ benutzerId: benutzer.id, quelle: "WEB", aktion: "Angemeldet", ip }); return benutzer; } /** * Legt beim ersten Start den Administrator aus ADMIN_BENUTZER/ADMIN_PASSWORT an – nur solange die * Tabelle leer ist. Danach sind die Variablen wirkungslos, damit ein vergessener Eintrag in der * .env kein zweites Hintertürchen ist. */ export async function ersterAdminFallsLeer(): Promise { if ((await prisma.benutzer.count()) > 0) return; const benutzername = process.env["ADMIN_BENUTZER"]?.trim().toLowerCase() || "admin"; const passwort = process.env["ADMIN_PASSWORT"] ?? ""; if (passwort.length < PASSWORT_MINDESTLAENGE) { logger.warn( `Es gibt noch keinen Benutzer. ADMIN_PASSWORT setzen (mindestens ${PASSWORT_MINDESTLAENGE} Zeichen) und neu starten.`, ); return; } let benutzer: Benutzer; try { benutzer = await prisma.benutzer.create({ data: { benutzername, anzeigename: "Administrator", rolle: "ADMIN", passwortHash: await passwortHashen(passwort), }, }); } catch (fehler) { // Ein zweiter Prozess war schneller (app und worker starten gleichzeitig) – das ist in Ordnung. if ((fehler as { code?: string }).code === "P2002") return; throw fehler; } await protokollieren({ benutzerId: benutzer.id, quelle: "SYSTEM", aktion: "Erster Administrator angelegt", }); logger.info({ benutzername }, "Erster Administrator angelegt"); }