feat: rate limiting sur les routes d'authentification critiques
- Ajout de lib/rate-limit.js : fabrique de limiter en mémoire (closure + Map avec nettoyage lazy), sans dépendance externe, réutilisable - Ajout de middleware.js : intercepte /api/auth/register (5 req/15min) et /api/auth/callback/credentials (10 req/5min), répond 429 + Retry-After - Ajout de tasks/todo.md et tasks/lessons.md (suivi CLAUDE.md) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Fabrique de rate-limiter en mémoire.
|
||||
*
|
||||
* Adapté à un déploiement single-process (PM2 sans cluster).
|
||||
* Pour un déploiement multi-instances, remplacer le Map par un store
|
||||
* Redis partagé (ex. @upstash/ratelimit).
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} options.windowMs - Durée de la fenêtre en millisecondes
|
||||
* @param {number} options.max - Nombre maximum de requêtes par fenêtre
|
||||
*/
|
||||
export function createRateLimiter({windowMs, max}) {
|
||||
const store = new Map()
|
||||
let lastCleanup = Date.now()
|
||||
|
||||
/**
|
||||
* Supprime les entrées expirées du store.
|
||||
* Appelé automatiquement une fois par fenêtre temporelle.
|
||||
*/
|
||||
function cleanup(now) {
|
||||
for (const [storeKey, entry] of store) {
|
||||
if (now - entry.start >= windowMs) {
|
||||
store.delete(storeKey)
|
||||
}
|
||||
}
|
||||
|
||||
lastCleanup = now
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la clé (IP:route) dépasse la limite autorisée.
|
||||
*
|
||||
* @param {string} key - Identifiant unique (ex. "1.2.3.4:/api/auth/register")
|
||||
* @returns {{ success: boolean, retryAfter?: number }}
|
||||
*/
|
||||
return key => {
|
||||
const now = Date.now()
|
||||
|
||||
if (now - lastCleanup >= windowMs) {
|
||||
cleanup(now)
|
||||
}
|
||||
|
||||
const entry = store.get(key)
|
||||
|
||||
// Première requête ou fenêtre expirée : on repart à zéro
|
||||
if (!entry || now - entry.start >= windowMs) {
|
||||
store.set(key, {count: 1, start: now})
|
||||
return {success: true}
|
||||
}
|
||||
|
||||
// Limite atteinte
|
||||
if (entry.count >= max) {
|
||||
const retryAfter = Math.ceil((entry.start + windowMs - now) / 1000)
|
||||
return {success: false, retryAfter}
|
||||
}
|
||||
|
||||
// Incrément normal
|
||||
store.set(key, {...entry, count: entry.count + 1})
|
||||
return {success: true}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user