'use server' import { z } from 'zod' import { db } from '@/lib/db' import { erfordereBenutzer } from '@/lib/auth/sitzung' import { passwortHashen, passwortPruefen } from '@/lib/crypto/hash' import { protokollieren } from '@/lib/audit' import { aufMindestdauerWarten } from '@/lib/auth/ratelimit' /** * Passwortwechsel. * * Danach wird sitzungsGeneration hochgezaehlt. Das macht alle bestehenden * Sitzungen ungueltig - auch die auf Geraeten, an denen man gerade nicht * sitzt. Genau darum aendert man ja das Passwort. * * Die eigene Sitzung faellt dabei mit; der Benutzer meldet sich neu an. * Das ist unbequem und trotzdem richtig: ein Wechsel, der offene Sitzungen * stehen laesst, schuetzt vor nichts. */ const PASSWORT_MINDESTLAENGE = 12 const PasswortSchema = z .object({ aktuell: z.string().min(1, 'Bitte das bisherige Passwort eingeben.'), neu: z .string() .min( PASSWORT_MINDESTLAENGE, `Bitte mindestens ${PASSWORT_MINDESTLAENGE} Zeichen. Vier zufällige Wörter sind ` + `leichter zu merken und sicherer als ein kurzes Kunstwort.`, ) .max(200), wiederholung: z.string(), }) .refine((daten) => daten.neu === daten.wiederholung, { message: 'Die beiden neuen Passwörter stimmen nicht überein.', path: ['wiederholung'], }) .refine((daten) => daten.neu !== daten.aktuell, { message: 'Das neue Passwort muss sich vom bisherigen unterscheiden.', path: ['neu'], }) export type PasswortZustand = { fehler?: string felder?: Partial> erfolg?: boolean } export async function passwortAendern( _vorher: PasswortZustand, formular: FormData, ): Promise { const begonnen = Date.now() const benutzer = await erfordereBenutzer() const gelesen = PasswortSchema.safeParse({ aktuell: formular.get('aktuell'), neu: formular.get('neu'), wiederholung: formular.get('wiederholung'), }) if (!gelesen.success) { const felder = gelesen.error.flatten().fieldErrors return { fehler: 'Bitte prüfe deine Eingaben.', felder: { aktuell: felder.aktuell?.[0], neu: felder.neu?.[0], wiederholung: felder.wiederholung?.[0], }, } } const stimmt = await passwortPruefen(benutzer.passwortHash, gelesen.data.aktuell) if (!stimmt) { await aufMindestdauerWarten(begonnen) return { fehler: 'Das bisherige Passwort stimmt nicht.', felder: { aktuell: ' ' } } } await db.user.update({ where: { id: benutzer.id }, data: { passwortHash: await passwortHashen(gelesen.data.neu), passwortGeaendertAm: new Date(), // Beendet alle bestehenden Sitzungen, die eigene eingeschlossen. sitzungsGeneration: { increment: 1 }, }, }) await protokollieren({ aktion: 'PASSWORT_GEAENDERT', entitaet: 'User', entitaetId: benutzer.id, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, }) await aufMindestdauerWarten(begonnen) return { erfolg: true } }