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.
58 lines
1.9 KiB
TypeScript
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"
|
|
);
|
|
}
|