add /auth/patch endpoint

This commit is contained in:
2023-07-17 00:12:17 +02:00
parent 111ca55cbf
commit 8d33fce53d
7 changed files with 120 additions and 4 deletions

View File

@@ -5,17 +5,22 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
Param, Param,
Patch,
Post, Post,
Request, Request,
Res, Res,
Response, Response,
UnauthorizedException, UnauthorizedException,
UploadedFiles,
UseGuards, UseGuards,
UseInterceptors,
} from '@nestjs/common' } from '@nestjs/common'
import { CommandBus } from '@nestjs/cqrs' import { CommandBus } from '@nestjs/cqrs'
import { FileFieldsInterceptor } from '@nestjs/platform-express'
import { import {
ApiBadRequestResponse, ApiBadRequestResponse,
ApiBody, ApiBody,
ApiConsumes,
ApiNoContentResponse, ApiNoContentResponse,
ApiNotFoundResponse, ApiNotFoundResponse,
ApiOperation, ApiOperation,
@@ -34,6 +39,7 @@ import {
ResendVerificationEmailDto, ResendVerificationEmailDto,
} from './dto' } from './dto'
import { ResetPasswordDto } from './dto/reset-password.dto' import { ResetPasswordDto } from './dto/reset-password.dto'
import { UpdateUserDataDto } from './dto/update-user-data.dto'
import { LoginResponse, UserEntity } from './entities/auth.entity' import { LoginResponse, UserEntity } from './entities/auth.entity'
import { JwtAuthGuard, JwtRefreshGuard, LocalAuthGuard } from './guards' import { JwtAuthGuard, JwtRefreshGuard, LocalAuthGuard } from './guards'
import { import {
@@ -45,6 +51,7 @@ import {
ResetPasswordCommand, ResetPasswordCommand,
SendPasswordRecoveryEmailCommand, SendPasswordRecoveryEmailCommand,
VerifyEmailCommand, VerifyEmailCommand,
UpdateUserCommand,
} from './use-cases' } from './use-cases'
@ApiTags('Auth') @ApiTags('Auth')
@@ -63,6 +70,23 @@ export class AuthController {
return await this.commandBus.execute(new GetCurrentUserDataCommand(userId)) 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<UserEntity> {
const userId = req.user.id
return await this.commandBus.execute(new UpdateUserCommand(userId, body, files.avatar?.[0]))
}
@ApiOperation({ @ApiOperation({
description: 'Sign in using email and password. Must have an account to do so.', 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.', summary: 'Sign in using email and password. Must have an account to do so.',

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { CqrsModule } from '@nestjs/cqrs' import { CqrsModule } from '@nestjs/cqrs'
import { FileUploadService } from '../../infrastructure/file-upload-service/file-upload.service'
import { UsersModule } from '../users/users.module' import { UsersModule } from '../users/users.module'
import { AuthController } from './auth.controller' import { AuthController } from './auth.controller'
@@ -16,6 +17,7 @@ import {
ResetPasswordHandler, ResetPasswordHandler,
SendPasswordRecoveryEmailHandler, SendPasswordRecoveryEmailHandler,
VerifyEmailHandler, VerifyEmailHandler,
UpdateUserHandler,
} from './use-cases' } from './use-cases'
const commandHandlers = [ const commandHandlers = [
@@ -27,12 +29,13 @@ const commandHandlers = [
ResetPasswordHandler, ResetPasswordHandler,
SendPasswordRecoveryEmailHandler, SendPasswordRecoveryEmailHandler,
VerifyEmailHandler, VerifyEmailHandler,
UpdateUserHandler,
] ]
@Module({ @Module({
imports: [UsersModule, CqrsModule], imports: [UsersModule, CqrsModule],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService, LocalStrategy, AuthRepository, ...commandHandlers], providers: [AuthService, LocalStrategy, AuthRepository, FileUploadService, ...commandHandlers],
exports: [AuthService, CqrsModule], exports: [AuthService, CqrsModule],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
import { OmitType } from '@nestjs/swagger' import { ApiProperty, OmitType } from '@nestjs/swagger'
export class User { export class User {
id: string id: string
@@ -6,9 +6,10 @@ export class User {
password: string password: string
isEmailVerified: boolean isEmailVerified: boolean
name: string name: string
@ApiProperty({ type: 'string', format: 'binary' })
avatar: string avatar: string
created: string created: Date
updated: string updated: Date
} }
export class LoginResponse { export class LoginResponse {

View File

@@ -5,4 +5,5 @@ export * from './resend-verification-email-use-case'
export * from './reset-password-use-case' export * from './reset-password-use-case'
export * from './refresh-token-use-case' export * from './refresh-token-use-case'
export * from './send-password-recovery-email-use-case' export * from './send-password-recovery-email-use-case'
export * from './update-user-use-case'
export * from './verify-email-use-case' export * from './verify-email-use-case'

View File

@@ -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<UpdateUserCommand> {
constructor(
private readonly usersRepository: UsersRepository,
private readonly usersService: UsersService,
private readonly fileUploadService: FileUploadService
) {}
async execute(command: UpdateUserCommand): Promise<UserEntity | null> {
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',
])
}
}

View File

@@ -95,6 +95,17 @@ export class UsersRepository {
} }
} }
async updateUser(id: string, data: Prisma.userUpdateInput): Promise<User> {
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<boolean> { async deleteUserById(id: string): Promise<boolean> {
try { try {
const result = await this.prisma.user.delete({ const result = await this.prisma.user.delete({