← 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 };