2026-05-03 19:51:57 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
2026-06-06 18:38:40 -05:00
|
|
|
const { rateLimit, ipKeyGenerator } = require('express-rate-limit');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
function makeLimiter(max, windowMs, message) {
|
|
|
|
|
return rateLimit({
|
|
|
|
|
windowMs,
|
|
|
|
|
max,
|
|
|
|
|
standardHeaders: 'draft-7',
|
|
|
|
|
legacyHeaders: false,
|
|
|
|
|
// Override default handler so the response is always JSON, not HTML
|
|
|
|
|
handler(req, res) {
|
|
|
|
|
res.status(429).json({ error: message });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 10 login attempts per 15 minutes per IP — brute-force protection
|
|
|
|
|
const loginLimiter = makeLimiter(
|
|
|
|
|
10, 15 * 60 * 1000,
|
|
|
|
|
'Too many login attempts. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-10 19:42:51 -05:00
|
|
|
// 5 FAILED login attempts per 15 minutes per username — layered on top of the
|
|
|
|
|
// per-IP limiter so a distributed attacker (or many clients behind one NAT/proxy
|
|
|
|
|
// sharing an IP bucket) cannot brute-force a single account. Successful logins
|
|
|
|
|
// don't count toward the limit, so legitimate users are unaffected.
|
|
|
|
|
const loginUsernameLimiter = rateLimit({
|
|
|
|
|
windowMs: 15 * 60 * 1000,
|
|
|
|
|
max: 5,
|
|
|
|
|
standardHeaders: 'draft-7',
|
|
|
|
|
legacyHeaders: false,
|
|
|
|
|
skipSuccessfulRequests: true,
|
|
|
|
|
keyGenerator: (req) => {
|
|
|
|
|
const username = String(req.body?.username || '').trim().toLowerCase();
|
|
|
|
|
return username ? `user:${username}` : ipKeyGenerator(req);
|
|
|
|
|
},
|
|
|
|
|
handler(req, res) {
|
|
|
|
|
res.status(429).json({ error: 'Too many failed login attempts for this account. Please try again in 15 minutes.' });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
// 5 password-change attempts per 15 minutes per IP
|
|
|
|
|
const passwordLimiter = makeLimiter(
|
|
|
|
|
5, 15 * 60 * 1000,
|
|
|
|
|
'Too many password change attempts. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 20 import preview/apply requests per 15 minutes per IP
|
|
|
|
|
const importLimiter = makeLimiter(
|
|
|
|
|
20, 15 * 60 * 1000,
|
|
|
|
|
'Too many import requests. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 30 export requests per 15 minutes per IP
|
|
|
|
|
const exportLimiter = makeLimiter(
|
|
|
|
|
30, 15 * 60 * 1000,
|
|
|
|
|
'Too many export requests. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 30 admin mutation actions per 15 minutes per IP (backup/restore/cleanup)
|
|
|
|
|
const adminActionLimiter = makeLimiter(
|
|
|
|
|
30, 15 * 60 * 1000,
|
|
|
|
|
'Too many admin actions. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 20 OIDC login/callback requests per 15 minutes per IP
|
|
|
|
|
const oidcLimiter = makeLimiter(
|
|
|
|
|
20, 15 * 60 * 1000,
|
|
|
|
|
'Too many authentication requests. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-09 13:03:36 -05:00
|
|
|
// 5 backup operations per 60 minutes per IP (backup creation, restore, import)
|
|
|
|
|
const backupOperationLimiter = makeLimiter(
|
|
|
|
|
5, 60 * 60 * 1000,
|
|
|
|
|
'Too many backup operations. Please try again in 60 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 3 demo data clear operations per 15 minutes per IP
|
|
|
|
|
const demoDataLimiter = makeLimiter(
|
|
|
|
|
3, 15 * 60 * 1000,
|
|
|
|
|
'Too many demo data clear operations. Please try again in 15 minutes.',
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-06 15:08:33 -05:00
|
|
|
// 10 sync/backfill requests per 15 minutes per user — prevents SimpleFIN hammering
|
|
|
|
|
const syncLimiter = rateLimit({
|
|
|
|
|
windowMs: 15 * 60 * 1000,
|
|
|
|
|
max: 10,
|
|
|
|
|
standardHeaders: 'draft-7',
|
|
|
|
|
legacyHeaders: false,
|
2026-06-06 18:38:40 -05:00
|
|
|
keyGenerator: (req) => req.user?.id?.toString() || ipKeyGenerator(req),
|
2026-06-06 15:08:33 -05:00
|
|
|
handler(req, res) {
|
|
|
|
|
res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-09 13:03:36 -05:00
|
|
|
// ── Export all limiters plus reset function ────────────────────────────────────
|
|
|
|
|
const allLimiters = [
|
|
|
|
|
loginLimiter,
|
2026-06-10 19:42:51 -05:00
|
|
|
loginUsernameLimiter,
|
2026-05-09 13:03:36 -05:00
|
|
|
passwordLimiter,
|
|
|
|
|
importLimiter,
|
|
|
|
|
exportLimiter,
|
|
|
|
|
adminActionLimiter,
|
|
|
|
|
oidcLimiter,
|
|
|
|
|
backupOperationLimiter,
|
|
|
|
|
demoDataLimiter,
|
2026-06-06 15:08:33 -05:00
|
|
|
syncLimiter,
|
2026-05-09 13:03:36 -05:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function resetStores() {
|
|
|
|
|
for (const limiter of allLimiters) {
|
|
|
|
|
if (limiter.store.reset) {
|
|
|
|
|
limiter.store.reset();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
module.exports = {
|
|
|
|
|
loginLimiter,
|
2026-06-10 19:42:51 -05:00
|
|
|
loginUsernameLimiter,
|
2026-05-03 19:51:57 -05:00
|
|
|
passwordLimiter,
|
|
|
|
|
importLimiter,
|
|
|
|
|
exportLimiter,
|
|
|
|
|
adminActionLimiter,
|
|
|
|
|
oidcLimiter,
|
2026-05-09 13:03:36 -05:00
|
|
|
backupOperationLimiter,
|
|
|
|
|
demoDataLimiter,
|
2026-06-06 15:08:33 -05:00
|
|
|
syncLimiter,
|
2026-05-09 13:03:36 -05:00
|
|
|
resetStores,
|
2026-05-03 19:51:57 -05:00
|
|
|
};
|