/** * Token-bucket en mémoire — best-effort par instance. * * Pour un déploiement multi-instance, swap pour un store partagé (Redis). * Ici on tourne en mono-instance Next derrière nginx-proxy-manager, donc * une Map locale suffit. * * Usage : * const r = await rateLimit({ key: ip + ":signup", windowMs: 60_000, limit: 5 }); * if (!r.ok) return tooManyRequests(r.retryAfter); */ type Bucket = { count: number; resetAt: number; }; const buckets = new Map(); const SWEEP_INTERVAL_MS = 60_000; let lastSweep = 0; function sweep(now: number) { if (now - lastSweep < SWEEP_INTERVAL_MS) return; lastSweep = now; for (const [k, b] of buckets) { if (b.resetAt <= now) buckets.delete(k); } } export type RateLimitOpts = { key: string; /** Fenêtre glissante en ms. */ windowMs: number; /** Nombre max d'appels par fenêtre. */ limit: number; }; export type RateLimitResult = { ok: boolean; remaining: number; retryAfter: number; // secondes }; export function rateLimit(opts: RateLimitOpts): RateLimitResult { const now = Date.now(); sweep(now); const b = buckets.get(opts.key); if (!b || b.resetAt <= now) { buckets.set(opts.key, { count: 1, resetAt: now + opts.windowMs }); return { ok: true, remaining: opts.limit - 1, retryAfter: 0 }; } if (b.count >= opts.limit) { return { ok: false, remaining: 0, retryAfter: Math.max(1, Math.ceil((b.resetAt - now) / 1000)), }; } b.count++; return { ok: true, remaining: opts.limit - b.count, retryAfter: 0 }; } /** Extract a client IP from a request, fallback to a safe default. */ export function getClientIp(req: Request): string { // nginx-proxy-manager pose x-forwarded-for, x-real-ip const xff = req.headers.get("x-forwarded-for"); if (xff) return xff.split(",")[0].trim(); const xri = req.headers.get("x-real-ip"); if (xri) return xri.trim(); return "unknown"; } /** Helper pratique : extract IP + applique le bucket. */ export function rateLimitRequest( req: Request, bucket: string, windowMs: number, limit: number, ): RateLimitResult { const ip = getClientIp(req); return rateLimit({ key: `${ip}:${bucket}`, windowMs, limit }); }