-
Notifications
You must be signed in to change notification settings - Fork 368
/
ratelimit.middleware.ts
68 lines (64 loc) · 2.53 KB
/
ratelimit.middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import type { Request, Response, NextFunction } from 'express';
import { createClient } from 'redis';
import type { RateLimiterRes } from 'rate-limiter-flexible';
import { RateLimiterRedis, RateLimiterMemory } from 'rate-limiter-flexible';
import { getAccount, getRedisUrl, logger } from '@nangohq/shared';
const rateLimiter = await (async () => {
const opts = {
keyPrefix: 'middleware',
points: parseInt(process.env['DEFAULT_RATE_LIMIT_PER_MIN'] || '0') || 2400,
duration: 60,
blockDuration: 0
};
const url = getRedisUrl();
if (url) {
const redisClient = await createClient({ url: url, disableOfflineQueue: true }).connect();
redisClient.on('error', (err) => {
logger.error(`Redis (rate-limiter) error: ${err}`);
});
return new RateLimiterRedis({
storeClient: redisClient,
...opts
});
}
return new RateLimiterMemory(opts);
})();
export const rateLimiterMiddleware = (req: Request, res: Response, next: NextFunction) => {
const setXRateLimitHeaders = (rateLimiterRes: RateLimiterRes) => {
const resetEpoch = Math.floor(new Date(Date.now() + rateLimiterRes.msBeforeNext).getTime() / 1000);
res.setHeader('X-RateLimit-Limit', rateLimiter.points);
res.setHeader('X-RateLimit-Remaining', rateLimiterRes.remainingPoints);
res.setHeader('X-RateLimit-Reset', resetEpoch);
};
const key = getKey(req, res);
const pointsToConsume = getPointsToConsume(req);
rateLimiter
.consume(key, pointsToConsume)
.then((rateLimiterRes) => {
setXRateLimitHeaders(rateLimiterRes);
next();
})
.catch((rateLimiterRes) => {
res.setHeader('Retry-After', Math.floor(rateLimiterRes.msBeforeNext / 1000));
setXRateLimitHeaders(rateLimiterRes);
logger.info(`Rate limit exceeded for ${key}. Request: ${req.method} ${req.path})`);
res.status(429).send('Too Many Requests');
});
};
function getKey(req: Request, res: Response): string {
try {
return `account-${getAccount(res)}`;
} catch {
if (req.user) {
return `user-${req.user.id}`;
}
return `ip-${req.ip}`;
}
}
function getPointsToConsume(req: Request): number {
if (['/api/v1/signin', '/api/v1/signup', '/api/v1/forgot-password', '/api/v1/reset-password'].includes(req.path)) {
// limiting to 6 requests per period to avoid brute force attacks
return rateLimiter.points / 6;
}
return 1;
}