source: Klonkt/src/middleware/rate-limit.js@ 7cc58bb

main
Last change on this file since 7cc58bb was 834bcc3, checked in by Robin Genis <roboburr@…>, 3 months ago

i18n: translate Dutch code comments to English across src/

Comments in routes/services/views/config/middleware/assets translated to
English for the public repo. A few dev-facing throw/console message strings
were Englished too. No user-facing UI strings or i18n dictionary values changed
(src/services/i18n.js untouched). Logic unchanged.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 2.7 KB
Line 
1/**
2 * Rate limiters — anti-brute-force.
3 *
4 * In-memory store via express-rate-limit's default. Fine for a single Node
5 * process. If we ever scale to multiple workers, swap to a shared store
6 * (Redis or rate-limit-redis).
7 *
8 * Friendly 429 response handles HTMX requests (returns HX-Redirect to the
9 * form page so the error is shown inline) and full requests (renders a
10 * small message page).
11 */
12
13import rateLimit from 'express-rate-limit';
14import { renderPage } from './render.js';
15
16// Behind Cloudflare/Caddy, req.ip can arrive as "1.2.3.4:11046" (IPv4 with
17// port). express-rate-limit v7 validates the IP and otherwise throws
18// ERR_ERL_INVALID_IP_ADDRESS — uncaught async → the process crashes (and pm2
19// enters a restart loop). Strip a trailing IPv4 port, fall back to the
20// socket address, and leave IPv6 (multiple colons) untouched.
21function clientKey(req) {
22 let ip = req.ip || req.socket?.remoteAddress || '';
23 if (/^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(ip)) ip = ip.split(':')[0];
24 return ip || 'unknown';
25}
26
27function blockedHandler(viewName, bodyClass, friendlyMsg) {
28 return (req, res, next, options) => {
29 const message = `${friendlyMsg} Try again in a few minutes.`;
30 if (req.headers['hx-request'] === 'true') {
31 // HTMX: surface the error in the form's error slot via partial render.
32 return renderPage(req, res, viewName, {
33 pageTitle: 'Too many attempts',
34 bodyClass,
35 error: message,
36 username: req.body?.username || '',
37 email: req.body?.email || '',
38 });
39 }
40 res.status(options.statusCode).type('html').send(`
41 <div style="font-family:system-ui;max-width:520px;margin:4rem auto;padding:2rem;text-align:center;">
42 <h1 style="font-size:2rem;margin:0 0 0.5rem;">Too many attempts</h1>
43 <p>${message}</p>
44 <p><a href="/" style="color:#c2410c;">&larr; Home</a></p>
45 </div>
46 `);
47 };
48}
49
50export const loginLimiter = rateLimit({
51 windowMs: 15 * 60 * 1000, // 15 min
52 max: 5, // 5 attempts per IP per window
53 standardHeaders: true,
54 legacyHeaders: false,
55 keyGenerator: clientKey,
56 validate: { ip: false },
57 // Only count failed attempts. Successful logins don't burn the budget.
58 skipSuccessfulRequests: true,
59 handler: blockedHandler('pages/auth-login', 'on-special', 'Too many login attempts.'),
60});
61
62export const registerLimiter = rateLimit({
63 windowMs: 60 * 60 * 1000, // 1 hour
64 max: 5, // 5 signups per IP per hour
65 standardHeaders: true,
66 legacyHeaders: false,
67 keyGenerator: clientKey,
68 validate: { ip: false },
69 skipSuccessfulRequests: false, // any attempt counts (registration spam is the concern)
70 handler: blockedHandler('pages/auth-register', 'on-special', 'Too many signup attempts.'),
71});
Note: See TracBrowser for help on using the repository browser.