/** * Rate limiting via fichiers JSON locaux * Implémentation recommandée spec F §8 : /tmp/nav-ratelimit/{IP_hash}.json * * - IP hashée SHA-256 (RGPD — pas de stockage IP en clair) * - Fichier JSON par IP, reset automatique si date != today * - Dossier créé au premier appel si absent * * Usage : * const allowed = await checkRateLimitJson(ip, 'chatbot', 10) * if (!allowed) throw createError({ statusCode: 429, ... }) */ import { createHash } from 'crypto' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' import { join } from 'path' const RATELIMIT_DIR = '/tmp/nav-ratelimit' type RateLimitFile = { [action: string]: { count: number; date: string } } function ensureDir() { if (!existsSync(RATELIMIT_DIR)) { mkdirSync(RATELIMIT_DIR, { recursive: true }) } } function hashIp(ip: string): string { return createHash('sha256').update(ip).digest('hex') } function todayStr(): string { const d = new Date() return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}` } function readFile(ipHash: string): RateLimitFile { const path = join(RATELIMIT_DIR, `${ipHash}.json`) try { return JSON.parse(readFileSync(path, 'utf-8')) } catch { return {} } } function writeFile(ipHash: string, data: RateLimitFile) { const path = join(RATELIMIT_DIR, `${ipHash}.json`) writeFileSync(path, JSON.stringify(data), 'utf-8') } /** * Vérifie et incrémente le compteur pour une IP et une action. * @param ip Adresse IP du client (sera hashée SHA-256) * @param action Clé d'action (ex : 'chatbot', 'submit', 'comment') * @param maxPerDay Nombre max d'appels autorisés par jour * @returns true si autorisé, false si limite dépassée */ export function checkRateLimitJson( ip: string, action: string, maxPerDay: number, ): boolean { ensureDir() const ipHash = hashIp(ip) const today = todayStr() const data = readFile(ipHash) const entry = data[action] if (!entry || entry.date !== today) { // Nouveau jour ou premier appel : reset et autoriser data[action] = { count: 1, date: today } writeFile(ipHash, data) return true } if (entry.count >= maxPerDay) { return false } entry.count++ writeFile(ipHash, data) return true }