48hr.email/infrastructure/web/api/middleware/rate-limiter.js

106 lines
3.2 KiB
JavaScript

/**
* API Rate Limiter
* Limits requests per token (if authenticated) or IP address (if not)
*/
class RateLimiter {
constructor() {
this.requests = new Map() // key -> {count, resetTime}
// Cleanup old entries every 5 minutes
setInterval(() => this.cleanup(), 5 * 60 * 1000)
}
cleanup() {
const now = Date.now()
for (const [key, data] of this.requests.entries()) {
if (data.resetTime < now) {
this.requests.delete(key)
}
}
}
/**
* Check and increment rate limit
* @param {string} key - Identifier (token or IP)
* @param {number} maxRequests - Max requests allowed
* @param {number} windowMs - Time window in milliseconds
* @returns {object} {allowed: boolean, remaining: number, resetTime: number}
*/
checkLimit(key, maxRequests, windowMs) {
const now = Date.now()
const data = this.requests.get(key)
// No previous requests or window expired
if (!data || data.resetTime < now) {
this.requests.set(key, {
count: 1,
resetTime: now + windowMs
})
return {
allowed: true,
remaining: maxRequests - 1,
resetTime: now + windowMs
}
}
// Within window - check if limit exceeded
if (data.count >= maxRequests) {
return {
allowed: false,
remaining: 0,
resetTime: data.resetTime
}
}
// Increment count
data.count++
return {
allowed: true,
remaining: maxRequests - data.count,
resetTime: data.resetTime
}
}
}
// Global rate limiter instance
const rateLimiter = new RateLimiter()
/**
* Create rate limiting middleware
* @param {number} maxRequests - Maximum requests allowed (default: 100)
* @param {number} windowMs - Time window in milliseconds (default: 60000 = 1 minute)
*/
function createRateLimiter(maxRequests = 100, windowMs = 60000) {
return function(req, res, next) {
// Determine key: use token if authenticated via Bearer, otherwise IP
let key
if (req.authMethod === 'token' && req.user) {
key = `token:${req.user.id}`
} else {
// Get IP address (consider proxy headers)
key = `ip:${req.ip || req.connection.remoteAddress}`
}
const result = rateLimiter.checkLimit(key, maxRequests, windowMs)
// Set rate limit headers
res.setHeader('X-RateLimit-Limit', maxRequests)
res.setHeader('X-RateLimit-Remaining', result.remaining)
res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000))
if (!result.allowed) {
const retryAfter = Math.ceil((result.resetTime - Date.now()) / 1000)
res.setHeader('Retry-After', retryAfter)
return res.apiError(
'Rate limit exceeded. Please try again later.',
'RATE_LIMIT_EXCEEDED',
429
)
}
next()
}
}
module.exports = createRateLimiter