66 lines
1.5 KiB
TypeScript
66 lines
1.5 KiB
TypeScript
import IORedis from 'ioredis'
|
|
import { BaseContext } from 'koa'
|
|
import {
|
|
RateLimiterMemory,
|
|
RateLimiterRedis,
|
|
RateLimiterRes,
|
|
} from 'rate-limiter-flexible'
|
|
|
|
const Redis = IORedis.default
|
|
|
|
const rateLimiter =
|
|
process.env.NODE_ENV === 'production' && process.env.SCALINGO_REDIS_URL
|
|
? new RateLimiterRedis({
|
|
storeClient: new Redis(process.env.SCALINGO_REDIS_URL, {
|
|
enableOfflineQueue: false,
|
|
keyPrefix: 'rate-limiter',
|
|
}),
|
|
keyPrefix: 'rate-limiter',
|
|
points: 5, // 5 requests for ctx.ip
|
|
duration: 1, // per 1 second
|
|
})
|
|
: new RateLimiterMemory({
|
|
points: 5, // 5 requests for ctx.ip
|
|
duration: 1, // per 1 seconds
|
|
})
|
|
|
|
export const rateLimiterMiddleware = async (
|
|
ctx: BaseContext,
|
|
next: () => Promise<unknown>
|
|
) => {
|
|
try {
|
|
await rateLimiter.consume(ctx.ip)
|
|
} catch (rejRes) {
|
|
ctx.status = 429
|
|
ctx.body = 'Too Many Requests'
|
|
|
|
if (isRateLimiterRes(rejRes)) {
|
|
ctx.set({
|
|
'Retry-After': (rejRes.msBeforeNext / 1000).toString(),
|
|
'X-RateLimit-Limit': rateLimiter.points.toString(),
|
|
'X-RateLimit-Remaining': rejRes.remainingPoints.toString(),
|
|
'X-RateLimit-Reset': new Date(
|
|
Date.now() + rejRes.msBeforeNext
|
|
).toString(),
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
return await next()
|
|
}
|
|
|
|
const isRateLimiterRes = (val: unknown): val is RateLimiterRes => {
|
|
return !!(
|
|
val &&
|
|
typeof val === 'object' &&
|
|
[
|
|
'msBeforeNext',
|
|
'remainingPoints',
|
|
'consumedPoints',
|
|
'isFirstInDuration',
|
|
].every((s) => s in val)
|
|
)
|
|
}
|