Add rate limiting in the server using built in Nest.js capability (#3566)
* Add rate limiting in the server using built in Nest.js capability * Generatekey based on ip address when an http request is sent * Update env var types to number for ttl and limit * Remove unused env variables * Use getRequest utility function * fix: remove dist from path * fix: adding .env variables * fix: remove unused functions * feat: throttler plugin * Fix according to review --------- Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -287,4 +287,12 @@ export class EnvironmentService {
|
||||
isSignUpDisabled(): boolean {
|
||||
return this.configService.get<boolean>('IS_SIGN_UP_DISABLED') ?? false;
|
||||
}
|
||||
|
||||
getApiRateLimitingTtl(): number {
|
||||
return this.configService.get<number>('API_RATE_LIMITING_TTL') ?? 100;
|
||||
}
|
||||
|
||||
getApiRateLimitingLimit(): number {
|
||||
return this.configService.get<number>('API_RATE_LIMITING_LIMIT') ?? 200;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
||||
import * as express from 'express';
|
||||
import {
|
||||
getDocumentString,
|
||||
handleStreamOrSingleExecutionResult,
|
||||
@ -7,10 +6,9 @@ import {
|
||||
Plugin,
|
||||
} from '@envelop/core';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { GraphQLContext } from 'src/graphql-config/interfaces/graphql-context.interface';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
||||
import { TokenService } from 'src/core/auth/services/token.service';
|
||||
import {
|
||||
convertExceptionToGraphQLError,
|
||||
filterException,
|
||||
@ -18,13 +16,9 @@ import {
|
||||
|
||||
export type ExceptionHandlerPluginOptions = {
|
||||
/**
|
||||
* The driver to use to handle exceptions.
|
||||
* The exception handler service to use.
|
||||
*/
|
||||
exceptionHandlerService: ExceptionHandlerService;
|
||||
/**
|
||||
* The token service to use to get the token from the request.
|
||||
*/
|
||||
tokenService: TokenService;
|
||||
/**
|
||||
* The key of the event id in the error's extension. `null` to disable.
|
||||
* @default exceptionEventId
|
||||
@ -32,13 +26,9 @@ export type ExceptionHandlerPluginOptions = {
|
||||
eventIdKey?: string | null;
|
||||
};
|
||||
|
||||
export const useExceptionHandler = <
|
||||
PluginContext extends Record<string, any> = object,
|
||||
>(
|
||||
export const useExceptionHandler = <PluginContext extends GraphQLContext>(
|
||||
options: ExceptionHandlerPluginOptions,
|
||||
): Plugin<PluginContext> => {
|
||||
const exceptionHandlerService = options.exceptionHandlerService;
|
||||
const tokenService = options.tokenService;
|
||||
const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId';
|
||||
|
||||
function addEventId(
|
||||
@ -54,28 +44,17 @@ export const useExceptionHandler = <
|
||||
|
||||
return {
|
||||
async onExecute({ args }) {
|
||||
const exceptionHandlerService = options.exceptionHandlerService;
|
||||
const rootOperation = args.document.definitions.find(
|
||||
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
||||
) as OperationDefinitionNode;
|
||||
const operationType = rootOperation.operation;
|
||||
const user = args.contextValue.user;
|
||||
const document = getDocumentString(args.document, print);
|
||||
const request = args.contextValue.req as express.Request;
|
||||
const opName =
|
||||
args.operationName ||
|
||||
rootOperation.name?.value ||
|
||||
'Anonymous Operation';
|
||||
let user: ExceptionHandlerUser | undefined;
|
||||
|
||||
if (tokenService.isTokenPresent(request)) {
|
||||
try {
|
||||
const data = await tokenService.validateToken(request);
|
||||
|
||||
user = {
|
||||
id: data.user?.id,
|
||||
email: data.user?.email,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
onExecuteDone(payload) {
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
import { getGraphQLRateLimiter } from 'graphql-rate-limit';
|
||||
import { Plugin } from '@envelop/core';
|
||||
import { useOnResolve } from '@envelop/on-resolve';
|
||||
|
||||
import { GraphQLContext } from 'src/graphql-config/graphql-config.service';
|
||||
|
||||
export class UnauthenticatedError extends Error {}
|
||||
|
||||
export type IdentifyFn<ContextType = ThrottlerContext> = (
|
||||
context: ContextType,
|
||||
) => string;
|
||||
|
||||
export type ThrottlerPluginOptions = {
|
||||
identifyFn: IdentifyFn;
|
||||
ttl?: number;
|
||||
limit?: number;
|
||||
transformError?: (message: string) => Error;
|
||||
onThrottlerError?: (event: {
|
||||
error: string;
|
||||
identifier: string;
|
||||
context: unknown;
|
||||
info: GraphQLResolveInfo;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
interface ThrottlerContext extends GraphQLContext {
|
||||
rateLimiterFn: ReturnType<typeof getGraphQLRateLimiter>;
|
||||
}
|
||||
|
||||
export const useThrottler = (
|
||||
options: ThrottlerPluginOptions,
|
||||
): Plugin<ThrottlerContext> => {
|
||||
const rateLimiterFn = getGraphQLRateLimiter({
|
||||
identifyContext: options.identifyFn,
|
||||
});
|
||||
|
||||
return {
|
||||
onPluginInit({ addPlugin }) {
|
||||
addPlugin(
|
||||
useOnResolve(async ({ args, root, context, info }) => {
|
||||
if (options.limit && options.ttl) {
|
||||
const id = options.identifyFn(context);
|
||||
|
||||
const errorMessage = await context.rateLimiterFn(
|
||||
{ parent: root, args, context, info },
|
||||
{
|
||||
max: options?.limit,
|
||||
window: `${options?.ttl}s`,
|
||||
message: interpolate('Too much request.', {
|
||||
id,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (errorMessage) {
|
||||
if (options.onThrottlerError) {
|
||||
options.onThrottlerError({
|
||||
error: errorMessage,
|
||||
identifier: id,
|
||||
context,
|
||||
info,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.transformError) {
|
||||
throw options.transformError(errorMessage);
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
async onContextBuilding({ extendContext }) {
|
||||
extendContext({
|
||||
rateLimiterFn,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function interpolate(message: string, args: { [key: string]: string }) {
|
||||
return message.replace(/\{{([^)]*)\}}/g, (_, key) => args[key.trim()]);
|
||||
}
|
||||
Reference in New Issue
Block a user