const rateLimit = require('express-rate-limit') const { getRedisClient } = require('./redisClient') /** * پیاده‌سازیِ سبکِ خودمون از Store — به‌جای rate-limit-redis. علتش: rate-limit-redis * موقعِ ساختنِ RedisStore (سازنده، همون لحظه‌ی require شدنِ این ماژول، خیلی زودتر از * وصل‌شدنِ کاملِ Redis) فوراً و بدون await دو تا SCRIPT LOAD می‌فرسته و نتیجه‌شون * (Promise) رو *برای همیشه‌ی عمرِ اون instance* کش می‌کنه. اگه Redis همون لحظه‌ی * boot در دسترس نباشه، اون دو Promise رد می‌شن (unhandled rejection، حتی می‌تونه * کل پروسه رو کرش کنه) و از اون به بعد، حتی وقتی Redis بعداً وصل بشه، همون Store * برای همیشه شکسته می‌مونه چون SHAی اسکریپت هیچ‌وقت دوباره لود نمی‌شه. * * این پیاده‌سازی فقط از INCR/PEXPIRE/DECR/DEL استفاده می‌کنه (بدون اسکریپتِ لوا، * بدون هیچ حالتِ ساخته‌شده‌ی یک‌بارمصرف) — هر متد کاملاً مستقلِ متدهای دیگه‌ست، پس * هیچ حالتی نیست که «یه بار شکست خورد، برای همیشه خرابه» */ class SimpleRedisRateLimitStore { constructor (prefix, windowMs) { this.prefix = prefix this.windowMs = windowMs } async increment (key) { const redis = getRedisClient() if (!redis) throw new Error('Redis not ready') const redisKey = this.prefix + key const totalHits = await redis.incr(redisKey) if (totalHits === 1) { // اولین ضربه‌ی این پنجره — شروع‌کنندهٔ TTL. اگه دقیقاً همین‌جا پروسه کرش کنه // (قبل از رسیدن به pExpire)، این کلید بدون انقضا می‌مونه — یه edge-caseِ // شناخته‌شده و پذیرفته‌شده برای این نوع شمارنده‌ی ساده، نه یه شمارنده‌ی // امنیتی‌ای که باید صددرصد atomic باشه await redis.pExpire(redisKey, this.windowMs) } const ttl = await redis.pTTL(redisKey) return { totalHits, resetTime: ttl > 0 ? new Date(Date.now() + ttl) : undefined } } async decrement (key) { const redis = getRedisClient() if (!redis) return await redis.decr(this.prefix + key) } async resetKey (key) { const redis = getRedisClient() if (!redis) return await redis.del(this.prefix + key) } } /** * می‌سازه: یه میدل‌ورِ ریت‌لیمیت که خودش روی هر درخواست بین Redis (وقتی وصله — * محدودیتِ سراسری بین همه‌ی اینستنس‌های سرور، برای مقیاس‌پذیری/امنیتِ واقعی لازمه — * مثلاً برای جلوگیری از brute-force روی لاگین وقتی چند سرور پشتِ لودبالانسر باشه) * و حافظه‌ی in-process (fallback وقتی Redis در دسترس نیست — فقط per-instance، ولی * از کرش‌کردن یا بی‌اثرشدنِ کاملِ محدودیت بهتره) سوییچ می‌کنه. * * چون express-rate-limit اجازه نمی‌ده store رو بعد از ساختنِ میدل‌ور عوض کنی، دو * نمونه‌ی کاملاً جدا ساخته می‌شه و روی هر درخواست، بر اساسِ آماده‌بودنِ فعلیِ Redis، * یکی‌شون صدا زده می‌شه. * * @param {string} prefix - پیشوندِ یکتا برای کلیدهای این limiter توی Redis (تا با * limiterهای دیگه قاطی نشن) * @param {import('express-rate-limit').Options} options */ function createRateLimiter (prefix, options) { const memoryLimiter = rateLimit(options) const redisLimiter = rateLimit({ ...options, store: new SimpleRedisRateLimitStore(`rl:${prefix}:`, options.windowMs) }) return (req, res, next) => { const limiter = getRedisClient() ? redisLimiter : memoryLimiter limiter(req, res, next) } } module.exports = createRateLimiter