import "server-only"; /** * Grobe Bremse gegen automatisiertes Durchprobieren. * * Ergänzt die Kontosperre, ersetzt sie aber nicht: Die Sperre schützt ein * einzelnes Konto, diese Bremse den Server vor jemandem, der viele Konten * gleichzeitig durchprobiert — dabei löst die Sperre nämlich nie aus. * * Im Arbeitsspeicher und damit pro Container. Bei einer selbst gehosteten * Anwendung dieser Größe ist das angemessen; ein verteilter Zähler bräuchte * einen weiteren Dienst. Die Adresse dient nur als Schlüssel und wird * nirgends gespeichert. */ type Eintrag = { anzahl: number; bis: number }; const zaehler = new Map(); /** Ab wie vielen Versuchen je Zeitfenster abgeblockt wird. */ export const LOGIN_ATTEMPTS_PER_WINDOW = 20; const FENSTER_MS = 5 * 60 * 1000; /** * Zählt einen Versuch und meldet, ob das Fenster überschritten ist. * Gibt die verbleibenden Sekunden zurück, sonst `null`. */ export function tooManyAttempts(schluessel: string): number | null { const jetzt = Date.now(); const eintrag = zaehler.get(schluessel); if (!eintrag || eintrag.bis < jetzt) { zaehler.set(schluessel, { anzahl: 1, bis: jetzt + FENSTER_MS }); // Gelegentlich ausmisten, damit die Tabelle nicht unbegrenzt wächst. if (zaehler.size > 5000) { for (const [key, wert] of zaehler) { if (wert.bis < jetzt) zaehler.delete(key); } } return null; } eintrag.anzahl += 1; if (eintrag.anzahl > LOGIN_ATTEMPTS_PER_WINDOW) { return Math.ceil((eintrag.bis - jetzt) / 1000); } return null; } /** Nach erfolgreicher Anmeldung ist der Zähler gegenstandslos. */ export function clearAttempts(schluessel: string): void { zaehler.delete(schluessel); } /** * Absender einer Anfrage, so gut es hinter einem Proxy geht. * * `X-Forwarded-For` ist fälschbar, wenn kein Proxy davorsteht — deshalb ist * das hier eine Bremse und keine Sperre: Wer den Wert fälscht, umgeht sie, * aber die Kontosperre greift weiterhin. */ export function requestOrigin(headers: Headers): string { return ( headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "unbekannt" ); }