'use server' import { AuthError } from 'next-auth' import { headers } from 'next/headers' import { z } from 'zod' import { signIn } from '@/auth' import { anmeldungErlaubt } from '@/lib/auth/ratelimit' import { anschlussAus } from '@/lib/auth/anschluss' /** * Server Action fuer das Anmeldeformular. * * Die Fehlermeldung ist absichtlich unspezifisch: Ob die E-Mail unbekannt * ist oder das Passwort falsch, geht niemanden etwas an, der davorsitzt. * Nur die Sperre wird konkret benannt - sonst probiert man ratlos weiter. */ const AnmeldeSchema = z.object({ email: z.string().trim().toLowerCase().email('Bitte eine gültige E-Mail-Adresse eingeben.'), passwort: z.string().min(1, 'Bitte das Passwort eingeben.'), code: z.string().trim().default(''), weiter: z.string().default('/heute'), }) export type AnmeldeZustand = { fehler?: string felder?: { email?: string; passwort?: string } /** Vorbelegung, damit die E-Mail nach einem Fehlversuch stehen bleibt. */ email?: string } /** * Erkennt die Weiterleitung, die Next.js intern als Ausnahme wirft. * Bewusst ueber das digest-Feld statt ueber einen Import aus next/dist - * interne Pfade aendern sich zwischen Nebenversionen. */ function istWeiterleitung(fehler: unknown): boolean { return ( typeof fehler === 'object' && fehler !== null && 'digest' in fehler && typeof (fehler as { digest?: unknown }).digest === 'string' && (fehler as { digest: string }).digest.startsWith('NEXT_REDIRECT') ) } /** Siehe src/lib/auth/anschluss.ts - der Kopf wird nur mit Proxy geglaubt. */ async function ipDerAnfrage(): Promise { try { return anschlussAus(await headers()) } catch { return null } } /** Verhindert offene Weiterleitungen auf fremde Seiten. */ function sicheresZiel(ziel: string): string { if (!ziel.startsWith('/') || ziel.startsWith('//')) return '/heute' return ziel } function fehlertext(code: string): string { const [grund, wert] = code.split(':') switch (grund) { case 'gesperrt': { const sekunden = Number.parseInt(wert ?? '0', 10) const minuten = Math.ceil(sekunden / 60) return ( `Zu viele Fehlversuche. Aus Sicherheitsgründen ist die Anmeldung für ` + `${minuten > 1 ? `etwa ${minuten} Minuten` : 'kurze Zeit'} gesperrt.` ) } case 'konto-inaktiv': return 'Dieser Zugang ist deaktiviert. Bitte wende dich an den Administrator.' case 'code-falsch': return ( 'Der Code stimmt nicht. Prüfe, ob die Uhrzeit deines Geräts stimmt — ' + 'TOTP-Codes hängen an der Uhr. Alternativ kannst du einen Wiederherstellungscode eingeben.' ) default: return 'E-Mail-Adresse, Passwort oder Code stimmen nicht.' } } export async function anmelden( _vorher: AnmeldeZustand, formular: FormData, ): Promise { const gelesen = AnmeldeSchema.safeParse({ email: formular.get('email'), passwort: formular.get('passwort'), code: formular.get('code'), weiter: formular.get('weiter') ?? '/heute', }) if (!gelesen.success) { const felder = gelesen.error.flatten().fieldErrors return { fehler: 'Bitte prüfe deine Eingaben.', felder: { email: felder.email?.[0], passwort: felder.passwort?.[0] }, email: String(formular.get('email') ?? ''), } } const { email, passwort, code, weiter } = gelesen.data // Die Sperre wird hier geprueft und nicht erst in authorize(). // // Grund: Auth.js reicht den Fehlercode aus authorize() nicht // unveraendert an die Server Action zurueck - man saehe auch bei // gesperrtem Zugang nur die allgemeine Meldung und wuerde ratlos // weiterprobieren. Die Pruefung in authorize() bleibt trotzdem // bestehen: sie ist die eigentliche Sperre, diese hier ist die // Erklaerung dafuer. const sperre = await anmeldungErlaubt(email, await ipDerAnfrage()) if (sperre.gesperrt) { return { fehler: fehlertext(`gesperrt:${sperre.nochSekunden}`), email } } try { await signIn('zugangsdaten', { email, passwort, code, redirectTo: sicheresZiel(weiter), }) } catch (fehler) { // Bei Erfolg wirft signIn eine Weiterleitung - die muss durch. if (istWeiterleitung(fehler)) throw fehler if (fehler instanceof AuthError) { const code = 'code' in fehler && typeof fehler.code === 'string' ? fehler.code : 'daten-falsch' return { fehler: fehlertext(code), email } } throw fehler } // Wird nicht erreicht - signIn leitet weiter. return {} }