38 lines
1.3 KiB
JavaScript
38 lines
1.3 KiB
JavaScript
// Lightweight in-memory rate limiter for sensitive endpoints such as login.
|
|
// Keeps POSHManager dependency-free for a single-process deployment. For a
|
|
// multi-replica deployment behind a load balancer, replace the Map with a
|
|
// shared store (Redis) so limits are enforced across instances.
|
|
|
|
const buckets = new Map();
|
|
|
|
export function rateLimit({ windowMs = 60_000, max = 10, keyPrefix = 'rl' } = {}) {
|
|
return function rateLimiter(req, res, next) {
|
|
const ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
|
const key = `${keyPrefix}:${ip}`;
|
|
const nowTs = Date.now();
|
|
const entry = buckets.get(key);
|
|
|
|
if (!entry || nowTs > entry.resetAt) {
|
|
buckets.set(key, { count: 1, resetAt: nowTs + windowMs });
|
|
return next();
|
|
}
|
|
|
|
entry.count += 1;
|
|
if (entry.count > max) {
|
|
const retryAfter = Math.max(1, Math.ceil((entry.resetAt - nowTs) / 1000));
|
|
res.setHeader('Retry-After', String(retryAfter));
|
|
return res.status(429).json({ error: 'Too many requests. Please slow down and try again shortly.' });
|
|
}
|
|
return next();
|
|
};
|
|
}
|
|
|
|
// Periodically drop expired buckets so the Map cannot grow unbounded.
|
|
const sweep = setInterval(() => {
|
|
const nowTs = Date.now();
|
|
for (const [key, entry] of buckets) {
|
|
if (nowTs > entry.resetAt) buckets.delete(key);
|
|
}
|
|
}, 5 * 60_000);
|
|
sweep.unref?.();
|