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:
@ -8,7 +8,7 @@ import GraphQLJSON from 'graphql-type-json';
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import config from '../../ormconfig';
|
import config from '../../ormconfig';
|
||||||
|
|
||||||
import { UserModule } from './userv2/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import { RefreshTokenModule } from './refresh-token/refresh-token.module';
|
import { RefreshTokenModule } from './refresh-token/refresh-token.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
IDField,
|
IDField,
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
} 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';
|
import { BeforeCreateOneRefreshToken } from './hooks/before-create-one-refresh-token.hook';
|
||||||
|
|
||||||
|
|||||||
28
server/src/coreV2/user/services/user.service.spec.ts
Normal file
28
server/src/coreV2/user/services/user.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
71
server/src/coreV2/user/services/user.service.ts
Normal file
71
server/src/coreV2/user/services/user.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
server/src/coreV2/user/user.auto-resolver-opts.ts
Normal file
39
server/src/coreV2/user/user.auto-resolver-opts.ts
Normal 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],
|
||||||
|
},
|
||||||
|
];
|
||||||
93
server/src/coreV2/user/user.entity.ts
Normal file
93
server/src/coreV2/user/user.entity.ts
Normal 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[];
|
||||||
|
}
|
||||||
27
server/src/coreV2/user/user.module.ts
Normal file
27
server/src/coreV2/user/user.module.ts
Normal 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 {}
|
||||||
108
server/src/coreV2/user/user.resolver.ts
Normal file
108
server/src/coreV2/user/user.resolver.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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[];
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user