import rateLimit from 'express-rate-limit'; import { Request } from 'express'; // First rate limiting in the codebase. The password-reset endpoints need it // most: without one, anyone can make the server send unlimited mail to any // address. Login and registration are the obvious next candidates. // // The default in-memory store suits a single-instance deployment, which this // is. Running more than one app container would need a shared store, or each // instance would enforce its own separate allowance. const WINDOW_MS = 15 * 60 * 1000; const MAX_REQUESTS = 5; // Keyed on caller *and* address rather than caller alone. Keying on IP only // would let one person's reset attempts lock out everyone behind the same // NAT or reverse proxy — and everything here arrives via Nginx Proxy Manager, // so a great many customers share an apparent address. // // This key only makes sense on a request that carries an email. Applying the // same limiter to an endpoint without one collapses every caller into a single // `ip:` bucket, which is a shared allowance rather than a per-caller one. function keyByCallerAndEmail(req: Request): string { const email = typeof req.body?.email === 'string' ? req.body.email.toLowerCase().trim() : ''; return `${req.ip}:${email}`; } export const passwordResetRequestLimiter = rateLimit({ windowMs: WINDOW_MS, limit: MAX_REQUESTS, keyGenerator: keyByCallerAndEmail, standardHeaders: 'draft-7', legacyHeaders: false, message: { error: 'too many attempts, please try again later' } }); // Client error reports carry no email, so this one is keyed on the caller // alone — deliberately not reusing passwordResetRequestLimiter, whose comment // above explains why its key is wrong for an endpoint without an email. // // `trust proxy` is set in app.ts, so `req.ip` is the real client address from // X-Forwarded-For rather than Nginx Proxy Manager's, making this a per-customer // allowance rather than one shared by everybody behind the proxy. // // Generous, because hitting the limit is harmless: the reporter ignores the // response either way. It exists so a render loop cannot fill the log. const CLIENT_ERROR_WINDOW_MS = 15 * 60 * 1000; const CLIENT_ERROR_MAX_REQUESTS = 30; export const clientErrorLimiter = rateLimit({ windowMs: CLIENT_ERROR_WINDOW_MS, limit: CLIENT_ERROR_MAX_REQUESTS, standardHeaders: 'draft-7', legacyHeaders: false, message: { error: 'too many reports' } });