'use server' import { redirect } from 'next/navigation' import { z } from 'zod' import { db } from '@/lib/db' import { erfordereBenutzer, reauthBestaetigen } from '@/lib/auth/sitzung' import { passwortPruefen } from '@/lib/crypto/hash' import { zweitenFaktorEinloesen } from '@/lib/auth/zweiter-faktor' import { aufMindestdauerWarten } from '@/lib/auth/ratelimit' /** * Erneute Bestaetigung vor sensiblen Aktionen. * * Betroffen sind: Vollexport, endgueltiges Loeschen, Benutzerverwaltung, * Aendern von Rollen und Freigaben, neue Wiederherstellungscodes. * * Warum ueberhaupt? Eine offene Sitzung an einem unbeaufsichtigten Rechner * ist der wahrscheinlichste Angriffsweg in diesem Umfeld - nicht der * Einbruch in den Server. Wer alle Gesundheits- und Finanzdaten in einer * ZIP-Datei mitnehmen will, soll dafuer Passwort und Code erneut eingeben. */ const BestaetigungSchema = z.object({ passwort: z.string().min(1, 'Bitte dein Passwort eingeben.'), code: z.string().trim(), weiter: z.string().default('/heute'), }) export type BestaetigenZustand = { fehler?: string } function sicheresZiel(ziel: string): string { if (!ziel.startsWith('/') || ziel.startsWith('//')) return '/heute' return ziel } export async function anmeldungBestaetigen( _vorher: BestaetigenZustand, formular: FormData, ): Promise { const begonnen = Date.now() const benutzer = await erfordereBenutzer() const gelesen = BestaetigungSchema.safeParse({ passwort: formular.get('passwort'), code: formular.get('code'), weiter: formular.get('weiter') ?? '/heute', }) if (!gelesen.success) { await aufMindestdauerWarten(begonnen) return { fehler: 'Bitte Passwort und Code eingeben.' } } const frisch = await db.user.findUnique({ where: { id: benutzer.id } }) if (!frisch) return { fehler: 'Der Zugang wurde zwischenzeitlich entfernt.' } const passwortStimmt = await passwortPruefen(frisch.passwortHash, gelesen.data.passwort) // Der Code wird nur eingeloest, wenn das Passwort stimmt. Sonst // verbrauchte ein falsches Passwort mit richtigem Code das Zeitfenster, // und der Benutzer muesste dreissig Sekunden auf den naechsten warten - // ohne zu verstehen, warum. let codeStimmt = true if (passwortStimmt && frisch.totpSecretVerschluesselt && frisch.totpAktiviertAm) { const eingeloest = await zweitenFaktorEinloesen(frisch, gelesen.data.code) codeStimmt = eingeloest.ok } if (!passwortStimmt || !codeStimmt) { await aufMindestdauerWarten(begonnen) return { fehler: 'Passwort oder Code stimmen nicht.' } } await reauthBestaetigen(benutzer.id) await aufMindestdauerWarten(begonnen) redirect(sicheresZiel(gelesen.data.weiter)) }