← back to AbramsOS
middleware/rate-limit.js
40 lines
// Tiny in-memory fixed-window rate limiter — no dependencies.
// Purpose: brute-force brake on the auth endpoints now that AbramsOS is on a
// public host. Keyed by client IP (accurate because server.js sets
// `trust proxy: 1`, so req.ip reflects nginx's X-Forwarded-For, not the
// tailnet peer). Single fork process → a Map is sufficient; state resets on
// restart, which is fine for a brute-force brake.
const BUCKETS = new Map(); // key -> { count, resetAt }
function rateLimit({ windowMs = 15 * 60 * 1000, max = 12, key = (req) => req.ip } = {}) {
return function rateLimitMw(req, res, next) {
const now = Date.now();
const k = key(req) || 'unknown';
let b = BUCKETS.get(k);
if (!b || b.resetAt <= now) {
b = { count: 0, resetAt: now + windowMs };
BUCKETS.set(k, b);
}
b.count++;
if (b.count > max) {
const retry = Math.max(1, Math.ceil((b.resetAt - now) / 1000));
res.set('Retry-After', String(retry));
return res.status(429).render('error', {
error: 'Too many attempts. Please wait a few minutes and try again.',
});
}
next();
};
}
// Bound memory: drop expired buckets periodically. unref so it never holds the
// process open on shutdown.
const sweep = setInterval(() => {
const now = Date.now();
for (const [k, b] of BUCKETS) if (b.resetAt <= now) BUCKETS.delete(k);
}, 10 * 60 * 1000);
if (sweep.unref) sweep.unref();
module.exports = { rateLimit };