Scope server with workspace (#157)

* Rename User to AuthUser to avoid naming conflict with user business entity

* Prevent query by workspace in graphql

* Make full user and workspace object available in graphql resolvers

* Add Seed to create companies and people accross two workspace

* Check workspace on all entities findMany, find, create, update)
This commit is contained in:
Charles Bochet
2023-05-30 20:40:04 +02:00
committed by GitHub
parent 0f9c6dede7
commit 3674365e6f
47 changed files with 380 additions and 483 deletions

View File

@ -3,7 +3,7 @@ import { ObjectType } from '@nestjs/graphql';
import { ID } from '@nestjs/graphql';
import { User } from '../user/user.model';
@ObjectType()
@ObjectType({})
export class RefreshToken {
@Field(() => ID, { nullable: false })
id!: string;

View File

@ -1,5 +1,6 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberCountAggregateInput {
@ -18,7 +19,7 @@ export class WorkspaceMemberCountAggregateInput {
@Field(() => Boolean, { nullable: true })
userId?: true;
@Field(() => Boolean, { nullable: true })
@HideField()
workspaceId?: true;
@Field(() => Boolean, { nullable: true })

View File

@ -1,6 +1,7 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { SortOrder } from '../prisma/sort-order.enum';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberCountOrderByAggregateInput {
@ -19,6 +20,6 @@ export class WorkspaceMemberCountOrderByAggregateInput {
@Field(() => SortOrder, { nullable: true })
userId?: keyof typeof SortOrder;
@Field(() => SortOrder, { nullable: true })
@HideField()
workspaceId?: keyof typeof SortOrder;
}

View File

@ -1,5 +1,6 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberCreateManyInput {
@ -18,6 +19,6 @@ export class WorkspaceMemberCreateManyInput {
@Field(() => String, { nullable: false })
userId!: string;
@Field(() => String, { nullable: false })
@HideField()
workspaceId!: string;
}

View File

@ -1,6 +1,7 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { WorkspaceCreateNestedOneWithoutWorkspaceMemberInput } from '../workspace/workspace-create-nested-one-without-workspace-member.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberCreateWithoutUserInput {
@ -16,8 +17,6 @@ export class WorkspaceMemberCreateWithoutUserInput {
@Field(() => Date, { nullable: true })
deletedAt?: Date | string;
@Field(() => WorkspaceCreateNestedOneWithoutWorkspaceMemberInput, {
nullable: false,
})
@HideField()
workspace!: WorkspaceCreateNestedOneWithoutWorkspaceMemberInput;
}

View File

@ -2,6 +2,7 @@ import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { UserCreateNestedOneWithoutWorkspaceMemberInput } from '../user/user-create-nested-one-without-workspace-member.input';
import { WorkspaceCreateNestedOneWithoutWorkspaceMemberInput } from '../workspace/workspace-create-nested-one-without-workspace-member.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberCreateInput {
@ -22,8 +23,6 @@ export class WorkspaceMemberCreateInput {
})
user!: UserCreateNestedOneWithoutWorkspaceMemberInput;
@Field(() => WorkspaceCreateNestedOneWithoutWorkspaceMemberInput, {
nullable: false,
})
@HideField()
workspace!: WorkspaceCreateNestedOneWithoutWorkspaceMemberInput;
}

View File

@ -1,5 +1,6 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberMaxAggregateInput {
@ -18,6 +19,6 @@ export class WorkspaceMemberMaxAggregateInput {
@Field(() => Boolean, { nullable: true })
userId?: true;
@Field(() => Boolean, { nullable: true })
@HideField()
workspaceId?: true;
}

View File

@ -1,6 +1,7 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { SortOrder } from '../prisma/sort-order.enum';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberMaxOrderByAggregateInput {
@ -19,6 +20,6 @@ export class WorkspaceMemberMaxOrderByAggregateInput {
@Field(() => SortOrder, { nullable: true })
userId?: keyof typeof SortOrder;
@Field(() => SortOrder, { nullable: true })
@HideField()
workspaceId?: keyof typeof SortOrder;
}

View File

@ -1,5 +1,6 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberMinAggregateInput {
@ -18,6 +19,6 @@ export class WorkspaceMemberMinAggregateInput {
@Field(() => Boolean, { nullable: true })
userId?: true;
@Field(() => Boolean, { nullable: true })
@HideField()
workspaceId?: true;
}

View File

@ -1,6 +1,7 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { SortOrder } from '../prisma/sort-order.enum';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberMinOrderByAggregateInput {
@ -19,6 +20,6 @@ export class WorkspaceMemberMinOrderByAggregateInput {
@Field(() => SortOrder, { nullable: true })
userId?: keyof typeof SortOrder;
@Field(() => SortOrder, { nullable: true })
@HideField()
workspaceId?: keyof typeof SortOrder;
}

View File

@ -1,6 +1,7 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { SortOrder } from '../prisma/sort-order.enum';
import { HideField } from '@nestjs/graphql';
import { WorkspaceMemberCountOrderByAggregateInput } from './workspace-member-count-order-by-aggregate.input';
import { WorkspaceMemberMaxOrderByAggregateInput } from './workspace-member-max-order-by-aggregate.input';
import { WorkspaceMemberMinOrderByAggregateInput } from './workspace-member-min-order-by-aggregate.input';
@ -22,7 +23,7 @@ export class WorkspaceMemberOrderByWithAggregationInput {
@Field(() => SortOrder, { nullable: true })
userId?: keyof typeof SortOrder;
@Field(() => SortOrder, { nullable: true })
@HideField()
workspaceId?: keyof typeof SortOrder;
@Field(() => WorkspaceMemberCountOrderByAggregateInput, { nullable: true })

View File

@ -1,6 +1,7 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { SortOrder } from '../prisma/sort-order.enum';
import { HideField } from '@nestjs/graphql';
import { UserOrderByWithRelationInput } from '../user/user-order-by-with-relation.input';
import { WorkspaceOrderByWithRelationInput } from '../workspace/workspace-order-by-with-relation.input';
@ -21,12 +22,12 @@ export class WorkspaceMemberOrderByWithRelationInput {
@Field(() => SortOrder, { nullable: true })
userId?: keyof typeof SortOrder;
@Field(() => SortOrder, { nullable: true })
@HideField()
workspaceId?: keyof typeof SortOrder;
@Field(() => UserOrderByWithRelationInput, { nullable: true })
user?: UserOrderByWithRelationInput;
@Field(() => WorkspaceOrderByWithRelationInput, { nullable: true })
@HideField()
workspace?: WorkspaceOrderByWithRelationInput;
}

View File

@ -3,6 +3,7 @@ import { InputType } from '@nestjs/graphql';
import { StringWithAggregatesFilter } from '../prisma/string-with-aggregates-filter.input';
import { DateTimeWithAggregatesFilter } from '../prisma/date-time-with-aggregates-filter.input';
import { DateTimeNullableWithAggregatesFilter } from '../prisma/date-time-nullable-with-aggregates-filter.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberScalarWhereWithAggregatesInput {
@ -36,6 +37,6 @@ export class WorkspaceMemberScalarWhereWithAggregatesInput {
@Field(() => StringWithAggregatesFilter, { nullable: true })
userId?: StringWithAggregatesFilter;
@Field(() => StringWithAggregatesFilter, { nullable: true })
@HideField()
workspaceId?: StringWithAggregatesFilter;
}

View File

@ -3,6 +3,7 @@ import { InputType } from '@nestjs/graphql';
import { StringFilter } from '../prisma/string-filter.input';
import { DateTimeFilter } from '../prisma/date-time-filter.input';
import { DateTimeNullableFilter } from '../prisma/date-time-nullable-filter.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberScalarWhereInput {
@ -30,6 +31,6 @@ export class WorkspaceMemberScalarWhereInput {
@Field(() => StringFilter, { nullable: true })
userId?: StringFilter;
@Field(() => StringFilter, { nullable: true })
@HideField()
workspaceId?: StringFilter;
}

View File

@ -1,5 +1,6 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUncheckedCreateWithoutUserInput {
@ -15,6 +16,6 @@ export class WorkspaceMemberUncheckedCreateWithoutUserInput {
@Field(() => Date, { nullable: true })
deletedAt?: Date | string;
@Field(() => String, { nullable: false })
@HideField()
workspaceId!: string;
}

View File

@ -1,5 +1,6 @@
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUncheckedCreateInput {
@ -18,6 +19,6 @@ export class WorkspaceMemberUncheckedCreateInput {
@Field(() => String, { nullable: false })
userId!: string;
@Field(() => String, { nullable: false })
@HideField()
workspaceId!: string;
}

View File

@ -3,6 +3,7 @@ import { InputType } from '@nestjs/graphql';
import { StringFieldUpdateOperationsInput } from '../prisma/string-field-update-operations.input';
import { DateTimeFieldUpdateOperationsInput } from '../prisma/date-time-field-update-operations.input';
import { NullableDateTimeFieldUpdateOperationsInput } from '../prisma/nullable-date-time-field-update-operations.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUncheckedUpdateManyInput {
@ -21,6 +22,6 @@ export class WorkspaceMemberUncheckedUpdateManyInput {
@Field(() => StringFieldUpdateOperationsInput, { nullable: true })
userId?: StringFieldUpdateOperationsInput;
@Field(() => StringFieldUpdateOperationsInput, { nullable: true })
@HideField()
workspaceId?: StringFieldUpdateOperationsInput;
}

View File

@ -3,6 +3,7 @@ import { InputType } from '@nestjs/graphql';
import { StringFieldUpdateOperationsInput } from '../prisma/string-field-update-operations.input';
import { DateTimeFieldUpdateOperationsInput } from '../prisma/date-time-field-update-operations.input';
import { NullableDateTimeFieldUpdateOperationsInput } from '../prisma/nullable-date-time-field-update-operations.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUncheckedUpdateWithoutUserInput {
@ -18,6 +19,6 @@ export class WorkspaceMemberUncheckedUpdateWithoutUserInput {
@Field(() => NullableDateTimeFieldUpdateOperationsInput, { nullable: true })
deletedAt?: NullableDateTimeFieldUpdateOperationsInput;
@Field(() => StringFieldUpdateOperationsInput, { nullable: true })
@HideField()
workspaceId?: StringFieldUpdateOperationsInput;
}

View File

@ -3,6 +3,7 @@ import { InputType } from '@nestjs/graphql';
import { StringFieldUpdateOperationsInput } from '../prisma/string-field-update-operations.input';
import { DateTimeFieldUpdateOperationsInput } from '../prisma/date-time-field-update-operations.input';
import { NullableDateTimeFieldUpdateOperationsInput } from '../prisma/nullable-date-time-field-update-operations.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUncheckedUpdateInput {
@ -21,6 +22,6 @@ export class WorkspaceMemberUncheckedUpdateInput {
@Field(() => StringFieldUpdateOperationsInput, { nullable: true })
userId?: StringFieldUpdateOperationsInput;
@Field(() => StringFieldUpdateOperationsInput, { nullable: true })
@HideField()
workspaceId?: StringFieldUpdateOperationsInput;
}

View File

@ -4,6 +4,7 @@ import { StringFieldUpdateOperationsInput } from '../prisma/string-field-update-
import { DateTimeFieldUpdateOperationsInput } from '../prisma/date-time-field-update-operations.input';
import { NullableDateTimeFieldUpdateOperationsInput } from '../prisma/nullable-date-time-field-update-operations.input';
import { WorkspaceUpdateOneRequiredWithoutWorkspaceMemberNestedInput } from '../workspace/workspace-update-one-required-without-workspace-member-nested.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUpdateWithoutUserInput {
@ -19,8 +20,6 @@ export class WorkspaceMemberUpdateWithoutUserInput {
@Field(() => NullableDateTimeFieldUpdateOperationsInput, { nullable: true })
deletedAt?: NullableDateTimeFieldUpdateOperationsInput;
@Field(() => WorkspaceUpdateOneRequiredWithoutWorkspaceMemberNestedInput, {
nullable: true,
})
@HideField()
workspace?: WorkspaceUpdateOneRequiredWithoutWorkspaceMemberNestedInput;
}

View File

@ -5,6 +5,7 @@ import { DateTimeFieldUpdateOperationsInput } from '../prisma/date-time-field-up
import { NullableDateTimeFieldUpdateOperationsInput } from '../prisma/nullable-date-time-field-update-operations.input';
import { UserUpdateOneRequiredWithoutWorkspaceMemberNestedInput } from '../user/user-update-one-required-without-workspace-member-nested.input';
import { WorkspaceUpdateOneRequiredWithoutWorkspaceMemberNestedInput } from '../workspace/workspace-update-one-required-without-workspace-member-nested.input';
import { HideField } from '@nestjs/graphql';
@InputType()
export class WorkspaceMemberUpdateInput {
@ -25,8 +26,6 @@ export class WorkspaceMemberUpdateInput {
})
user?: UserUpdateOneRequiredWithoutWorkspaceMemberNestedInput;
@Field(() => WorkspaceUpdateOneRequiredWithoutWorkspaceMemberNestedInput, {
nullable: true,
})
@HideField()
workspace?: WorkspaceUpdateOneRequiredWithoutWorkspaceMemberNestedInput;
}

View File

@ -3,6 +3,7 @@ import { InputType } from '@nestjs/graphql';
import { StringFilter } from '../prisma/string-filter.input';
import { DateTimeFilter } from '../prisma/date-time-filter.input';
import { DateTimeNullableFilter } from '../prisma/date-time-nullable-filter.input';
import { HideField } from '@nestjs/graphql';
import { UserRelationFilter } from '../user/user-relation-filter.input';
import { WorkspaceRelationFilter } from '../workspace/workspace-relation-filter.input';
@ -32,12 +33,12 @@ export class WorkspaceMemberWhereInput {
@Field(() => StringFilter, { nullable: true })
userId?: StringFilter;
@Field(() => StringFilter, { nullable: true })
@HideField()
workspaceId?: StringFilter;
@Field(() => UserRelationFilter, { nullable: true })
user?: UserRelationFilter;
@Field(() => WorkspaceRelationFilter, { nullable: true })
@HideField()
workspace?: WorkspaceRelationFilter;
}

View File

@ -6,7 +6,7 @@ import { Company } from '../company/company.model';
import { Person } from '../person/person.model';
import { WorkspaceCount } from './workspace-count.output';
@ObjectType()
@ObjectType({})
export class Workspace {
@Field(() => ID, { nullable: false })
id!: string;

View File

@ -3,15 +3,16 @@ import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { CompanyResolver } from './resolvers/company.resolver';
import { UserResolver } from './resolvers/user.resolver';
import { PeopleResolver } from './resolvers/people.resolver';
import { PersonResolver } from './resolvers/person.resolver';
import { PersonRelationsResolver } from './resolvers/relations/people-relations.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 { ConfigService } from '@nestjs/config';
import { AuthModule } from 'src/auth/auth.module';
import { CompanyRelationsResolver } from './resolvers/relations/company-relations.resolver';
import { PrismaModule } from 'src/database/prisma.module';
import { ArgsService } from './resolvers/services/args.service';
@Module({
imports: [
@ -25,9 +26,10 @@ import { PrismaModule } from 'src/database/prisma.module';
],
providers: [
ConfigService,
ArgsService,
CompanyResolver,
PeopleResolver,
PersonResolver,
UserResolver,
CompanyRelationsResolver,

View File

@ -1,37 +1,39 @@
import { Resolver, Query, Args, Mutation } from '@nestjs/graphql';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { UseGuards } from '@nestjs/common';
import { User, UserType } from './decorators/user.decorator';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { PrismaService } from 'src/database/prisma.service';
import { Company } from '../@generated/company/company.model';
import { FindManyCompanyArgs } from '../@generated/company/find-many-company.args';
import { DeleteOneCompanyArgs } from '../@generated/company/delete-one-company.args';
import { UpdateOneCompanyArgs } from '../@generated/company/update-one-company.args';
import { CreateOneCompanyArgs } from '../@generated/company/create-one-company.args';
import { AffectedRows } from '../@generated/prisma/affected-rows.output';
import { DeleteManyCompanyArgs } from '../@generated/company/delete-many-company.args';
import { Workspace } from '@prisma/client';
import { ArgsService } from './services/args.service';
import { CheckWorkspaceOwnership } from 'src/auth/guards/check-workspace-ownership.guard';
@UseGuards(JwtAuthGuard, CheckWorkspaceOwnership)
@Resolver(() => Company)
export class CompanyResolver {
constructor(private readonly prismaService: PrismaService) {}
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@UseGuards(JwtAuthGuard)
@Query(() => [Company])
async companies(@Args() args: FindManyCompanyArgs) {
return this.prismaService.company.findMany(args);
async findManyCompany(
@Args() args: FindManyCompanyArgs,
@AuthWorkspace() workspace: Workspace,
) {
const preparedArgs =
await this.argsService.prepareFindManyArgs<FindManyCompanyArgs>(
args,
workspace,
);
return this.prismaService.company.findMany(preparedArgs);
}
@UseGuards(JwtAuthGuard)
@Mutation(() => Company, {
nullable: true,
})
async deleteOneCompany(
@Args() args: DeleteOneCompanyArgs,
): Promise<Company | null> {
return this.prismaService.company.delete(args);
}
@UseGuards(JwtAuthGuard)
@Mutation(() => Company, {
nullable: true,
})
@ -46,23 +48,6 @@ export class CompanyResolver {
});
}
@UseGuards(JwtAuthGuard)
@Mutation(() => Company, {
nullable: false,
})
async createOneCompany(
@Args() args: CreateOneCompanyArgs,
@User() user: UserType,
): Promise<Company> {
return this.prismaService.company.create({
data: {
...args.data,
...{ workspace: { connect: { id: user.workspaceId } } },
},
});
}
@UseGuards(JwtAuthGuard)
@Mutation(() => AffectedRows, {
nullable: false,
})
@ -73,4 +58,19 @@ export class CompanyResolver {
...args,
});
}
@Mutation(() => Company, {
nullable: false,
})
async createOneCompany(
@Args() args: CreateOneCompanyArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Company> {
return this.prismaService.company.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
});
}
}

View File

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

View File

@ -0,0 +1,10 @@
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,7 +1,6 @@
import { Resolver, Query, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { User, UserType } from './decorators/user.decorator';
import { PrismaService } from 'src/database/prisma.service';
import { Person } from '../@generated/person/person.model';
import { FindManyPersonArgs } from '../@generated/person/find-many-person.args';
@ -9,22 +8,36 @@ import { UpdateOnePersonArgs } from '../@generated/person/update-one-person.args
import { CreateOnePersonArgs } from '../@generated/person/create-one-person.args';
import { AffectedRows } from '../@generated/prisma/affected-rows.output';
import { DeleteManyPersonArgs } from '../@generated/person/delete-many-person.args';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { ArgsService } from './services/args.service';
import { CheckWorkspaceOwnership } from 'src/auth/guards/check-workspace-ownership.guard';
@UseGuards(JwtAuthGuard, CheckWorkspaceOwnership)
@Resolver(() => Person)
export class PeopleResolver {
constructor(private readonly prismaService: PrismaService) {}
export class PersonResolver {
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@UseGuards(JwtAuthGuard)
@Query(() => [Person], {
nullable: false,
})
async people(@Args() args: FindManyPersonArgs): Promise<Person[]> {
async findManyPerson(
@Args() args: FindManyPersonArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Person[]> {
const preparedArgs =
await this.argsService.prepareFindManyArgs<FindManyPersonArgs>(
args,
workspace,
);
return this.prismaService.person.findMany({
...args,
...preparedArgs,
});
}
@UseGuards(JwtAuthGuard)
@Mutation(() => Person, {
nullable: true,
})
@ -34,28 +47,12 @@ export class PeopleResolver {
if (!args.data.company?.connect?.id) {
args.data.company = { disconnect: true };
}
return this.prismaService.person.update({
...args,
});
}
@UseGuards(JwtAuthGuard)
@Mutation(() => Person, {
nullable: false,
})
async createOnePerson(
@Args() args: CreateOnePersonArgs,
@User() user: UserType,
): Promise<Person> {
return this.prismaService.person.create({
data: {
...args.data,
...{ workspace: { connect: { id: user.workspaceId } } },
},
});
}
@UseGuards(JwtAuthGuard)
@Mutation(() => AffectedRows, {
nullable: false,
})
@ -66,4 +63,19 @@ export class PeopleResolver {
...args,
});
}
@Mutation(() => Person, {
nullable: false,
})
async createOnePerson(
@Args() args: CreateOnePersonArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Person> {
return this.prismaService.person.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
});
}
}

View File

@ -0,0 +1,18 @@
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

@ -2,31 +2,39 @@ import { Resolver, Query, Args } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { User } from '../@generated/user/user.model';
import { FindManyUserArgs } from '../@generated/user/find-many-user.args';
import { FindUniqueUserOrThrowArgs } from '../@generated/user/find-unique-user-or-throw.args';
import { Workspace } from '@prisma/client';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { ArgsService } from './services/args.service';
import { CheckWorkspaceOwnership } from 'src/auth/guards/check-workspace-ownership.guard';
@UseGuards(JwtAuthGuard, CheckWorkspaceOwnership)
@Resolver(() => User)
export class UserResolver {
constructor(private readonly prismaService: PrismaService) {}
constructor(
private readonly prismaService: PrismaService,
private readonly argsService: ArgsService,
) {}
@UseGuards(JwtAuthGuard)
@Query(() => [User], {
nullable: false,
})
async users(@Args() args: FindManyUserArgs): Promise<User[]> {
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,
});
}
@UseGuards(JwtAuthGuard)
@Query(() => User, {
nullable: false,
})
async user(@Args() args: FindUniqueUserOrThrowArgs): Promise<User | null> {
return await this.prismaService.user.findUnique({
...args,
});
}
}

View File

@ -8,8 +8,9 @@ import {
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request, Response } from 'express';
import { Response } from 'express';
import { AuthService } from './services/auth.service';
import { GoogleRequest } from './strategies/google.auth.strategy';
@Controller('auth/google')
export class GoogleAuthController {
@ -24,10 +25,8 @@ export class GoogleAuthController {
@Get('redirect')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
const user = await this.authService.upsertUser(
req.user as { firstName: string; lastName: string; email: string },
);
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
const user = await this.authService.upsertUser(req.user);
if (!user) {
throw new HttpException(

View File

@ -0,0 +1,85 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Request } from 'express';
import { PrismaService } from 'src/database/prisma.service';
type OperationEntity = {
operation?: string;
entity?: string;
};
@Injectable()
export class CheckWorkspaceOwnership implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const { operation, entity } = this.fetchOperationAndEntity(request);
const variables = request.body.variables;
const workspace = await request.workspace;
if (!entity || !operation) {
return false;
}
if (operation === 'updateOne') {
const object = await this.prismaService[entity].findUniqueOrThrow({
where: { id: variables.id },
});
if (!object) {
throw new HttpException(
{ reason: 'Record not found' },
HttpStatus.NOT_FOUND,
);
}
if (object.workspaceId !== workspace.id) {
throw new HttpException(
{ reason: 'Record not found' },
HttpStatus.NOT_FOUND,
);
}
return true;
}
if (operation === 'deleteMany') {
// TODO: write this logic
return true;
}
if (operation === 'findMany') {
return true;
}
if (operation === 'createOne') {
return true;
}
return false;
}
private fetchOperationAndEntity(request: Request): OperationEntity {
if (!request.body.operationName) {
return { operation: undefined, entity: undefined };
}
const regex =
/(updateOne|deleteMany|createOne|findMany)(Person|Company|User)/i;
const match = request.body.query.match(regex);
if (match) {
return {
operation: match[1],
entity: match[2].toLowerCase(),
};
}
return { operation: undefined, entity: undefined };
}
}

View File

@ -1,6 +1,8 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
@ -8,12 +10,15 @@ 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> {
@ -24,10 +29,34 @@ export class JwtAuthGuard implements CanActivate {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get('JWT_SECRET'),
});
request['user'] = payload;
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();
}

View File

@ -8,6 +8,12 @@ import { RefreshTokenRepository } from 'src/entities/refresh-token/refresh-token
import { v4 } from 'uuid';
import { RefreshToken, User } from '@prisma/client';
export type UserPayload = {
firstName: string;
lastName: string;
email: string;
};
@Injectable()
export class AuthService {
constructor(
@ -18,11 +24,7 @@ export class AuthService {
private refreshTokenRepository: RefreshTokenRepository,
) {}
async upsertUser(rawUser: {
firstName: string;
lastName: string;
email: string;
}) {
async upsertUser(rawUser: UserPayload) {
if (!rawUser.email) {
throw new HttpException(
{ reason: 'Email is missing' },

View File

@ -3,6 +3,11 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
export type GoogleRequest = Request & {
user: { firstName: string; lastName: string; email: string };
};
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {

View File

@ -24,7 +24,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
});
}
async validate(payload: JwtPayload) {
async validate(payload: JwtPayload): Promise<JwtPayload> {
return { userId: payload.userId, workspaceId: payload.workspaceId };
}
}

View File

@ -35,6 +35,7 @@ model User {
@@map("users")
}
/// @TypeGraphQL.omit(input: true)
model Workspace {
id String @id
createdAt DateTime @default(now())
@ -57,7 +58,9 @@ model WorkspaceMember {
deletedAt DateTime?
userId String @unique
user User @relation(fields: [userId], references: [id])
/// @TypeGraphQL.omit(input: true)
workspaceId String
/// @TypeGraphQL.omit(input: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
@@map("workspace_members")
@ -105,6 +108,7 @@ model Person {
@@map("people")
}
/// @TypeGraphQL.omit(input: true)
model RefreshToken {
id String @id
createdAt DateTime @default(now())

View File

@ -147,4 +147,16 @@ export const seedCompanies = async (prisma: PrismaClient) => {
address: '',
},
});
await prisma.company.upsert({
where: { id: 'a674fa6c-1455-4c57-afaf-dd5dc086361e' },
update: {},
create: {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361e',
name: 'Instagram',
domainName: 'instagram.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
address: '',
},
});
};

View File

@ -209,4 +209,19 @@ export const seedPeople = async (prisma: PrismaClient) => {
email: 'louis.duss@google.com',
},
});
await prisma.person.upsert({
where: { id: '240da2ec-2d40-4e49-8df4-9c6a049190dh' },
update: {},
create: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190dh',
firstname: 'Lorie',
lastname: 'Vladim',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
phone: '+33788901235',
city: 'Seattle',
companyId: 'a674fa6c-1455-4c57-afaf-dd5dc086361e',
email: 'lorie.vladim@google.com',
},
});
};