/** * In-Memory Rate-Limiting für Brute-Force-Schutz * Speichert Versuche pro Identifier (IP:Email Kombination) */ interface RateLimitEntry { count: number; lastAttempt: number; locked: boolean; lockedUntil?: number; } const attempts = new Map(); // Cleanup: Alte Einträge alle 10 Minuten löschen (Memory-Leak vermeiden) const CLEANUP_INTERVAL = 10 * 60 * 1000; const RESET_WINDOW = 15 * 60 * 1000; // 15 Minuten Fenster const MAX_ATTEMPTS = 5; // Max 5 Versuche pro Fenster const LOCK_THRESHOLD = 10; // Account sperren nach 10 Versuchen const LOCK_DURATION = 15 * 60 * 1000; // 15 Minuten Sperrung setInterval(() => { const now = Date.now(); for (const [key, entry] of attempts.entries()) { if (now - entry.lastAttempt > RESET_WINDOW) { attempts.delete(key); } } }, CLEANUP_INTERVAL); /** * Prüfe ob Request innerhalb von Rate-Limit liegt * @param identifier - Eindeutige ID (z.B. "login:admin@example.com:192.168.1.1") * @returns { allowed, delayMs, locked } */ export function checkRateLimit(identifier: string) { const now = Date.now(); let entry = attempts.get(identifier); // Neuer Eintrag if (!entry) { attempts.set(identifier, { count: 1, lastAttempt: now, locked: false, }); return { allowed: true, delayMs: 0, locked: false }; } // Fenster abgelaufen? → Zurücksetzen if (now - entry.lastAttempt > RESET_WINDOW) { attempts.set(identifier, { count: 1, lastAttempt: now, locked: false, }); return { allowed: true, delayMs: 0, locked: false }; } // Account gesperrt? if (entry.locked && entry.lockedUntil) { if (now < entry.lockedUntil) { const remainingMs = entry.lockedUntil - now; return { allowed: false, delayMs: remainingMs, locked: true }; } else { // Sperrung abgelaufen entry.locked = false; entry.count = 1; entry.lastAttempt = now; return { allowed: true, delayMs: 0, locked: false }; } } // Versuch inkrementieren entry.count++; entry.lastAttempt = now; // Exponential Backoff: 1s, 2s, 4s, 8s, 16s, 30s max const delayMs = Math.min(Math.pow(2, entry.count - 2) * 1000, 30000); // Nach 10 Versuchen sperren if (entry.count >= LOCK_THRESHOLD) { entry.locked = true; entry.lockedUntil = now + LOCK_DURATION; return { allowed: false, delayMs: LOCK_DURATION, locked: true }; } // Vor dem 5. Versuch noch erlaubt, danach mit Delay const allowed = entry.count <= MAX_ATTEMPTS; return { allowed, delayMs: allowed ? 0 : delayMs, locked: false }; } /** * Manuelles Zurücksetzen (z.B. nach erfolgreichem Login) */ export function resetRateLimit(identifier: string) { attempts.delete(identifier); } /** * Hole Versuchszahl für einen Identifier (für Monitoring) */ export function getAttemptCount(identifier: string): number { const entry = attempts.get(identifier); return entry?.count ?? 0; }