/** * Rate limiters — anti-brute-force. * * In-memory store via express-rate-limit's default. Fine for a single Node * process. If we ever scale to multiple workers, swap to a shared store * (Redis or rate-limit-redis). * * Friendly 429 response handles HTMX requests (returns HX-Redirect to the * form page so the error is shown inline) and full requests (renders a * small message page). */ import rateLimit from 'express-rate-limit'; import { renderPage } from './render.js'; // Achter Cloudflare/Caddy kan req.ip binnenkomen als "1.2.3.4:11046" (IPv4 met // poort). express-rate-limit v7 valideert het IP en gooit anders // ERR_ERL_INVALID_IP_ADDRESS — onafgevangen async → het proces crasht (en pm2 // loopt in een restart-loop). Strip een trailing IPv4-poort, val terug op de // socket, en laat IPv6 (meerdere dubbele punten) ongemoeid. function clientKey(req) { let ip = req.ip || req.socket?.remoteAddress || ''; if (/^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(ip)) ip = ip.split(':')[0]; return ip || 'unknown'; } function blockedHandler(viewName, bodyClass, friendlyMsg) { return (req, res, next, options) => { const message = `${friendlyMsg} Try again in a few minutes.`; if (req.headers['hx-request'] === 'true') { // HTMX: surface the error in the form's error slot via partial render. return renderPage(req, res, viewName, { pageTitle: 'Too many attempts', bodyClass, error: message, username: req.body?.username || '', email: req.body?.email || '', }); } res.status(options.statusCode).type('html').send(`

Too many attempts

${message}

← Home

`); }; } export const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 min max: 5, // 5 attempts per IP per window standardHeaders: true, legacyHeaders: false, keyGenerator: clientKey, validate: { ip: false }, // Only count failed attempts. Successful logins don't burn the budget. skipSuccessfulRequests: true, handler: blockedHandler('pages/auth-login', 'on-special', 'Too many login attempts.'), }); export const registerLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 signups per IP per hour standardHeaders: true, legacyHeaders: false, keyGenerator: clientKey, validate: { ip: false }, skipSuccessfulRequests: false, // any attempt counts (registration spam is the concern) handler: blockedHandler('pages/auth-register', 'on-special', 'Too many signup attempts.'), });