Files
modstagram-back/utils/createRateLimiter.js
payacom 3f416a7a60 full
2026-08-31 20:45:41 +03:30

84 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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