diff --git a/lib/middlewares/rateLimiter.js b/lib/middlewares/rateLimiter.js index 63f8df99c..b566a751c 100644 --- a/lib/middlewares/rateLimiter.js +++ b/lib/middlewares/rateLimiter.js @@ -3,7 +3,7 @@ * Reads named profiles from config.rateLimit and exports a limiter for each. * When a profile is missing (e.g. in dev), returns a passthrough middleware. */ -import rateLimit from 'express-rate-limit'; +import rateLimit, { ipKeyGenerator } from 'express-rate-limit'; import config from '../../config/index.js'; @@ -16,6 +16,14 @@ import config from '../../config/index.js'; */ const passthrough = (req, res, next) => next(); +/** + * @desc Default rate-limit key generator. Uses the authenticated user id when + * available, otherwise falls back to an IPv6-safe IP key via ipKeyGenerator. + * @param {import('express').Request} req - Express request object + * @returns {string} User id when available, otherwise IPv6-safe IP key + */ +const defaultKeyGenerator = (req) => req.user?._id?.toString() || ipKeyGenerator(req.ip); + /** * @desc Proxy that lazily creates rate-limit middleware for each named profile. * Returns the configured limiter or a passthrough when the profile is missing. @@ -28,7 +36,7 @@ const limiters = new Proxy({}, { if (name in target) return target[name]; const opts = config.rateLimit?.[name]; if (opts) { - const keyGenerator = opts.keyGenerator ?? ((req) => req.user?._id?.toString() || req.ip); + const keyGenerator = opts.keyGenerator ?? defaultKeyGenerator; target[name] = rateLimit({ ...opts, keyGenerator }); return target[name]; } diff --git a/lib/middlewares/tests/rateLimiter.unit.tests.js b/lib/middlewares/tests/rateLimiter.unit.tests.js index e3b4b6ebc..4ed4f1d15 100644 --- a/lib/middlewares/tests/rateLimiter.unit.tests.js +++ b/lib/middlewares/tests/rateLimiter.unit.tests.js @@ -21,6 +21,7 @@ jest.unstable_mockModule('express-rate-limit', () => ({ middleware._keyGenerator = opts.keyGenerator; return middleware; }, + ipKeyGenerator: (ip) => `ipkg:${ip}`, })); // Import once — the Proxy is a singleton, so use distinct profile names per test @@ -60,7 +61,7 @@ describe('rateLimiter middleware unit tests:', () => { expect(opts.keyGenerator(req)).toBe('507f1f77bcf86cd799439011'); }); - test('should fall back to req.ip for unauthenticated requests', () => { + test('should fall back to ipKeyGenerator(req.ip) for unauthenticated requests', () => { mockRateLimitConfig = { profileUnauth: { windowMs: 1000, max: 100 }, }; @@ -68,10 +69,10 @@ describe('rateLimiter middleware unit tests:', () => { const opts = calls[calls.length - 1]; const req = { ip: '203.0.113.42' }; - expect(opts.keyGenerator(req)).toBe('203.0.113.42'); + expect(opts.keyGenerator(req)).toBe('ipkg:203.0.113.42'); }); - test('should fall back to req.ip when user exists but _id is undefined', () => { + test('should fall back to ipKeyGenerator(req.ip) when user exists but _id is undefined', () => { mockRateLimitConfig = { profileNoId: { windowMs: 1000, max: 5 }, }; @@ -79,7 +80,7 @@ describe('rateLimiter middleware unit tests:', () => { const opts = calls[calls.length - 1]; const req = { user: {}, ip: '192.168.1.1' }; - expect(opts.keyGenerator(req)).toBe('192.168.1.1'); + expect(opts.keyGenerator(req)).toBe('ipkg:192.168.1.1'); }); test('should respect custom keyGenerator from config profile', () => {