chore: refacto NestJS in modules (#308)

* chore: wip refacto in modules

* fix: rollback port

* fix: jwt guard in wrong folder

* chore: rename folder exception-filter in filters

* fix: tests are running

* fix: excessive stack depth comparing types

* fix: auth issue

* chore: move createUser in UserService

* fix: test

* fix: guards

* fix: jwt guard don't handle falsy user
This commit is contained in:
Jérémy M 2023-06-16 10:38:11 +02:00 committed by GitHub
parent 5921c7f11d
commit 2cd081234f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1084 changed files with 2251 additions and 758 deletions

1
server/.nvmrc Normal file
View File

@ -0,0 +1 @@
18.10.0

18
server/jest.config.ts Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
clearMocks: true,
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/src/prisma-mock/jest-prisma-singleton.ts'],
moduleFileExtensions: ['js', 'json', 'ts'],
moduleNameMapper: {
'^src/(.*)': '<rootDir>/src/$1',
},
rootDir: './',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
};

View File

@ -43,6 +43,7 @@
"graphql": "^16.6.0",
"graphql-type-json": "^0.3.2",
"jest-mock-extended": "^3.0.4",
"jsonwebtoken": "^9.0.0",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
@ -60,7 +61,9 @@
"@types/jest": "28.1.8",
"@types/node": "^16.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
@ -78,23 +81,6 @@
"tsconfig-paths": "4.1.0",
"typescript": "^4.9.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"prisma": {
"schema": "src/database/schema.prisma",
"seed": "ts-node src/database/seeds/index.ts"

View File

@ -1,68 +0,0 @@
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ConfigService } from '@nestjs/config';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { AuthModule } from 'src/auth/auth.module';
import { PrismaModule } from 'src/database/prisma.module';
import { ArgsService } from './resolvers/services/args.service';
import { CompanyResolver } from './resolvers/company.resolver';
import { UserResolver } from './resolvers/user.resolver';
import { PersonResolver } from './resolvers/person.resolver';
import { CommentResolver } from './resolvers/comment.resolver';
import { CommentThreadResolver } from './resolvers/comment-thread.resolver';
import { PipelineResolver } from './resolvers/pipeline.resolver';
import { PipelineStageResolver } from './resolvers/pipeline-stage.resolver';
import { PersonRelationsResolver } from './resolvers/relations/person-relations.resolver';
import { UserRelationsResolver } from './resolvers/relations/user-relations.resolver';
import { WorkspaceMemberRelationsResolver } from './resolvers/relations/workspace-member-relations.resolver';
import { CompanyRelationsResolver } from './resolvers/relations/company-relations.resolver';
import { CommentThreadRelationsResolver } from './resolvers/relations/comment-thread-relations.resolver';
import { PipelineRelationsResolver } from './resolvers/relations/pipeline-relations.resolver';
import { GraphQLError } from 'graphql';
import { CommentRelationsResolver } from './resolvers/relations/comment-relations.resolver';
import { PipelineProgressResolver } from './resolvers/pipeline-progress.resolver';
import { PipelineStageRelationsResolver } from './resolvers/relations/pipeline-stage-relations.resolver';
import { PipelineProgressRelationsResolver } from './resolvers/relations/pipeline-progress-relations.resolver';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
context: ({ req }) => ({ req }),
driver: ApolloDriver,
autoSchemaFile: true,
formatError: (error: GraphQLError) => {
error.extensions.stacktrace = undefined;
return error;
},
}),
AuthModule,
PrismaModule,
],
providers: [
ConfigService,
ArgsService,
CompanyResolver,
PersonResolver,
UserResolver,
CommentResolver,
CommentThreadResolver,
PipelineResolver,
PipelineStageResolver,
PipelineProgressResolver,
CompanyRelationsResolver,
CommentRelationsResolver,
PersonRelationsResolver,
UserRelationsResolver,
WorkspaceMemberRelationsResolver,
CommentThreadRelationsResolver,
PipelineRelationsResolver,
PipelineStageRelationsResolver,
PipelineProgressRelationsResolver,
],
})
export class ApiModule {}

View File

@ -1,102 +0,0 @@
import { Resolver, Args, Mutation, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { CommentThread } from '../@generated/comment-thread/comment-thread.model';
import { CreateOneCommentThreadArgs } from '../@generated/comment-thread/create-one-comment-thread.args';
import { CreateOneCommentThreadGuard } from './guards/create-one-comment-thread.guard';
import { FindManyCommentThreadArgs } from '../@generated/comment-thread/find-many-comment-thread.args';
import { ArgsService } from './services/args.service';
import { UpdateOneCommentThreadArgs } from '../@generated/comment-thread/update-one-comment-thread.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => CommentThread)
export class CommentThreadResolver {
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@UseGuards(CreateOneCommentThreadGuard)
@Mutation(() => CommentThread, {
nullable: false,
})
async createOneCommentThread(
@Args() args: CreateOneCommentThreadArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<CommentThread> {
const newCommentData = args.data.comments?.createMany?.data
? args.data.comments?.createMany?.data?.map((comment) => ({
...comment,
...{ workspaceId: workspace.id },
}))
: [];
const createdCommentThread = await this.prismaService.commentThread.create({
data: {
...args.data,
...{ commentThreadTargets: undefined },
...{ comments: { createMany: { data: newCommentData } } },
...{ workspace: { connect: { id: workspace.id } } },
},
});
if (args.data.commentThreadTargets?.createMany?.data) {
await this.prismaService.commentThreadTarget.createMany({
data: args.data.commentThreadTargets?.createMany?.data?.map(
(target) => ({
...target,
commentThreadId: args.data.id,
}),
),
skipDuplicates:
args.data.commentThreadTargets?.createMany?.skipDuplicates ?? false,
});
return await this.prismaService.commentThread.update({
where: { id: args.data.id },
data: {
commentThreadTargets: {
connect: args.data.commentThreadTargets?.connect,
},
},
});
}
return createdCommentThread;
}
@Mutation(() => CommentThread, {
nullable: false,
})
async updateOneCommentThread(
@Args() args: UpdateOneCommentThreadArgs,
): Promise<CommentThread> {
const updatedCommentThread = await this.prismaService.commentThread.update({
data: args.data,
where: args.where,
});
return updatedCommentThread;
}
@Query(() => [CommentThread])
async findManyCommentThreads(
@Args() args: FindManyCommentThreadArgs,
@AuthWorkspace() workspace: Workspace,
) {
const preparedArgs =
await this.argsService.prepareFindManyArgs<FindManyCommentThreadArgs>(
args,
workspace,
);
const result = await this.prismaService.commentThread.findMany(
preparedArgs,
);
return result;
}
}

View File

@ -1,32 +0,0 @@
import { Resolver, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { CreateOneCommentArgs } from '../@generated/comment/create-one-comment.args';
import { Comment } from '../@generated/comment/comment.model';
import { CreateOneCommentGuard } from './guards/create-one-comment.guard';
import { Prisma } from '@prisma/client';
@UseGuards(JwtAuthGuard)
@Resolver(() => Comment)
export class CommentResolver {
constructor(private readonly prismaService: PrismaService) {}
@UseGuards(CreateOneCommentGuard)
@Mutation(() => Comment, {
nullable: false,
})
async createOneComment(
@Args() args: CreateOneCommentArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Comment> {
return this.prismaService.comment.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
} satisfies CreateOneCommentArgs as Prisma.CommentCreateArgs);
}
}

View File

@ -1,10 +0,0 @@
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const AuthUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const gqlContext = GqlExecutionContext.create(ctx);
const request = gqlContext.getContext().req;
return request.user;
},
);

View File

@ -1,10 +0,0 @@
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const AuthWorkspace = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const gqlContext = GqlExecutionContext.create(ctx);
const request = gqlContext.getContext().req;
return request.workspace;
},
);

View File

@ -1,73 +0,0 @@
import { Resolver, Args, Query, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { ArgsService } from './services/args.service';
import { FindManyPipelineProgressArgs } from '../@generated/pipeline-progress/find-many-pipeline-progress.args';
import { PipelineProgress } from '../@generated/pipeline-progress/pipeline-progress.model';
import { UpdateOnePipelineProgressArgs } from '../@generated/pipeline-progress/update-one-pipeline-progress.args';
import { Prisma } from '@prisma/client';
import { AffectedRows } from '../@generated/prisma/affected-rows.output';
import { DeleteManyPipelineProgressArgs } from '../@generated/pipeline-progress/delete-many-pipeline-progress.args';
import { CreateOnePipelineProgressArgs } from '../@generated/pipeline-progress/create-one-pipeline-progress.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => PipelineProgress)
export class PipelineProgressResolver {
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@Query(() => [PipelineProgress])
async findManyPipelineProgress(
@Args() args: FindManyPipelineProgressArgs,
@AuthWorkspace() workspace: Workspace,
) {
const preparedArgs =
await this.argsService.prepareFindManyArgs<FindManyPipelineProgressArgs>(
args,
workspace,
);
return this.prismaService.pipelineProgress.findMany(preparedArgs);
}
@Mutation(() => PipelineProgress, {
nullable: true,
})
async updateOnePipelineProgress(
@Args() args: UpdateOnePipelineProgressArgs,
): Promise<PipelineProgress | null> {
return this.prismaService.pipelineProgress.update({
...args,
} satisfies UpdateOnePipelineProgressArgs as Prisma.PipelineProgressUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
async deleteManyPipelineProgress(
@Args() args: DeleteManyPipelineProgressArgs,
): Promise<AffectedRows> {
return this.prismaService.pipelineProgress.deleteMany({
...args,
});
}
@Mutation(() => PipelineProgress, {
nullable: false,
})
async createOnePipelineProgress(
@Args() args: CreateOnePipelineProgressArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<PipelineProgress> {
return this.prismaService.pipelineProgress.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
} satisfies CreateOnePipelineProgressArgs as Prisma.PipelineProgressCreateArgs);
}
}

View File

@ -1,31 +0,0 @@
import { Resolver, Args, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { PipelineStage } from '../@generated/pipeline-stage/pipeline-stage.model';
import { FindManyPipelineStageArgs } from '../@generated/pipeline-stage/find-many-pipeline-stage.args';
import { ArgsService } from './services/args.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => PipelineStage)
export class PipelineStageResolver {
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@Query(() => [PipelineStage])
async findManyPipelineStage(
@Args() args: FindManyPipelineStageArgs,
@AuthWorkspace() workspace: Workspace,
) {
const preparedArgs =
await this.argsService.prepareFindManyArgs<FindManyPipelineStageArgs>(
args,
workspace,
);
return this.prismaService.pipelineStage.findMany(preparedArgs);
}
}

View File

@ -1,31 +0,0 @@
import { Resolver, Args, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { Pipeline } from '../@generated/pipeline/pipeline.model';
import { FindManyPipelineArgs } from '../@generated/pipeline/find-many-pipeline.args';
import { ArgsService } from './services/args.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => Pipeline)
export class PipelineResolver {
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@Query(() => [Pipeline])
async findManyPipeline(
@Args() args: FindManyPipelineArgs,
@AuthWorkspace() workspace: Workspace,
) {
const preparedArgs =
await this.argsService.prepareFindManyArgs<FindManyPipelineArgs>(
args,
workspace,
);
return this.prismaService.pipeline.findMany(preparedArgs);
}
}

View File

@ -1,18 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Workspace } from '@prisma/client';
type FindManyArgsType = { where?: object; orderBy?: object };
@Injectable()
export class ArgsService {
async prepareFindManyArgs<T extends FindManyArgsType>(
args: T,
workspace: Workspace,
): Promise<T> {
args.where = {
...args.where,
...{ workspace: { is: { id: { equals: workspace.id } } } },
};
return args;
}
}

View File

@ -1,37 +0,0 @@
import { Resolver, Query, Args } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
import { UseFilters, UseGuards } from '@nestjs/common';
import { User } from '../@generated/user/user.model';
import { FindManyUserArgs } from '../@generated/user/find-many-user.args';
import { Workspace } from '@prisma/client';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { ExceptionFilter } from './exception-filters/exception.filter';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
@UseGuards(JwtAuthGuard)
@Resolver(() => User)
export class UserResolver {
constructor(private readonly prismaService: PrismaService) {}
@UseFilters(ExceptionFilter)
@Query(() => [User], {
nullable: false,
})
async findManyUser(
@Args() args: FindManyUserArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<User[]> {
args.where = {
...args.where,
...{
workspaceMember: {
is: { workspace: { is: { id: { equals: workspace.id } } } },
},
},
};
return await this.prismaService.user.findMany({
...args,
});
}
}

View File

@ -1,14 +1,32 @@
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { AppService } from './app.service';
import { HealthController } from './health.controller';
import { TerminusModule } from '@nestjs/terminus';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import { ApiModule } from './api/api.module';
import { CoreModule } from './core/core.module';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { GraphQLError } from 'graphql';
import { PrismaModule } from './database/prisma.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [ConfigModule.forRoot({}), TerminusModule, AuthModule, ApiModule],
controllers: [HealthController],
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
context: ({ req }) => ({ req }),
driver: ApolloDriver,
autoSchemaFile: true,
formatError: (error: GraphQLError) => {
error.extensions.stacktrace = undefined;
return error;
},
}),
PrismaModule,
HealthModule,
CoreModule,
],
providers: [AppService],
})
export class AppModule {}

View File

@ -1,70 +0,0 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from 'src/database/prisma.service';
import { JwtPayload } from '../strategies/jwt.auth.strategy';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private prismaService: PrismaService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get('JWT_SECRET'),
});
const user = this.prismaService.user.findUniqueOrThrow({
where: { id: payload.userId },
});
if (!user) {
throw new HttpException(
{ reason: 'User does not exist' },
HttpStatus.FORBIDDEN,
);
}
const workspace = this.prismaService.workspace.findUniqueOrThrow({
where: { id: payload.workspaceId },
});
if (!workspace) {
throw new HttpException(
{ reason: 'Workspace does not exist' },
HttpStatus.FORBIDDEN,
);
}
request.user = user;
request.workspace = workspace;
} catch (exception) {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@ -1,30 +0,0 @@
import { Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
export type JwtPayload = { userId: string; workspaceId: string };
@Injectable()
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(configService: ConfigService) {
const extractJwtFromCookie = (req) => {
let token = null;
if (req && req.cookies) {
token = req.cookies['jwt'];
}
return token;
};
super({
jwtFromRequest: extractJwtFromCookie,
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: JwtPayload): Promise<JwtPayload> {
return { userId: payload.userId, workspaceId: payload.workspaceId };
}
}

Some files were not shown because too many files have changed in this diff Show More