From 3721f4365e63493691aa42dcdf7c51fad85edf49 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 6 Jan 2025 18:54:02 +0100 Subject: [PATCH] feat: allow user/edit/:id endpoint to upload avatar --- api/src/user/controllers/user.controller.ts | 44 ++++++++++++++++++++- api/src/user/dto/user.dto.ts | 9 +++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/api/src/user/controllers/user.controller.ts b/api/src/user/controllers/user.controller.ts index cc50f821..adcc0b8a 100644 --- a/api/src/user/controllers/user.controller.ts +++ b/api/src/user/controllers/user.controller.ts @@ -21,11 +21,14 @@ import { Req, Session, UnauthorizedException, + UploadedFile, UseInterceptors, } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { CsrfCheck } from '@tekuconcept/nestjs-csrf'; import { Request } from 'express'; import { Session as ExpressSession } from 'express-session'; +import { diskStorage, memoryStorage } from 'multer'; import { AttachmentService } from '@/attachment/services/attachment.service'; import { config } from '@/config'; @@ -261,17 +264,54 @@ export class ReadWriteUserController extends ReadOnlyUserController { * @returns A promise that resolves to the updated user. */ @CsrfCheck(true) + @UseInterceptors( + FileInterceptor('avatar', { + limits: { + fileSize: config.parameters.maxUploadSize, + }, + storage: (() => { + if (config.parameters.storageMode === 'memory') { + return memoryStorage(); + } else { + return diskStorage({}); + } + })(), + }), + ) @Patch('edit/:id') async updateOne( @Req() req: Request, @Param('id') id: string, @Body() userUpdate: UserEditProfileDto, + @UploadedFile() avatarFile?: Express.Multer.File, ) { if (!('id' in req.user && req.user.id) || req.user.id !== id) { - throw new UnauthorizedException(); + throw new ForbiddenException(); } - const result = await this.userService.updateOne(req.user.id, userUpdate); + // Upload Avatar if provided + const avatar = avatarFile + ? await this.attachmentService.store( + avatarFile, + { + name: avatarFile.originalname, + size: avatarFile.size, + type: avatarFile.mimetype, + }, + config.parameters.avatarDir, + ) + : undefined; + + const result = await this.userService.updateOne( + req.user.id, + avatar + ? { + ...userUpdate, + avatar: avatar.id, + } + : userUpdate, + ); + if (!result) { this.logger.warn(`Unable to update User by id ${id}`); throw new NotFoundException(`User with ID ${id} not found`); diff --git a/api/src/user/dto/user.dto.ts b/api/src/user/dto/user.dto.ts index e7d7ca4e..56d298ac 100644 --- a/api/src/user/dto/user.dto.ts +++ b/api/src/user/dto/user.dto.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -14,12 +14,12 @@ import { PartialType, } from '@nestjs/swagger'; import { - IsEmail, - IsNotEmpty, - IsString, IsArray, IsBoolean, + IsEmail, + IsNotEmpty, IsOptional, + IsString, } from 'class-validator'; import { IsObjectId } from '@/utils/validation-rules/is-object-id'; @@ -66,6 +66,7 @@ export class UserCreateDto { export class UserEditProfileDto extends OmitType(PartialType(UserCreateDto), [ 'username', 'roles', + 'avatar', ]) { @ApiPropertyOptional({ description: 'User language', type: String }) @IsOptional()