const {RateLimiterPostgres, RateLimiterUnion} = require('rate-limiter-flexible'); const {Duration} = require('luxon') const pg = require('../../db/pg') const globalOptions = { storeClient: pg, tableName: 'sos.rate_limits', tableCreated: true } function createRateLimit(maxTimes, duration, prefix) { return new RateLimiterPostgres({ ...globalOptions, points: maxTimes, inmemoryBlockOnConsumed: maxTimes, keyPrefix: prefix, duration }) } module.exports = {}; const loginRateLimits = [ // Once per second createRateLimit(1, 1, 'logon1'), // Ten tries in five minutes createRateLimit(10, 5 * 60, 'logon2'), // One hundred in a day createRateLimit(100, 24 * 60 * 60, 'logon3') ] const loginRateLimitPool = new RateLimiterUnion(...loginRateLimits) module.exports.loginRateLimit = async (req, res) => { try { let currentLimits = await Promise.all( loginRateLimits.map(limiter => limiter.get(req.body.email)) ) const failed = currentLimits.filter(limit => limit?.remainingPoints === 0) if(failed.length > 0) throw failed; await loginRateLimitPool.consume(req.body.email) return 'next'; } catch (limits) { const waitMillis = Math.max(...Object.values(limits).map(limit => limit?.msBeforeNext || 1)); const wait = Math.ceil(waitMillis / 1000) res.status(429) res.set({ 'Retry-After': wait }) const duration = secondsToTimeString(wait) res.json({errors: [{ param: 'email', msg: `Too many login attempts, please wait ${duration}` },{ param: 'password', msg: ' ' }]}) } } module.exports.loginRateLimit.reset = email => Promise.all( loginRateLimits.map(rateLimit => rateLimit.delete(email) ) ) function secondsToTimeString(seconds) { const duration = Duration.fromObject({seconds}).shiftTo('days', 'hours', 'minutes', 'seconds') let string = ''; if(duration.days) string += `${duration.days} day${duration.days > 1 ? 's' : ''}` if(duration.hours) { if (string) string += ', ' string += `${duration.hours} hour${duration.hours > 1 ? 's' : ''}` } if(duration.minutes) { if (string) string += ', ' string += `${duration.minutes} minute${duration.minutes > 1 ? 's' : ''}` } if(duration.seconds) { if (string) string += ', ' string += `${duration.seconds} second${duration.seconds > 1 ? 's' : ''}` } if(!string) return '1 second' return string }