chore(server): convert User model to TypeORM entity (#2499)

* chore: convert basic RefreshToken model to TypeORM entity

Co-authored-by: v1b3m <vibenjamin6@gmail.com>

* Fix import

Co-authored-by: v1b3m <vibenjamin6@gmail.com>

* Refactor according to review

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>

* Refactor according to review

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>

* Refactor according to review

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>

---------

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
This commit is contained in:
gitstart-twenty
2023-11-14 22:00:31 +05:45
committed by GitHub
parent 970d9ee7f6
commit 1f49ed2acf
13 changed files with 368 additions and 105 deletions

View File

@ -8,7 +8,7 @@ import GraphQLJSON from 'graphql-type-json';
// eslint-disable-next-line no-restricted-imports
import config from '../../ormconfig';
import { UserModule } from './userv2/user.module';
import { UserModule } from './user/user.module';
import { RefreshTokenModule } from './refresh-token/refresh-token.module';
@Module({

View File

@ -15,7 +15,7 @@ import {
IDField,
} from '@ptc-org/nestjs-query-graphql';
import { User } from 'src/coreV2/userv2/user.entity';
import { User } from 'src/coreV2/user/user.entity';
import { BeforeCreateOneRefreshToken } from './hooks/before-create-one-refresh-token.hook';

View File

@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from 'src/coreV2/user/user.entity';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useValue: {},
},
],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,71 @@
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { assert } from 'src/utils/assert';
import { User } from 'src/coreV2/user/user.entity';
export class UserService extends TypeOrmQueryService<User> {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super(userRepository);
}
async deleteUser({
workspaceId: _workspaceId,
userId,
}: {
workspaceId: string;
userId: string;
}) {
// const { workspaceMember, refreshToken } = this.prismaService.client;
// const queryRunner =
// this.userRepository.manager.connection.createQueryRunner();
// await queryRunner.connect();
const user = await this.userRepository.findBy({ id: userId });
assert(user, 'User not found');
// FIXME: Workspace entity is not defined
// const workspace = await queryRunner.manager.findOneBy(Workspace, {
// id: userId,
// });
// assert(workspace, 'Workspace not found');
// const workSpaceMembers = await queryRunner.manager.findBy(WorkspaceMember, {
// workspaceId,
// });
// const isLastMember =
// workSpaceMembers.length === 1 && workSpaceMembers[0].userId === userId;
// if (isLastMember) {
// // FIXME: workspaceService is not defined
// await this.workspaceService.deleteWorkspace({
// workspaceId,
// });
// } else {
// await queryRunner.startTransaction();
// // FIXME: these other entities are not defined
// await queryRunner.manager.delete(WorkspaceMember, {
// userId,
// });
// await queryRunner.manager.delete(RefreshToken, {
// userId,
// });
// await queryRunner.manager.delete(User, {
// id: userId,
// });
// await queryRunner.commitTransaction();
// await queryRunner.release();
// }
return user;
}
}

View File

@ -0,0 +1,39 @@
import { SortDirection } from '@ptc-org/nestjs-query-core';
import {
AutoResolverOpts,
ReadResolverOpts,
PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { User } from './user.entity';
export const userAutoResolverOpts: AutoResolverOpts<
any,
any,
unknown,
unknown,
ReadResolverOpts<any>,
PagingStrategies
>[] = [
{
EntityClass: User,
DTOClass: User,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
read: {
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
},
create: {
many: { disabled: true },
one: { disabled: true },
},
update: {
many: { disabled: true },
one: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
},
];

View File

@ -0,0 +1,93 @@
import { ID, Field, ObjectType } from '@nestjs/graphql';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { GraphQLJSONObject } from 'graphql-type-json';
import { RefreshToken } from 'src/coreV2/refresh-token/refresh-token.entity';
@Entity('users')
@ObjectType('user')
// @Authorize({
// authorize: (context: any) => ({
// // FIXME: We do not have this relation in the database
// workspaceMember: {
// workspaceId: { eq: context?.req?.user?.workspace?.id },
// },
// }),
// })
export class User {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ nullable: true })
firstName: string;
@Field()
@Column({ nullable: true })
lastName: string;
@Field()
@Column()
email: string;
@Field()
@Column({ default: false })
emailVerified: boolean;
@Field()
@Column({ nullable: true })
avatarUrl: string;
@Field()
@Column()
locale: string;
@Field({ nullable: true })
@Column({ nullable: true })
phoneNumber: string;
@Field({ nullable: true })
@Column({ nullable: true })
lastSeen: Date;
@Field({ nullable: true })
@Column({ default: false })
disabled: boolean;
@Field({ nullable: true })
@Column({ nullable: true })
passwordHash: string;
@Field(() => GraphQLJSONObject, { nullable: true })
@Column({ type: 'json', nullable: true })
metadata: Record<string, any>;
@Field()
@Column({ default: false })
canImpersonate: boolean;
@Field()
@CreateDateColumn({ type: 'timestamp with time zone' })
createdAt: Date;
@Field()
@UpdateDateColumn({ type: 'timestamp with time zone' })
updatedAt: Date;
@Field({ nullable: true })
@Column({ nullable: true })
deletedAt: Date;
@OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user)
refreshTokens: RefreshToken[];
}

View File

@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AbilityModule } from 'src/ability/ability.module';
import { FileModule } from 'src/core/file/file.module';
import { User } from './user.entity';
import { UserResolver } from './user.resolver';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
import { UserService } from './services/user.service';
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([User])],
services: [UserService],
resolvers: userAutoResolverOpts,
}),
AbilityModule,
FileModule,
],
providers: [UserService, UserResolver],
})
export class UserModule {}

View File

@ -0,0 +1,108 @@
import {
Resolver,
Query,
Args,
Parent,
ResolveField,
Mutation,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import crypto from 'crypto';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Workspace } from '@prisma/client';
import { SupportDriver } from 'src/integrations/environment/interfaces/support.interface';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import { DeleteUserAbilityHandler } from 'src/ability/handlers/user.ability-handler';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { assert } from 'src/utils/assert';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { User } from './user.entity';
import { UserService } from './services/user.service';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
const hmac = crypto.createHmac('sha256', key);
return hmac.update(email).digest('hex');
};
@UseGuards(JwtAuthGuard)
@Resolver(() => User)
export class UserResolver {
constructor(
private readonly userService: UserService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
) {}
@Query(() => User)
async currentUser(@AuthUser() { id }: User) {
const user = await this.userService.findById(id);
assert(user, 'User not found');
return user;
}
@ResolveField(() => String, {
nullable: false,
})
displayName(@Parent() parent: User): string {
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
}
@ResolveField(() => String, {
nullable: true,
})
supportUserHash(@Parent() parent: User): string | null {
if (this.environmentService.getSupportDriver() !== SupportDriver.Front) {
return null;
}
const key = this.environmentService.getSupportFrontHMACKey();
return getHMACKey(parent.email, key);
}
@Mutation(() => String)
async uploadProfilePicture(
@AuthUser() { id }: User,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const fileFolder = FileFolder.ProfilePicture;
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename,
mimeType: mimetype,
fileFolder,
});
await this.userService.updateOne(id, {
avatarUrl: paths[0],
});
return paths[0];
}
@Mutation(() => User)
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteUserAbilityHandler)
async deleteUserAccount(
@AuthUser() { id: userId }: User,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.userService.deleteUser({ userId, workspaceId });
}
}

View File

@ -1,19 +0,0 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class TUser {
@Field(() => ID, { nullable: false })
id: number;
@Field(() => String, { nullable: true })
firstName: string | null;
@Field(() => String, { nullable: true })
lastName: string | null;
@Field(() => String, { nullable: false })
email: string;
@Field(() => Boolean, { nullable: false, defaultValue: false })
emailVerified: boolean;
}

View File

@ -1,24 +0,0 @@
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { RefreshToken } from 'src/coreV2/refresh-token/refresh-token.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
email: string;
@Column({ default: false })
emailVerified: boolean;
@OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user)
refreshTokens: RefreshToken[];
}

View File

@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AbilityModule } from 'src/ability/ability.module';
import { User } from './user.entity';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
@Module({
imports: [TypeOrmModule.forFeature([User]), AbilityModule],
providers: [UserService, UserResolver],
})
export class UserModule {}

View File

@ -1,28 +0,0 @@
import { Resolver, Query } from '@nestjs/graphql';
import { UseFilters, UseGuards } from '@nestjs/common';
import { ExceptionFilter } from 'src/filters/exception.filter';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import { ReadUserAbilityHandler } from 'src/ability/handlers/user.ability-handler';
import { TUser } from './user.dto';
import { UserService } from './user.service';
import { User } from './user.entity';
@UseGuards(JwtAuthGuard)
@Resolver(() => TUser)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@UseFilters(ExceptionFilter)
@Query(() => [TUser], {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(ReadUserAbilityHandler)
async findManyUserV2(): Promise<Partial<User>[]> {
return this.userService.findAll();
}
}

View File

@ -1,18 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findAll() {
return this.usersRepository.find();
}
}