"use server"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { kurzzeitTokenErzeugen } from "@/lib/kurzzeit-token"; import { totpGeheimnisErzeugen } from "@/lib/totp"; import { z } from "@/lib/validierung"; import { sicheresZiel, sitzungSetzen, ueberHttps } from "@/server/auth/anmeldung"; import { angemeldet } from "@/server/auth/sitzungspflicht"; import { EINRICHTUNG_SEKUNDEN, EINRICHTUNG_ZWECK, TOTP_EINRICHTUNG_COOKIE, totpEinrichtungLesen, } from "@/server/auth/totp-einrichtung"; import { AnmeldeFehler, notfallAnmelden, notfallBestaetigen, totpEinrichtungAbschliessen, } from "@/server/services/anmeldung"; /** * Formular-Aktionen rund um die Anmeldung (#004). Duenn: Eingaben pruefen, Service rufen, * Cookie setzen, weiterleiten. Fehler kommen als Text zurueck, nie als Ausnahme – sonst * zeigte Next eine Fehlerseite statt der Meldung im Formular. */ export type FormularZustand = { readonly fehler?: string }; const notfallSchema = z.object({ anmeldename: z.string().trim().min(1).max(64), passwort: z.string().min(1).max(256), code: z.string().trim().max(10).default(""), ziel: z.string().max(512).optional(), }); export async function notfallAnmeldenAktion( _vorher: FormularZustand, formular: FormData, ): Promise { const eingabe = notfallSchema.safeParse(Object.fromEntries(formular)); if (!eingabe.success) return { fehler: "Bitte Anmeldename und Passwort angeben." }; const ziel = sicheresZiel(eingabe.data.ziel); let ergebnis; try { ergebnis = await notfallAnmelden(eingabe.data.anmeldename, eingabe.data.passwort, eingabe.data.code); } catch (fehler) { if (fehler instanceof AnmeldeFehler) return { fehler: fehler.message }; throw fehler; } if (ergebnis.art === "totpEinrichten") { // Das neue Geheimnis lebt bis zur Bestaetigung nur in einem signierten, kurzlebigen // httpOnly-Cookie dieses Browsers – gespeichert wird es erst, wenn ein Code passt. const token = await kurzzeitTokenErzeugen( EINRICHTUNG_ZWECK, { benutzerId: ergebnis.benutzer.id, geheimnis: totpGeheimnisErzeugen(), ziel }, EINRICHTUNG_SEKUNDEN, ); (await cookies()).set(TOTP_EINRICHTUNG_COOKIE, token, { httpOnly: true, sameSite: "strict", secure: ueberHttps(), path: "/anmelden", maxAge: EINRICHTUNG_SEKUNDEN, }); redirect("/anmelden/totp"); } await sitzungSetzen(ergebnis.benutzer, "notfall"); redirect(ziel); } export async function totpEinrichtenAktion( _vorher: FormularZustand, formular: FormData, ): Promise { const einrichtung = await totpEinrichtungLesen(); if (!einrichtung) return { fehler: "Die Einrichtung ist abgelaufen. Bitte erneut anmelden." }; const code = String(formular.get("code") ?? ""); let benutzer; try { benutzer = await totpEinrichtungAbschliessen(einrichtung.benutzerId, einrichtung.geheimnis, code); } catch (fehler) { if (fehler instanceof AnmeldeFehler) return { fehler: fehler.message }; throw fehler; } const laden = await cookies(); laden.delete({ name: TOTP_EINRICHTUNG_COOKIE, path: "/anmelden" }); await sitzungSetzen(benutzer, "notfall"); redirect(sicheresZiel(einrichtung.ziel)); } const bestaetigenSchema = z.object({ passwort: z.string().min(1).max(256), code: z.string().trim().min(6).max(10), ziel: z.string().max(512).optional(), }); export async function notfallBestaetigenAktion( _vorher: FormularZustand, formular: FormData, ): Promise { const vorhanden = await angemeldet(); if (!vorhanden) redirect("/anmelden"); const eingabe = bestaetigenSchema.safeParse(Object.fromEntries(formular)); if (!eingabe.success) return { fehler: "Bitte Passwort und Code angeben." }; try { await notfallBestaetigen(vorhanden.benutzer, eingabe.data.passwort, eingabe.data.code); } catch (fehler) { if (fehler instanceof AnmeldeFehler) return { fehler: fehler.message }; throw fehler; } redirect(sicheresZiel(eingabe.data.ziel)); }