import { Response } from 'express'; import prisma from '../lib/prisma.js'; import { AuthRequest, ApiResponse } from '../types/index.js'; import { loginRateLimiter, passwordResetRateLimiter } from '../middleware/rateLimit.js'; import { logChange } from '../services/audit.service.js'; const LOGIN_WINDOW_MS = 15 * 60 * 1000; type ActiveLock = { ipAddress: string; email: string | null; // null = Passwort-Reset oder Login ohne Email lastHit: Date; hitCount: number; lastEndpoint: string | null; limiters: string[]; // 'login' / 'password-reset' }; function lockKey(ip: string, email: string | null): string { return `${ip}|${(email || '').toLowerCase()}`; } /** * Listet aktive Sperren als (IP, Email)-Tupel. Jedes Tupel ist ein eigener * Bucket im Limiter – Reset gilt exakt für dieses Paar. */ export async function getActiveRateLimits(req: AuthRequest, res: Response): Promise { try { const since = new Date(Date.now() - LOGIN_WINDOW_MS); const events = await prisma.securityEvent.findMany({ where: { type: 'RATE_LIMIT_HIT', createdAt: { gte: since } }, orderBy: { createdAt: 'desc' }, select: { ipAddress: true, userEmail: true, endpoint: true, createdAt: true, details: true, }, }); const byKey = new Map(); for (const ev of events) { const ip = ev.ipAddress || 'unknown'; const email = (ev.userEmail || '').toLowerCase() || null; const limiter = (ev.details as any)?.limiter ?? 'unknown'; const key = lockKey(ip, email); const existing = byKey.get(key); if (existing) { existing.hitCount += 1; if (!existing.limiters.includes(limiter)) existing.limiters.push(limiter); } else { byKey.set(key, { ipAddress: ip, email, lastHit: ev.createdAt, hitCount: 1, lastEndpoint: ev.endpoint, limiters: [limiter], }); } } // Bereits manuell freigegebene aus der Liste werfen. Reset-Audit-Logs // nutzen resourceId = "|" (gleicher Schlüssel wie Bucket). const candidates = Array.from(byKey.entries()).map(([k, e]) => ({ mapKey: k, resourceId: k, lastHit: e.lastHit, })); if (candidates.length > 0) { const recentResets = await prisma.auditLog.findMany({ where: { resourceType: 'RateLimit', resourceId: { in: candidates.map((c) => c.resourceId) }, createdAt: { gte: since }, }, select: { resourceId: true, createdAt: true }, orderBy: { createdAt: 'desc' }, }); const resetMap = new Map(); for (const r of recentResets) { if (r.resourceId && !resetMap.has(r.resourceId)) resetMap.set(r.resourceId, r.createdAt); } for (const c of candidates) { const reset = resetMap.get(c.resourceId); if (reset && reset >= c.lastHit) byKey.delete(c.mapKey); } } const list = Array.from(byKey.values()).sort( (a, b) => b.lastHit.getTime() - a.lastHit.getTime(), ); res.json({ success: true, data: list } as ApiResponse); } catch (error) { console.error('getActiveRateLimits error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der aktiven Rate-Limits', } as ApiResponse); } } /** * Reset für ein konkretes (IP, Email)-Tupel. Body MUSS ipAddress enthalten * + optional email. Bei fehlender Email wird `|` reseted * (für Login-Versuche mit leerem Body). Für Passwort-Reset-Limit wird der * IP-only-Key (alter Stil) zusätzlich reseted. */ export async function resetRateLimit(req: AuthRequest, res: Response): Promise { try { const ip = (req.body?.ipAddress || '').toString().trim(); const email = (req.body?.email || '').toString().trim().toLowerCase(); if (!ip) { res.status(400).json({ success: false, error: 'IP-Adresse erforderlich', } as ApiResponse); return; } // Login-Tuple-Bucket: `${ip}|${email}` bzw. `${ip}|` const loginKey = email ? `${ip}|${email}` : `${ip}|`; await (loginRateLimiter as any).resetKey?.(loginKey); // Passwort-Reset-Limit ist (noch) IP-only – auch zurücksetzen await (passwordResetRateLimiter as any).resetKey?.(ip); // Audit-Resource-ID = der Bucket-Key, damit getActiveRateLimits den // Eintrag aus der Anzeige filtern kann. const audited = `${ip}|${email || ''}`; await logChange({ req, action: 'UPDATE', resourceType: 'RateLimit', resourceId: audited, label: email ? `Rate-Limit für (IP ${ip}, Email ${email}) manuell freigegeben` : `Rate-Limit für IP ${ip} manuell freigegeben`, }); res.json({ success: true, message: email ? `Rate-Limit für (${ip}, ${email}) freigegeben` : `Rate-Limit für ${ip} freigegeben`, } as ApiResponse); } catch (error) { console.error('resetRateLimit error:', error); res.status(500).json({ success: false, error: 'Fehler beim Zurücksetzen des Rate-Limits', } as ApiResponse); } }