From 8d33fce53d227f31acc0f0152466c64fcebd15de Mon Sep 17 00:00:00 2001 From: Andres Date: Mon, 17 Jul 2023 00:12:17 +0200 Subject: [PATCH] add /auth/patch endpoint --- src/modules/auth/auth.controller.ts | 24 ++++++++ src/modules/auth/auth.module.ts | 5 +- src/modules/auth/dto/update-user-data.dto.ts | 15 +++++ src/modules/auth/entities/auth.entity.ts | 7 ++- src/modules/auth/use-cases/index.ts | 1 + .../auth/use-cases/update-user-use-case.ts | 61 +++++++++++++++++++ .../users/infrastructure/users.repository.ts | 11 ++++ 7 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 src/modules/auth/dto/update-user-data.dto.ts create mode 100644 src/modules/auth/use-cases/update-user-use-case.ts diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 48a27cc..11a42ed 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -5,17 +5,22 @@ import { HttpCode, HttpStatus, Param, + Patch, Post, Request, Res, Response, UnauthorizedException, + UploadedFiles, UseGuards, + UseInterceptors, } from '@nestjs/common' import { CommandBus } from '@nestjs/cqrs' +import { FileFieldsInterceptor } from '@nestjs/platform-express' import { ApiBadRequestResponse, ApiBody, + ApiConsumes, ApiNoContentResponse, ApiNotFoundResponse, ApiOperation, @@ -34,6 +39,7 @@ import { ResendVerificationEmailDto, } from './dto' import { ResetPasswordDto } from './dto/reset-password.dto' +import { UpdateUserDataDto } from './dto/update-user-data.dto' import { LoginResponse, UserEntity } from './entities/auth.entity' import { JwtAuthGuard, JwtRefreshGuard, LocalAuthGuard } from './guards' import { @@ -45,6 +51,7 @@ import { ResetPasswordCommand, SendPasswordRecoveryEmailCommand, VerifyEmailCommand, + UpdateUserCommand, } from './use-cases' @ApiTags('Auth') @@ -63,6 +70,23 @@ export class AuthController { return await this.commandBus.execute(new GetCurrentUserDataCommand(userId)) } + @ApiConsumes('multipart/form-data') + @ApiOperation({ description: 'Update current user data.', summary: 'Update user data' }) + @ApiUnauthorizedResponse({ description: 'Not logged in' }) + @ApiBadRequestResponse({ description: 'User not found' }) + @UseInterceptors(FileFieldsInterceptor([{ name: 'avatar', maxCount: 1 }])) + @UseGuards(JwtAuthGuard) + @Patch('me') + async updateUserData( + @Request() req, + @UploadedFiles() + files: { avatar: Express.Multer.File[] }, + @Body() body: UpdateUserDataDto + ): Promise { + const userId = req.user.id + + return await this.commandBus.execute(new UpdateUserCommand(userId, body, files.avatar?.[0])) + } @ApiOperation({ description: 'Sign in using email and password. Must have an account to do so.', summary: 'Sign in using email and password. Must have an account to do so.', diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 0a13ace..ab3cea2 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common' import { CqrsModule } from '@nestjs/cqrs' +import { FileUploadService } from '../../infrastructure/file-upload-service/file-upload.service' import { UsersModule } from '../users/users.module' import { AuthController } from './auth.controller' @@ -16,6 +17,7 @@ import { ResetPasswordHandler, SendPasswordRecoveryEmailHandler, VerifyEmailHandler, + UpdateUserHandler, } from './use-cases' const commandHandlers = [ @@ -27,12 +29,13 @@ const commandHandlers = [ ResetPasswordHandler, SendPasswordRecoveryEmailHandler, VerifyEmailHandler, + UpdateUserHandler, ] @Module({ imports: [UsersModule, CqrsModule], controllers: [AuthController], - providers: [AuthService, LocalStrategy, AuthRepository, ...commandHandlers], + providers: [AuthService, LocalStrategy, AuthRepository, FileUploadService, ...commandHandlers], exports: [AuthService, CqrsModule], }) export class AuthModule {} diff --git a/src/modules/auth/dto/update-user-data.dto.ts b/src/modules/auth/dto/update-user-data.dto.ts new file mode 100644 index 0000000..39fc45c --- /dev/null +++ b/src/modules/auth/dto/update-user-data.dto.ts @@ -0,0 +1,15 @@ +import { PickType } from '@nestjs/swagger' +import { IsEmail, IsOptional } from 'class-validator' + +import { User } from '../entities/auth.entity' + +export class UpdateUserDataDto extends PickType(User, ['name', 'email', 'avatar'] as const) { + avatar: string + + @IsOptional() + name: string + + @IsOptional() + @IsEmail() + email: string +} diff --git a/src/modules/auth/entities/auth.entity.ts b/src/modules/auth/entities/auth.entity.ts index b102139..c12976d 100644 --- a/src/modules/auth/entities/auth.entity.ts +++ b/src/modules/auth/entities/auth.entity.ts @@ -1,4 +1,4 @@ -import { OmitType } from '@nestjs/swagger' +import { ApiProperty, OmitType } from '@nestjs/swagger' export class User { id: string @@ -6,9 +6,10 @@ export class User { password: string isEmailVerified: boolean name: string + @ApiProperty({ type: 'string', format: 'binary' }) avatar: string - created: string - updated: string + created: Date + updated: Date } export class LoginResponse { diff --git a/src/modules/auth/use-cases/index.ts b/src/modules/auth/use-cases/index.ts index a59d9f2..9dfbbc6 100644 --- a/src/modules/auth/use-cases/index.ts +++ b/src/modules/auth/use-cases/index.ts @@ -5,4 +5,5 @@ export * from './resend-verification-email-use-case' export * from './reset-password-use-case' export * from './refresh-token-use-case' export * from './send-password-recovery-email-use-case' +export * from './update-user-use-case' export * from './verify-email-use-case' diff --git a/src/modules/auth/use-cases/update-user-use-case.ts b/src/modules/auth/use-cases/update-user-use-case.ts new file mode 100644 index 0000000..0686ed5 --- /dev/null +++ b/src/modules/auth/use-cases/update-user-use-case.ts @@ -0,0 +1,61 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' +import { pick } from 'remeda' + +import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service' +import { UsersRepository } from '../../users/infrastructure/users.repository' +import { UsersService } from '../../users/services/users.service' +import { UpdateUserDataDto } from '../dto/update-user-data.dto' +import { UserEntity } from '../entities/auth.entity' + +export class UpdateUserCommand { + constructor( + public readonly userId: string, + public readonly user: UpdateUserDataDto, + public readonly avatar: Express.Multer.File + ) {} +} + +@CommandHandler(UpdateUserCommand) +export class UpdateUserHandler implements ICommandHandler { + constructor( + private readonly usersRepository: UsersRepository, + private readonly usersService: UsersService, + private readonly fileUploadService: FileUploadService + ) {} + + async execute(command: UpdateUserCommand): Promise { + let avatar + + if (command.avatar) { + const addAvatarImagePromise = this.fileUploadService.uploadFile( + command.avatar?.buffer, + command.avatar?.originalname + ) + + const result = await addAvatarImagePromise + + avatar = result.fileUrl + } else if (command.user.avatar === '') { + avatar = null + } + + const updatedUser = await this.usersRepository.updateUser(command.userId, { + ...command.user, + avatar, + }) + + if (!updatedUser) { + return null + } + + return pick(updatedUser, [ + 'id', + 'name', + 'email', + 'isEmailVerified', + 'avatar', + 'created', + 'updated', + ]) + } +} diff --git a/src/modules/users/infrastructure/users.repository.ts b/src/modules/users/infrastructure/users.repository.ts index 680ea6c..ac2f757 100644 --- a/src/modules/users/infrastructure/users.repository.ts +++ b/src/modules/users/infrastructure/users.repository.ts @@ -95,6 +95,17 @@ export class UsersRepository { } } + async updateUser(id: string, data: Prisma.userUpdateInput): Promise { + try { + return await this.prisma.user.update({ + where: { id }, + data, + }) + } catch (e) { + this.logger.error(e?.message || e) + throw new InternalServerErrorException(e) + } + } async deleteUserById(id: string): Promise { try { const result = await this.prisma.user.delete({