Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
♻️ Use custom rate limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Nov 17, 2020
1 parent 6840caa commit c6ba4af
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 11 deletions.
6 changes: 6 additions & 0 deletions src/config/configuration.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface Configuration {
apiKeyLruSize: number;
};

rateLimit: {
public: { points: number; duration: number };
authenticated: { points: number; duration: number };
apiKey: { points: number; duration: number };
};

security: {
saltRounds: number;
jwtSecret: string;
Expand Down
14 changes: 14 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ const configuration: Configuration = {
domainVerificationFile:
process.env.DOMAIN_VERIFICATION_FILE ?? 'staart-verify.txt',
},
rateLimit: {
public: {
points: int(process.env.RATE_LIMIT_PUBLIC_POINTS, 250),
duration: int(process.env.RATE_LIMIT_PUBLIC_DURATION, 3600),
},
authenticated: {
points: int(process.env.RATE_LIMIT_AUTHENTICATED_POINTS, 5000),
duration: int(process.env.RATE_LIMIT_AUTHENTICATED_DURATION, 3600),
},
apiKey: {
points: int(process.env.RATE_LIMIT_API_KEY_POINTS, 10000),
duration: int(process.env.RATE_LIMIT_API_KEY_DURATION, 3600),
},
},
caching: {
geolocationLruSize: int(process.env.GEOLOCATION_LRU_SIZE, 100),
apiKeyLruSize: int(process.env.API_KEY_LRU_SIZE, 100),
Expand Down
2 changes: 2 additions & 0 deletions src/errors/errors.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ export const BILLING_ACCOUNT_CREATED_CONFLICT =
export const MFA_ENABLED_CONFLICT =
'409004: Multi-factor authentication is already enabled';
export const MERGE_USER_CONFLICT = '409005: Cannot merge the same user';

export const RATE_LIMIT_EXCEEDED = '429000: Rate limit exceeded';
66 changes: 57 additions & 9 deletions src/interceptors/rate-limit.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,72 @@
import {
CallHandler,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { RateLimiterMemory } from 'rate-limiter-flexible';
import { getClientIp } from 'request-ip';
import { Observable } from 'rxjs';
import { STAART_AUDIT_LOG_DATA } from '../modules/audit-logs/audit-log.constants';
import { Configuration } from '../config/configuration.interface';
import { RATE_LIMIT_EXCEEDED } from '../errors/errors.constants';
import { UserRequest } from '../modules/auth/auth.interface';

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
logger = new Logger(RateLimitInterceptor.name);
private rateLimiterPublic = new RateLimiterMemory(
this.configService.get<Configuration['rateLimit']['public']>(
'rateLimit.public',
),
);
private rateLimiterAuthenticated = new RateLimiterMemory(
this.configService.get<Configuration['rateLimit']['authenticated']>(
'rateLimit.authenticated',
),
);
private rateLimiterApiKey = new RateLimiterMemory(
this.configService.get<Configuration['rateLimit']['apiKey']>(
'rateLimit.apiKey',
),
);

constructor(private readonly reflector: Reflector) {}
constructor(
private readonly reflector: Reflector,
private configService: ConfigService,
) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
let auditLog = this.reflector.get<string | string[]>(
STAART_AUDIT_LOG_DATA,
context.getHandler(),
);
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const points =
this.reflector.get<number>('rateLimit', context.getHandler()) ?? 1;
const request = context.switchToHttp().getRequest() as UserRequest;
const response = context.switchToHttp().getResponse();
let limiter = this.rateLimiterPublic;
if (request.user.type === 'api-key') limiter = this.rateLimiterApiKey;
else if (request.user.type === 'user')
limiter = this.rateLimiterAuthenticated;
try {
const ip = getClientIp(request);
const result = await limiter.consume(ip.replace(/^.*:/, ''), points);
response.header('Retry-After', Math.ceil(result.msBeforeNext / 1000));
response.header('X-RateLimit-Limit', points);
response.header('X-Retry-Remaining', result.remainingPoints);
response.header(
'X-Retry-Reset',
new Date(Date.now() + result.msBeforeNext).toUTCString(),
);
} catch (result) {
response.header('Retry-After', Math.ceil(result.msBeforeNext / 1000));
throw new HttpException(
RATE_LIMIT_EXCEEDED,
HttpStatus.TOO_MANY_REQUESTS,
);
}
return next.handle();
}
}
4 changes: 2 additions & 2 deletions src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
Query,
} from '@nestjs/common';
import { users } from '@prisma/client';
import { RateLimit } from 'nestjs-rate-limiter';
import { Expose } from '../../providers/prisma/prisma.interface';
import { CursorPipe } from '../../pipes/cursor.pipe';
import { OptionalIntPipe } from '../../pipes/optional-int.pipe';
import { OrderByPipe } from '../../pipes/order-by.pipe';
import { WherePipe } from '../../pipes/where.pipe';
import { Expose } from '../../providers/prisma/prisma.interface';
import { RateLimit } from '../auth/rate-limit.decorator';
import { Scopes } from '../auth/scope.decorator';
import { UpdateUserDto } from './users.dto';
import { UsersService } from './users.service';
Expand Down

0 comments on commit c6ba4af

Please sign in to comment.