This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

View File

@@ -0,0 +1,83 @@
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