const rateLimit = require('express-rate-limit') const log = require('../utils/logger')('ratelimit') function makeLimiter({ windowMs, max, label, message }) { return rateLimit({ windowMs, max, standardHeaders: true, legacyHeaders: false, message: { message }, handler: (req, res, next, options) => { log.warn(`${label} rate limit exceeded`, { ip: req.ip, path: req.originalUrl }) res.status(options.statusCode).json(options.message) }, }) } // Brute-force protection on login. const loginLimiter = makeLimiter({ windowMs: 15 * 60 * 1000, max: 10, label: 'login', message: 'Too many login attempts. Please try again later.', }) // Public self-registration. Mirrors the login cap: a handful of legitimate // attempts per window, a flood is abuse. The global botScore guard + honeypot // cover the rest. const registerLimiter = makeLimiter({ windowMs: 15 * 60 * 1000, max: 10, label: 'register', message: 'Too many registration attempts. Please try again later.', }) // Authenticated self-service credential changes (username / password). Tighter // than login — a signed-in player rarely changes these, and the wrong-current- // password path also feeds the shared login backoff (see the controller). const accountChangeLimiter = makeLimiter({ windowMs: 15 * 60 * 1000, max: 10, label: 'account-change', message: 'Too many changes. Please try again later.', }) // Throttle the public contact form. const contactLimiter = makeLimiter({ windowMs: 60 * 60 * 1000, max: 5, label: 'contact', message: 'Too many messages sent. Please try again later.', }) // Cap mobile refresh-token exchanges per IP. Legitimate apps refresh at most a // handful of times per window; a flood is either a bug or an attempt to brute // the refresh endpoint. const mobileRefreshLimiter = makeLimiter({ windowMs: 15 * 60 * 1000, max: 30, label: 'mobile-refresh', message: 'Too many refresh attempts. Please try again later.', }) // Throttle SSO redirect starts per IP — cheap to trigger, and a flood is either a // bug or an attempt to spin the OAuth flow. Generous enough for real users. const ssoStartLimiter = makeLimiter({ windowMs: 15 * 60 * 1000, max: 30, label: 'sso-start', message: 'Too many sign-in attempts. Please try again later.', }) // Password-reset requests per IP. Each one can send email, so cap tighter than // login to blunt email-bombing and enumeration timing probes. The endpoint always // returns a generic success regardless of match, so honest users never see this. const passwordResetRequestLimiter = makeLimiter({ windowMs: 60 * 60 * 1000, max: 5, label: 'password-reset-request', message: 'Too many reset requests. Please try again later.', }) // Reset confirmations (token + new password) per IP. A wrong/expired token is a // guessing surface; the token itself is 256-bit random, but cap anyway. const passwordResetConfirmLimiter = makeLimiter({ windowMs: 15 * 60 * 1000, max: 15, label: 'password-reset-confirm', message: 'Too many attempts. Please try again later.', }) module.exports = { loginLimiter, registerLimiter, accountChangeLimiter, contactLimiter, mobileRefreshLimiter, ssoStartLimiter, passwordResetRequestLimiter, passwordResetConfirmLimiter, }