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:
Joe S
2024-02-07 11:11:32 -06:00
committed by GitHub
parent 3831ddc002
commit 850eab8f8f
16 changed files with 413 additions and 59 deletions

View File

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { YogaDriverServerContext } from '@graphql-yoga/nestjs';
import { GraphQLContext } from 'src/graphql-config/interfaces/graphql-context.interface';
import { TokenService } from 'src/core/auth/services/token.service';
@Injectable()
export class CreateContextFactory {
constructor(private readonly tokenService: TokenService) {}
async create(
context: YogaDriverServerContext<'express'>,
): Promise<GraphQLContext> {
// Check if token is present in the request
if (this.tokenService.isTokenPresent(context.req)) {
const data = await this.tokenService.validateToken(context.req);
// Inject user and workspace into the context
return { ...context, ...data };
}
return context;
}
}

View File

@ -0,0 +1,3 @@
import { CreateContextFactory } from './create-context.factory';
export const graphQLFactories = [CreateContextFactory];

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { CoreModule } from 'src/core/core.module';
import { graphQLFactories } from 'src/graphql-config/factories';
@Module({
imports: [CoreModule],
providers: [...graphQLFactories],
exports: [...graphQLFactories],
})
export class GraphQLConfigModule {}

View File

@ -0,0 +1,147 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { GqlOptionsFactory } from '@nestjs/graphql';
import {
YogaDriverConfig,
YogaDriverServerContext,
} from '@graphql-yoga/nestjs';
import { GraphQLSchema, GraphQLError } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
import { TokenService } from 'src/core/auth/services/token.service';
import { CoreModule } from 'src/core/core.module';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { WorkspaceFactory } from 'src/workspace/workspace.factory';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/global-exception-handler.util';
import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { useExceptionHandler } from 'src/integrations/exception-handler/hooks/use-exception-handler.hook';
import { User } from 'src/core/user/user.entity';
import { useThrottler } from 'src/integrations/throttler/hooks/use-throttler';
import { CreateContextFactory } from './factories/create-context.factory';
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
user?: User;
workspace?: Workspace;
}
@Injectable()
export class GraphQLConfigService
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
{
constructor(
private readonly createContextFactory: CreateContextFactory,
private readonly tokenService: TokenService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly environmentService: EnvironmentService,
private readonly moduleRef: ModuleRef,
) {}
createGqlOptions(): YogaDriverConfig {
const isDebugMode = this.environmentService.isDebugMode();
const config: YogaDriverConfig = {
context: (context) => this.createContextFactory.create(context),
autoSchemaFile: true,
include: [CoreModule],
conditionalSchema: async (context) => {
let user: User | undefined;
try {
if (!this.tokenService.isTokenPresent(context.req)) {
return new GraphQLSchema({});
}
const data = await this.tokenService.validateToken(context.req);
user = data.user;
return await this.createSchema(context, data.workspace);
} catch (error) {
if (error instanceof UnauthorizedException) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof JsonWebTokenError) {
//mockedUserJWT
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof TokenExpiredError) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
throw handleExceptionAndConvertToGraphQLError(
error,
this.exceptionHandlerService,
user
? {
id: user.id,
email: user.email,
}
: undefined,
);
}
},
resolvers: { JSON: GraphQLJSON },
plugins: [
useThrottler({
ttl: this.environmentService.getApiRateLimitingTtl(),
limit: this.environmentService.getApiRateLimitingLimit(),
identifyFn: (context) => {
return context.user?.id ?? context.req.ip ?? 'anonymous';
},
}),
useExceptionHandler({
exceptionHandlerService: this.exceptionHandlerService,
}),
],
};
if (isDebugMode) {
config.renderGraphiQL = () => {
return renderApolloPlayground();
};
}
return config;
}
async createSchema(
context: YogaDriverServerContext<'express'> & YogaInitialContext,
workspace: Workspace,
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
// Create a new contextId for each request
const contextId = ContextIdFactory.create();
// Register the request in the contextId
this.moduleRef.registerRequestByContextId(context.req, contextId);
// Resolve the WorkspaceFactory for the contextId
const workspaceFactory = await this.moduleRef.resolve(
WorkspaceFactory,
contextId,
{
strict: false,
},
);
return await workspaceFactory.createGraphQLSchema(workspace.id);
}
}

View File

@ -0,0 +1,9 @@
import { YogaDriverServerContext } from '@graphql-yoga/nestjs';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
user?: User;
workspace?: Workspace;
}