import { headers } from "next/headers"; interface Bucket { count: number; resetAt: number; } // In-Memory-Limiter: ausreichend fuer den Single-Container-Betrieb dieser // Seite (siehe docker-compose). Bei horizontaler Skalierung muss der Zustand // in einen gemeinsamen Store (z.B. Redis) wandern. const buckets = new Map(); const MAX_BUCKETS = 10_000; /** * Sliding-Window-Zaehler: erlaubt `limit` Aufrufe pro `windowMs` je `key`. * Gibt true zurueck, wenn der Aufruf noch erlaubt ist. */ export function rateLimit(key: string, limit: number, windowMs: number): boolean { const now = Date.now(); // Speicher begrenzen: abgelaufene Eintraege gelegentlich aufraeumen. if (buckets.size > MAX_BUCKETS) { for (const [k, bucket] of buckets) { if (now > bucket.resetAt) buckets.delete(k); } } const existing = buckets.get(key); if (!existing || now > existing.resetAt) { buckets.set(key, { count: 1, resetAt: now + windowMs }); return true; } if (existing.count >= limit) return false; buckets.set(key, { ...existing, count: existing.count + 1 }); return true; } /** * Client-IP fuer Rate-Limit-Schluessel in Server Actions/Route Handlern. * Hinter einem Reverse-Proxy muss dieser x-forwarded-for korrekt setzen. * * Nimmt bewusst den LETZTEN Eintrag von x-forwarded-for, nicht den ersten: * jeder Proxy in der Kette haengt die von IHM beobachtete Adresse ans Ende * an, der letzte Eintrag ist also der vom eigenen (vertrauenswuerdigen) * Reverse-Proxy gesehene Absender. Der erste Eintrag ist dagegen ein vom * Client frei waehlbarer Header-Wert und liesse sich zum Umgehen jedes * Rate-Limits in dieser Datei faelschen (Login, 2FA, Checkout, Reset, ...). */ export async function getClientIp(): Promise { const headerStore = await headers(); return ( headerStore.get("x-forwarded-for")?.split(",").pop()?.trim() || headerStore.get("x-real-ip") || "unknown" ); }