Files
website/server/src/middleware/rateLimit.js
Claude f8bcc7f6a3 Player accounts backend: schema, registration, self-service, SSO provision
- Widen users.role enum to include 'player'; make password_hash nullable;
  add email/email_verified/status/last_login_ip; pin username _ci collation.
- POST /auth/register (honeypot + registerLimiter + botScore, reserved-name
  blocklist, duplicate->409, auto-login). player_registration setting gates it.
- SSO auto-provision in finishLogin (setting-gated); return/portal-aware SSO
  redirects for the player portal; status refusal on login + requireAuth.
- New /player self-service group (account, change username/password, TOTP,
  identities), reusing account.controller; accountChangeLimiter.
- Admin: 'player' role + status/email on user create/update, role/status audit,
  player_registration enum validation, derived public registration flags.
- usernamePolicy module (reserved, sanitize, derive, dedup) + unit tests;
  extend SSO callback tests. 133 server tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
2026-07-06 01:36:51 -05:00

82 lines
2.4 KiB
JavaScript

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.',
})
module.exports = {
loginLimiter,
registerLimiter,
accountChangeLimiter,
contactLimiter,
mobileRefreshLimiter,
ssoStartLimiter,
}