/** * Rate limiting via Redis (ioredis) * Fallback : compteur en mémoire si Redis indisponible (dev local sans Docker) * * Usage : * const allowed = await checkRateLimit(ip, 'submit', 3) * if (!allowed) throw createError({ statusCode: 429, ... }) */ import Redis from 'ioredis' let redisClient: Redis | null = null let redisConnected = false // Compteur en mémoire (fallback si Redis KO) const memoryCounters: Map = new Map() function getRedisClient(): Redis | null { if (redisClient !== null) return redisConnected ? redisClient : null try { const config = useRuntimeConfig() const url = config.redisUrl || 'redis://127.0.0.1:6379' redisClient = new Redis(url, { lazyConnect: true, enableOfflineQueue: false, maxRetriesPerRequest: 1, connectTimeout: 2000, }) redisClient.on('connect', () => { redisConnected = true console.log('[rateLimit] Redis connecté') }) redisClient.on('error', (err) => { redisConnected = false console.warn('[rateLimit] Redis erreur — fallback mémoire activé:', err.message) }) redisClient.connect().catch(() => { redisConnected = false }) return redisConnected ? redisClient : null } catch (e) { console.warn('[rateLimit] Impossible de créer le client Redis — fallback mémoire') return null } } /** * Vérifie et incrémente le compteur d'appels. * @param ip Adresse IP du client * @param action Clé d'action (ex : 'submit', 'comment') * @param maxPerDay Nombre max d'appels autorisés par jour * @returns true si autorisé, false si limite dépassée */ export async function checkRateLimit( ip: string, action: string, maxPerDay: number, ): Promise { const key = `ratelimit:${action}:${ip}:${todayKey()}` const client = getRedisClient() if (client && redisConnected) { try { const count = await client.incr(key) if (count === 1) { // Expire à minuit (secondes restantes dans la journée) await client.expireat(key, tomorrowMidnightUnix()) } return count <= maxPerDay } catch (e) { console.warn('[rateLimit] Redis incr échoué — fallback mémoire:', (e as Error).message) } } // Fallback mémoire return memoryRateLimit(key, maxPerDay) } // ── Helpers ──────────────────────────────────────────────────────────────────── function todayKey(): string { const d = new Date() return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}` } function tomorrowMidnightUnix(): number { const d = new Date() d.setUTCHours(24, 0, 0, 0) return Math.floor(d.getTime() / 1000) } function memoryRateLimit(key: string, maxPerDay: number): boolean { const now = Date.now() const entry = memoryCounters.get(key) if (!entry || entry.resetAt <= now) { memoryCounters.set(key, { count: 1, resetAt: tomorrowMidnightUnix() * 1000 }) return true } entry.count++ return entry.count <= maxPerDay }