Files
Wabenhain/src/lib/rate-limit.ts
T
Nick Szaunig c7927f2883 feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
2026-08-17 14:30:28 +02:00

58 lines
1.9 KiB
TypeScript

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<string, Bucket>();
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<string> {
const headerStore = await headers();
return (
headerStore.get("x-forwarded-for")?.split(",").pop()?.trim() ||
headerStore.get("x-real-ip") ||
"unknown"
);
}