feat: Erinnerungen, Wartungslauf und Playwright-Tests der Anmeldung
Etappe 8, erster Teil. Erinnerungsmails sind bewusst eine Zugabe: Ist kein SMTP konfiguriert, wird nichts gesendet und nichts geworfen. Die Fristen stehen ohnehin im Dashboard und im abonnierten Kalender - eine ausgefallene Mail darf nicht dazu fuehren, dass der Wartungslauf abbricht und danach auch das Aufraeumen liegen bleibt. In der Mail steht, DASS etwas faellig ist und wo man nachsieht - nicht, worum es geht. Kein Name der betreuten Person, keine Inhalte. E-Mail ist unterwegs unverschluesselt und landet auf fremden Servern. Je Frist geht hoechstens eine Mail pro Vorwarnstufe raus. Taeglich waere nach einer Woche Rauschen, das man wegklickt - und genau dann verpasst man die eine, auf die es ankam. Der Wartungslauf faengt jeden Schritt einzeln ab: Papierkorb leeren, Anmeldeversuche und altes Audit-Log aufraeumen laufen auch dann, wenn der SMTP-Server nicht antwortet. Angestossen wird er ueber /api/wartung mit einem HMAC aus AUTH_SECRET - ohne gueltigen Schluessel antwortet die Route mit 404 statt 401. Ein echter Fund aus den Tests: Auth.js reicht den Fehlercode aus authorize() nicht an die Server Action zurueck. Bei gesperrtem Zugang sah man deshalb nur die allgemeine Meldung "stimmen nicht" und haette ratlos weiterprobiert, obwohl die Sperre bereits lief. Die Sperre wird jetzt zusaetzlich in der Server Action geprueft und benannt; die Pruefung in authorize() bleibt als eigentliche Sperre bestehen. Sechs Playwright-Tests der Anmeldung, darunter zwei, die Sicherheitszusagen nachweisen: dass zwei Fehlversuche mit unterschiedlicher Ursache dieselbe Meldung liefern (sonst liesse sich ablesen, welche Adressen existieren), und dass die Anmeldeseite keine einzige Anfrage an einen fremden Host stellt. Etappe: 8 Phase: Haertung
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
'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'
|
||||
|
||||
/**
|
||||
* Server Action fuer das Anmeldeformular.
|
||||
@@ -41,6 +43,16 @@ function istWeiterleitung(fehler: unknown): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
/** Hinter dem Reverse Proxy steht die echte Adresse im weitergereichten Kopf. */
|
||||
async function ipDerAnfrage(): Promise<string | null> {
|
||||
try {
|
||||
const kopf = await headers()
|
||||
return kopf.get('x-forwarded-for')?.split(',')[0]?.trim() || kopf.get('x-real-ip') || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Verhindert offene Weiterleitungen auf fremde Seiten. */
|
||||
function sicheresZiel(ziel: string): string {
|
||||
if (!ziel.startsWith('/') || ziel.startsWith('//')) return '/heute'
|
||||
@@ -93,6 +105,19 @@ export async function anmelden(
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user