From 633f97b925f75803fb97899ca3d0c8342251f3a8 Mon Sep 17 00:00:00 2001 From: Narek Date: Mon, 1 Apr 2024 00:23:11 +0400 Subject: [PATCH] fix(): fix password hash --- package.json | 12 ++-- src/app.module.ts | 23 +++++- src/common/abstract.entity.ts | 15 ++-- src/common/extended-entity-repository.ts | 27 +++++++ src/decorators/transform.decorators.ts | 2 +- src/decorators/use-dto.decorator.ts | 4 +- src/entity-subscribers/user-subscriber.ts | 71 +++++++++++-------- src/main.ts | 7 -- .../post/commands/create-post.command.ts | 6 +- src/modules/post/post-translation.entity.ts | 4 +- src/modules/post/post.entity.ts | 5 +- src/modules/user/dtos/user.dto.ts | 2 +- src/modules/user/user-settings.entity.ts | 4 +- src/modules/user/user.entity.ts | 4 +- src/modules/user/user.module.ts | 5 +- src/modules/user/user.service.ts | 6 +- src/shared/services/api-config.service.ts | 3 + 17 files changed, 129 insertions(+), 71 deletions(-) create mode 100644 src/common/extended-entity-repository.ts diff --git a/package.json b/package.json index cea9a69..34b4e2e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "awesome-nestjs-boilerplate", "version": "10.0.0", - "description": "Awesome NestJS Boilerplate, Typescript, Postgres, TypeORM", + "description": "Awesome NestJS Boilerplate, Typescript, Postgres, MikroOrm", "author": "Narek Hakobyan ", "private": true, "license": "MIT", @@ -9,12 +9,12 @@ "build:prod": "nest build", "start:dev": "ts-node src/main.ts", "start:prod": "node dist/main.js", - "typeorm": "typeorm-ts-node-esm", - "migration:generate": "yarn run typeorm migration:generate -d ormconfig", - "migration:create": "yarn run typeorm migration:create -d ormconfig", "new": "hygen new", - "migration:revert": "yarn run typeorm migration:revert", - "schema:drop": "yarn run typeorm schema:drop", + "migration:generate": "mikro-orm migration:creater", + "migration:create": "mikro-orm migration:creater --blank", + "migration:down": "mikro-orm migration:down", + "migration:fresh": "mikro-orm migration:fresh", + "schema:fresh": "mikro-orm schema:fresh", "watch:dev": "ts-node-dev src/main.ts", "debug:dev": "cross-env TS_NODE_CACHE=false ts-node-dev --inspect --ignore '/^src/.*\\.spec\\.ts$/' src/main.ts", "lint": "eslint . --ext .ts", diff --git a/src/app.module.ts b/src/app.module.ts index 662adc5..6982d6e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,7 +2,13 @@ import './boilerplate.polyfill'; import path from 'node:path'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { MikroORM } from '@mikro-orm/core'; +import { MikroOrmMiddleware, MikroOrmModule } from '@mikro-orm/nestjs'; +import type { + MiddlewareConsumer, + NestModule, + OnModuleInit, +} from '@nestjs/common'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ThrottlerModule } from '@nestjs/throttler'; @@ -70,4 +76,17 @@ import { SharedModule } from './shared/shared.module'; ], providers: [], }) -export class AppModule {} +export class AppModule implements NestModule, OnModuleInit { + constructor(private readonly orm: MikroORM) {} + + async onModuleInit(): Promise { + await this.orm.getMigrator().up(); + } + + // for some reason the auth middlewares in profile and article modules are fired before the request context one, + // so they would fail to access contextual EM. by registering the middleware directly in AppModule, we can get + // around this issue + configure(consumer: MiddlewareConsumer) { + consumer.apply(MikroOrmMiddleware).forRoutes('*'); + } +} diff --git a/src/common/abstract.entity.ts b/src/common/abstract.entity.ts index 2008469..b82c0fc 100644 --- a/src/common/abstract.entity.ts +++ b/src/common/abstract.entity.ts @@ -38,10 +38,10 @@ export abstract class AbstractEntity< translations?: Collection; - private dtoClass?: Constructor; + abstract dtoClass?: () => Constructor; toDto(options?: O): DTO { - const dtoClass = this.dtoClass; + const dtoClass = this.dtoClass?.(); if (!dtoClass) { throw new Error( @@ -53,10 +53,15 @@ export abstract class AbstractEntity< } } -export class AbstractTranslationEntity< +export abstract class AbstractTranslationEntity< DTO extends AbstractTranslationDto = AbstractTranslationDto, - O = never, -> extends AbstractEntity { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + O = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Optional = any, +> extends AbstractEntity { + abstract dtoClass?: () => Constructor; + @Enum(() => LanguageCode) languageCode!: LanguageCode; } diff --git a/src/common/extended-entity-repository.ts b/src/common/extended-entity-repository.ts new file mode 100644 index 0000000..599696e --- /dev/null +++ b/src/common/extended-entity-repository.ts @@ -0,0 +1,27 @@ +import type { AnyEntity, EntityManager } from '@mikro-orm/postgresql'; +import { EntityRepository } from '@mikro-orm/postgresql'; + +export class ExtendedEntityRepository< + // eslint-disable-next-line @typescript-eslint/ban-types + T extends object, +> extends EntityRepository { + persist(entity: AnyEntity | AnyEntity[]): EntityManager { + return this.em.persist(entity); + } + + async persistAndFlush(entity: AnyEntity | AnyEntity[]): Promise { + await this.em.persistAndFlush(entity); + } + + remove(entity: AnyEntity): EntityManager { + return this.em.remove(entity); + } + + async removeAndFlush(entity: AnyEntity): Promise { + await this.em.removeAndFlush(entity); + } + + async flush(): Promise { + return this.em.flush(); + } +} diff --git a/src/decorators/transform.decorators.ts b/src/decorators/transform.decorators.ts index f2b8644..206ff46 100644 --- a/src/decorators/transform.decorators.ts +++ b/src/decorators/transform.decorators.ts @@ -153,5 +153,5 @@ export function S3UrlParser(): PropertyDecorator { } export function PhoneNumberSerializer(): PropertyDecorator { - return Transform((params) => parsePhoneNumber(params.value as string).number); + return Transform((params) => params.value ? parsePhoneNumber(params.value as string).number: undefined); } diff --git a/src/decorators/use-dto.decorator.ts b/src/decorators/use-dto.decorator.ts index b3e8d02..fdb9902 100644 --- a/src/decorators/use-dto.decorator.ts +++ b/src/decorators/use-dto.decorator.ts @@ -1,6 +1,6 @@ -import { type Constructor } from '../types'; +import type {Constructor} from '../types'; -export function UseDto(dtoClass: Constructor): ClassDecorator { +export function UseDto(dtoClass: () => Constructor): ClassDecorator { return (ctor) => { // FIXME make dtoClass function returning dto diff --git a/src/entity-subscribers/user-subscriber.ts b/src/entity-subscribers/user-subscriber.ts index e46502f..69420f8 100644 --- a/src/entity-subscribers/user-subscriber.ts +++ b/src/entity-subscribers/user-subscriber.ts @@ -1,31 +1,40 @@ -// import { -// type EntitySubscriberInterface, -// EventSubscriber, -// type InsertEvent, -// type UpdateEvent, -// } from 'typeorm'; -// -// import { generateHash } from '../common/utils'; -// import { UserEntity } from '../modules/user/user.entity'; -// -// @EventSubscriber() -// export class UserSubscriber implements EntitySubscriberInterface { -// listenTo(): typeof UserEntity { -// return UserEntity; -// } -// -// beforeInsert(event: InsertEvent): void { -// if (event.entity.password) { -// event.entity.password = generateHash(event.entity.password); -// } -// } -// -// beforeUpdate(event: UpdateEvent): void { -// // FIXME check event.databaseEntity.password -// const entity = event.entity as UserEntity; -// -// if (entity.password !== event.databaseEntity.password) { -// entity.password = generateHash(entity.password!); -// } -// } -// } +import type { + EventArgs, + EventSubscriber, + FlushEventArgs, +} from '@mikro-orm/core'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { Injectable } from '@nestjs/common'; + +import { generateHash } from '../common/utils'; +import { UserEntity } from '../modules/user/user.entity'; + +@Injectable() +export class UserSubscriber implements EventSubscriber { + constructor(em: EntityManager) { + em.getEventManager().registerSubscriber(this); + } + + getSubscribedEntities() { + return [UserEntity]; + } + + onFlush(args: FlushEventArgs): void { + for (const changeSet of args.uow.getChangeSets()) { + const changedPassword = changeSet.payload.password; + + if (changedPassword) { + changeSet.entity.password = generateHash(changedPassword); + args.uow.recomputeSingleChangeSet(changeSet.entity); + } + } + } + + beforeUpdate(event: EventArgs): void { + const entity = event.entity; + + if (entity.password !== event.changeSet?.entity.password) { + entity.password = generateHash(entity.password!); + } + } +} diff --git a/src/main.ts b/src/main.ts index d553901..19af25d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -import { MikroORM } from '@mikro-orm/core'; import { ClassSerializerInterceptor, HttpStatus, @@ -61,12 +60,6 @@ export async function bootstrap(): Promise { const configService = app.select(SharedModule).get(ApiConfigService); - const orm = app.get(MikroORM); - const migrator = orm.getMigrator(); - - // Run migrations automatically - await migrator.up(); // This will apply all pending migrations - // only start nats if it is enabled if (configService.natsEnabled) { const natsConfig = configService.natsConfig; diff --git a/src/modules/post/commands/create-post.command.ts b/src/modules/post/commands/create-post.command.ts index 8b838a8..e307987 100644 --- a/src/modules/post/commands/create-post.command.ts +++ b/src/modules/post/commands/create-post.command.ts @@ -1,6 +1,5 @@ import { Collection } from '@mikro-orm/core'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { EntityRepository } from '@mikro-orm/postgresql'; import type { ICommand, ICommandHandler } from '@nestjs/cqrs'; import { CommandHandler } from '@nestjs/cqrs'; import { find } from 'lodash'; @@ -8,6 +7,7 @@ import { find } from 'lodash'; import type { CreatePostDto } from '../dtos/create-post.dto'; import { PostEntity } from '../post.entity'; import { PostTranslationEntity } from '../post-translation.entity'; +import { ExtendedEntityRepository } from '../../../common/extended-entity-repository.ts'; export class CreatePostCommand implements ICommand { constructor( @@ -22,9 +22,9 @@ export class CreatePostHandler { constructor( @InjectRepository(PostEntity) - private postRepository: EntityRepository, + private postRepository: ExtendedEntityRepository, @InjectRepository(PostTranslationEntity) - private postTranslationRepository: EntityRepository, + private postTranslationRepository: ExtendedEntityRepository, ) {} async execute(command: CreatePostCommand) { diff --git a/src/modules/post/post-translation.entity.ts b/src/modules/post/post-translation.entity.ts index 37e58d2..0ddd93f 100644 --- a/src/modules/post/post-translation.entity.ts +++ b/src/modules/post/post-translation.entity.ts @@ -3,11 +3,11 @@ import { Entity, ManyToOne, Property } from '@mikro-orm/core'; import { AbstractTranslationEntity } from '../../common/abstract.entity'; import { PostTranslationDto } from './dtos/post-translation.dto'; import { PostEntity } from './post.entity'; -import { UseDto } from '../../decorators/use-dto.decorator.ts'; @Entity({ tableName: 'post_translations' }) -@UseDto(PostTranslationDto) export class PostTranslationEntity extends AbstractTranslationEntity { + dtoClass = () => PostTranslationDto as any; + @Property() title!: string; diff --git a/src/modules/post/post.entity.ts b/src/modules/post/post.entity.ts index 254fe9c..3609ec8 100644 --- a/src/modules/post/post.entity.ts +++ b/src/modules/post/post.entity.ts @@ -7,14 +7,15 @@ import { } from '@mikro-orm/core'; import { AbstractEntity } from '../../common/abstract.entity'; -import { UseDto } from '../../decorators/use-dto.decorator.ts'; import { UserEntity } from '../user/user.entity'; import { PostDto } from './dtos/post.dto'; import { PostTranslationEntity } from './post-translation.entity'; @Entity({ tableName: 'posts' }) -@UseDto(PostDto) export class PostEntity extends AbstractEntity { + // FIXME fix type + dtoClass = () => PostDto as any; + @Property({ type: 'uuid', fieldName: 'user_id', persist: false }) userId!: Uuid; diff --git a/src/modules/user/dtos/user.dto.ts b/src/modules/user/dtos/user.dto.ts index 437b240..879133e 100644 --- a/src/modules/user/dtos/user.dto.ts +++ b/src/modules/user/dtos/user.dto.ts @@ -7,7 +7,7 @@ import { PhoneFieldOptional, StringFieldOptional, } from '../../../decorators/field.decorators'; -import { type UserEntity } from '../user.entity'; +import type { UserEntity } from '../user.entity'; // TODO, remove this class and use constructor's second argument's type export type UserDtoOptions = Partial<{ isActive: boolean }>; diff --git a/src/modules/user/user-settings.entity.ts b/src/modules/user/user-settings.entity.ts index a1b8d3e..13d02c3 100644 --- a/src/modules/user/user-settings.entity.ts +++ b/src/modules/user/user-settings.entity.ts @@ -1,17 +1,17 @@ import { Entity, OneToOne, Property } from '@mikro-orm/core'; import { AbstractEntity } from '../../common/abstract.entity'; -import { UseDto } from '../../decorators/use-dto.decorator.ts'; import type { UserDtoOptions } from './dtos/user.dto'; import { UserDto } from './dtos/user.dto'; import { UserEntity } from './user.entity'; @Entity({ tableName: 'user_settings' }) -@UseDto(UserDto) export class UserSettingsEntity extends AbstractEntity< UserDto, UserDtoOptions > { + dtoClass = () => UserDto as any; + @Property({ default: false }) isEmailVerified = false; diff --git a/src/modules/user/user.entity.ts b/src/modules/user/user.entity.ts index 33a8c95..0dae94a 100644 --- a/src/modules/user/user.entity.ts +++ b/src/modules/user/user.entity.ts @@ -9,15 +9,15 @@ import { import { AbstractEntity } from '../../common/abstract.entity'; import { RoleType } from '../../constants'; -import { UseDto } from '../../decorators/use-dto.decorator.ts'; import { PostEntity } from '../post/post.entity'; import type { UserDtoOptions } from './dtos/user.dto'; import { UserDto } from './dtos/user.dto'; import { UserSettingsEntity } from './user-settings.entity'; @Entity({ tableName: 'users' }) -@UseDto(UserDto) export class UserEntity extends AbstractEntity { + dtoClass = () => UserDto as any; + @Property({ nullable: true, type: 'varchar' }) firstName!: string | null; diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index a7d56c1..33bae97 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,11 +1,12 @@ +import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Module } from '@nestjs/common'; +import { UserSubscriber } from '../../entity-subscribers/user-subscriber.ts'; import { CreateSettingsHandler } from './commands/create-settings.command'; import { UserController } from './user.controller'; import { UserEntity } from './user.entity'; import { UserService } from './user.service'; import { UserSettingsEntity } from './user-settings.entity'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; const handlers = [CreateSettingsHandler]; @@ -13,6 +14,6 @@ const handlers = [CreateSettingsHandler]; imports: [MikroOrmModule.forFeature([UserEntity, UserSettingsEntity])], controllers: [UserController], exports: [UserService], - providers: [UserService, ...handlers], + providers: [UserService, UserSubscriber, ...handlers], }) export class UserModule {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index d6b06d9..f82b5fe 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,6 +1,5 @@ import type { FilterQuery } from '@mikro-orm/core/typings'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { EntityRepository } from '@mikro-orm/postgresql'; import { Injectable } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { plainToClass } from 'class-transformer'; @@ -16,12 +15,13 @@ import { CreateSettingsDto } from './dtos/create-settings.dto'; import type { UserDto } from './dtos/user.dto'; import { UserEntity } from './user.entity'; import type { UserSettingsEntity } from './user-settings.entity'; +import { ExtendedEntityRepository } from '../../common/extended-entity-repository.ts'; @Injectable() export class UserService { constructor( @InjectRepository(UserEntity) - private userRepository: EntityRepository, + private userRepository: ExtendedEntityRepository, private validatorService: ValidatorService, private awsS3Service: AwsS3Service, private commandBus: CommandBus, @@ -74,7 +74,7 @@ export class UserService { user.avatar = await this.awsS3Service.uploadImage(file); } - await this.userRepository.insert(user); + await this.userRepository.persistAndFlush(user); user.settings = await this.createSettings( user.id, diff --git a/src/shared/services/api-config.service.ts b/src/shared/services/api-config.service.ts index d42998b..2ef5fb5 100644 --- a/src/shared/services/api-config.service.ts +++ b/src/shared/services/api-config.service.ts @@ -10,6 +10,7 @@ import type { ThrottlerOptions } from '@nestjs/throttler'; import { isNil } from 'lodash'; import type { Units } from 'parse-duration'; import { default as parse } from 'parse-duration'; +import { ExtendedEntityRepository } from '../../common/extended-entity-repository.ts'; @Injectable() export class ApiConfigService { @@ -84,6 +85,8 @@ export class ApiConfigService { return { entities: ['./dist/modules/**/*.entity.js'], entitiesTs: ['./src/modules/**/*.entity.ts'], + entityRepository: ExtendedEntityRepository, + // subscribers: [UserSubscriber], migrations: { transactional: true, path: './dist/database/migrations',