From 5a70707cd507b9b255be600e9b1b13b64f942d0f Mon Sep 17 00:00:00 2001 From: lapatric <42653152+lapatric@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:09:49 +0200 Subject: [PATCH] [DEV-3978] refactor to use express.multer.file --- src/shared/dto/file-upload.dto.ts | 4 ++ .../kyc/controllers/kyc-admin.controller.ts | 13 ++++- .../generic/kyc/controllers/kyc.controller.ts | 56 +++++++++++-------- .../kyc/dto/input/create-kyc-log.dto.ts | 11 +--- .../generic/kyc/dto/input/kyc-data.dto.ts | 30 ---------- .../generic/kyc/dto/kyc-file.dto.ts | 14 +++++ .../generic/kyc/services/kyc-log.service.ts | 9 +-- .../generic/kyc/services/kyc.service.ts | 42 ++++++++++---- .../models/user-data/dto/upload-file.dto.ts | 12 ++-- .../models/user-data/user-data.controller.ts | 36 ++++++++++-- .../dto/create-support-message.dto.ts | 16 +----- .../services/support-issue.service.ts | 55 +++++++++++++----- .../support-issue/support-issue.controller.ts | 35 +++++++++--- 13 files changed, 202 insertions(+), 131 deletions(-) create mode 100644 src/shared/dto/file-upload.dto.ts diff --git a/src/shared/dto/file-upload.dto.ts b/src/shared/dto/file-upload.dto.ts new file mode 100644 index 0000000000..c80fa5913a --- /dev/null +++ b/src/shared/dto/file-upload.dto.ts @@ -0,0 +1,4 @@ +export interface FileUploadData { + fileName: string; + file: string; // Base64 encoded file content +} diff --git a/src/subdomains/generic/kyc/controllers/kyc-admin.controller.ts b/src/subdomains/generic/kyc/controllers/kyc-admin.controller.ts index 2fd3eda6e8..417114e028 100644 --- a/src/subdomains/generic/kyc/controllers/kyc-admin.controller.ts +++ b/src/subdomains/generic/kyc/controllers/kyc-admin.controller.ts @@ -1,5 +1,6 @@ -import { Body, Controller, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Param, Post, Put, Query, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiExcludeController, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; @@ -63,8 +64,14 @@ export class KycAdminController { @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) @ApiExcludeEndpoint() - async createLog(@GetJwt() jwt: JwtPayload, @Body() dto: CreateKycLogDto): Promise { - await this.kycLogService.createLog(jwt.account, dto); + @UseInterceptors(FileInterceptor('file')) + async createLog( + @GetJwt() jwt: JwtPayload, + @Body() dto: CreateKycLogDto, + @UploadedFile() file?: Express.Multer.File, + ): Promise { + const document = file ? { fileName: file.originalname, file: file.buffer.toString('base64') } : undefined; + await this.kycLogService.createLog(jwt.account, dto, document); } @Put('log/:id') diff --git a/src/subdomains/generic/kyc/controllers/kyc.controller.ts b/src/subdomains/generic/kyc/controllers/kyc.controller.ts index 09905e4993..882e1894c6 100644 --- a/src/subdomains/generic/kyc/controllers/kyc.controller.ts +++ b/src/subdomains/generic/kyc/controllers/kyc.controller.ts @@ -7,14 +7,18 @@ import { Headers, InternalServerErrorException, Param, + ParseFilePipeBuilder, Post, Put, Query, Req, Res, + UploadedFile, UseGuards, + UseInterceptors, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiConflictResponse, @@ -38,13 +42,11 @@ import { UserRole } from 'src/shared/auth/user-role.enum'; import { CountryDtoMapper } from 'src/shared/models/country/dto/country-dto.mapper'; import { CountryDto } from 'src/shared/models/country/dto/country.dto'; import { DfxLogger } from 'src/shared/services/dfx-logger'; -import { Util } from 'src/shared/utils/util'; import { IdNowResult } from '../dto/ident-result.dto'; import { IdentStatus } from '../dto/ident.dto'; import { KycBeneficialData, KycContactData, - KycFileData, KycLegalEntityData, KycManualIdentData, KycNationalityData, @@ -194,13 +196,14 @@ export class KycController { @Put('data/owner/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateOwnerDirectoryData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, - @Body() data: KycFileData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - data.fileName = this.fileName('stock-register', data.fileName); - return this.kycService.updateFileData(code, +id, data, FileType.STOCK_REGISTER); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateFileData(code, +id, document, FileType.STOCK_REGISTER); } @Put('data/nationality/:id') @@ -217,49 +220,54 @@ export class KycController { @Put('data/legal/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateCommercialRegisterData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, @Body() data: KycLegalEntityData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - data.fileName = this.fileName('commercial-register', data.fileName); - return this.kycService.updateLegalData(code, +id, data, FileType.COMMERCIAL_REGISTER); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateLegalData(code, +id, data, document, FileType.COMMERCIAL_REGISTER); } @Put('data/residence/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateResidencePermitData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, - @Body() data: KycFileData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - data.fileName = this.fileName('residence-permit', data.fileName); - return this.kycService.updateFileData(code, +id, data, FileType.RESIDENCE_PERMIT); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateFileData(code, +id, document, FileType.RESIDENCE_PERMIT); } @Put('data/statutes/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateStatutesData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, - @Body() data: KycFileData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - data.fileName = this.fileName('statutes', data.fileName); - return this.kycService.updateFileData(code, +id, data, FileType.STATUTES); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateFileData(code, +id, document, FileType.STATUTES); } @Put('data/additional/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateAdditionalDocumentsData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, - @Body() data: KycFileData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - data.fileName = this.fileName('additional-documents', data.fileName); - return this.kycService.updateFileData(code, +id, data, FileType.ADDITIONAL_DOCUMENTS); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateFileData(code, +id, document, FileType.ADDITIONAL_DOCUMENTS); } @Put('data/signatory/:id') @@ -298,13 +306,14 @@ export class KycController { @Put('data/authority/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateAuthorityData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, - @Body() data: KycFileData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - data.fileName = this.fileName('authority', data.fileName); - return this.kycService.updateFileData(code, +id, data, FileType.AUTHORITY); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateFileData(code, +id, document, FileType.AUTHORITY); } @Get('data/financial/:id') @@ -365,12 +374,15 @@ export class KycController { @Put('ident/manual/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) + @UseInterceptors(FileInterceptor('file')) async updateIdentData( @Headers(CodeHeaderName) code: string, @Param('id') id: string, @Body() data: KycManualIdentData, + @UploadedFile(new ParseFilePipeBuilder().build({ fileIsRequired: true })) file: Express.Multer.File, ): Promise { - return this.kycService.updateIdentManual(code, +id, data); + const document = { fileName: file.originalname, file: file.buffer.toString('base64') }; + return this.kycService.updateIdentManual(code, +id, data, document); } @Post('ident/:type') @@ -416,8 +428,4 @@ export class KycController { .join(';'); res.setHeader('Content-Security-Policy', updatedPolicy); } - - private fileName(type: string, file: string): string { - return `${Util.isoDateTime(new Date())}_${type}_user-upload_${Util.randomId()}_${file}`; - } } diff --git a/src/subdomains/generic/kyc/dto/input/create-kyc-log.dto.ts b/src/subdomains/generic/kyc/dto/input/create-kyc-log.dto.ts index 6371afcf64..5cce92a483 100644 --- a/src/subdomains/generic/kyc/dto/input/create-kyc-log.dto.ts +++ b/src/subdomains/generic/kyc/dto/input/create-kyc-log.dto.ts @@ -1,5 +1,5 @@ import { Type } from 'class-transformer'; -import { IsDate, IsNotEmpty, IsObject, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator'; +import { IsDate, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; import { EntityDto } from 'src/shared/dto/entity.dto'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; @@ -27,13 +27,4 @@ export class CreateKycLogDto extends UpdateKycLogDto { @ValidateNested() @Type(() => EntityDto) userData: UserData; - - @IsOptional() - @IsString() - file?: string; - - @ValidateIf((d: CreateKycLogDto) => d.file != null) - @IsNotEmpty() - @IsString() - fileName?: string; } diff --git a/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts b/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts index b2bc48759e..30ca69a4bf 100644 --- a/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts +++ b/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts @@ -184,31 +184,7 @@ export class KycNationalityData { nationality: Country; } -export class KycFileData { - @ApiProperty({ description: 'Base64 encoded file' }) - @IsNotEmpty() - @IsString() - file: string; - - @ApiProperty({ description: 'Name of the file' }) - @IsNotEmpty() - @IsString() - @Transform(Util.sanitize) - fileName: string; -} - export class KycLegalEntityData { - @ApiProperty({ description: 'Base64 encoded commercial register file' }) - @IsNotEmpty() - @IsString() - file: string; - - @ApiProperty({ description: 'Name of the commercial register file' }) - @IsNotEmpty() - @IsString() - @Transform(Util.sanitize) - fileName: string; - @ApiProperty({ enum: LegalEntity }) @IsNotEmpty() @IsEnum(LegalEntity) @@ -267,12 +243,6 @@ export class KycManualIdentData { @IsString() @Transform(Util.sanitize) documentNumber: string; - - @ApiProperty({ type: KycFileData }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => KycFileData) - document: KycFileData; } export class PaymentDataDto { diff --git a/src/subdomains/generic/kyc/dto/kyc-file.dto.ts b/src/subdomains/generic/kyc/dto/kyc-file.dto.ts index b9035944fb..ea0b508443 100644 --- a/src/subdomains/generic/kyc/dto/kyc-file.dto.ts +++ b/src/subdomains/generic/kyc/dto/kyc-file.dto.ts @@ -20,6 +20,20 @@ export enum FileType { AUTHORITY = 'Authority', } +export const fileLabel: { [key in FileType]: string } = { + [FileType.NAME_CHECK]: 'name-check', + [FileType.USER_INFORMATION]: 'user-information', + [FileType.IDENTIFICATION]: 'identification', + [FileType.USER_NOTES]: 'user-notes', + [FileType.TRANSACTION_NOTES]: 'transaction-notes', + [FileType.STOCK_REGISTER]: 'stock-register', + [FileType.COMMERCIAL_REGISTER]: 'commercial-register', + [FileType.RESIDENCE_PERMIT]: 'residence-permit', + [FileType.STATUTES]: 'statutes', + [FileType.ADDITIONAL_DOCUMENTS]: 'additional-documents', + [FileType.AUTHORITY]: 'authority', +}; + export enum FileSubType { GWG_FILE_COVER = 'GwGFileCover', BLOCKCHAIN_ADDRESS_ANALYSIS = 'BlockchainAddressAnalysis', diff --git a/src/subdomains/generic/kyc/services/kyc-log.service.ts b/src/subdomains/generic/kyc/services/kyc-log.service.ts index 282274c1c9..c237a6239d 100644 --- a/src/subdomains/generic/kyc/services/kyc-log.service.ts +++ b/src/subdomains/generic/kyc/services/kyc-log.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable, NotFoundException, forwardRef } from '@nestjs/common'; +import { FileUploadData } from 'src/shared/dto/file-upload.dto'; import { Util } from 'src/shared/utils/util'; import { UserData } from '../../user/models/user-data/user-data.entity'; import { UserDataService } from '../../user/models/user-data/user-data.service'; @@ -27,7 +28,7 @@ export class KycLogService { await this.kycLogRepo.save(entity); } - async createLog(creatorUserDataId: number, dto: CreateKycLogDto): Promise { + async createLog(creatorUserDataId: number, dto: CreateKycLogDto, document?: FileUploadData): Promise { const entity = this.kycLogRepo.create({ type: KycLogType.MANUAL, comment: dto.comment, @@ -38,13 +39,13 @@ export class KycLogService { entity.userData = await this.userDataService.getUserData(dto.userData.id); if (!entity.userData) throw new NotFoundException('UserData not found'); - if (dto.file) { - const { contentType, buffer } = Util.fromBase64(dto.file); + if (document) { + const { contentType, buffer } = Util.fromBase64(document.file); const { file, url } = await this.kycDocumentService.uploadUserFile( entity.userData, FileType.USER_NOTES, - `Manual/${Util.isoDateTime(new Date())}_manual-upload_${Util.randomId()}_${dto.fileName}`, + `Manual/${Util.isoDateTime(new Date())}_manual-upload_${Util.randomId()}_${document.fileName}`, buffer, contentType as ContentType, true, diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index 24da2f6a6a..35eab18bc4 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -43,11 +43,11 @@ import { getIdentResult, } from '../dto/ident-result.dto'; import { IdentDocument, IdentStatus } from '../dto/ident.dto'; +import { FileUploadData } from 'src/shared/dto/file-upload.dto'; import { ContactPersonData, KycBeneficialData, KycContactData, - KycFileData, KycLegalEntityData, KycManualIdentData, KycNationalityData, @@ -57,7 +57,7 @@ import { } from '../dto/input/kyc-data.dto'; import { KycFinancialInData, KycFinancialResponse } from '../dto/input/kyc-financial-in.dto'; import { KycError } from '../dto/kyc-error.enum'; -import { FileType, KycFileDataDto } from '../dto/kyc-file.dto'; +import { FileType, KycFileDataDto, fileLabel } from '../dto/kyc-file.dto'; import { KycFileMapper } from '../dto/mapper/kyc-file.mapper'; import { KycInfoMapper } from '../dto/mapper/kyc-info.mapper'; import { KycStepMapper } from '../dto/mapper/kyc-step.mapper'; @@ -480,16 +480,21 @@ export class KycService { return this.updateKycStepAndLog(kycStep, user, data, ReviewStatus.MANUAL_REVIEW); } - async updateFileData(kycHash: string, stepId: number, data: KycFileData, fileType: FileType): Promise { + async updateFileData( + kycHash: string, + stepId: number, + document: FileUploadData, + fileType: FileType, + ): Promise { const user = await this.getUser(kycHash); const kycStep = user.getPendingStepOrThrow(stepId); // upload file - const { contentType, buffer } = Util.fromBase64(data.file); + const { contentType, buffer } = Util.fromBase64(document.file); const { url } = await this.documentService.uploadUserFile( user, fileType, - data.fileName, + this.fileName(fileType, document.fileName), buffer, contentType as ContentType, false, @@ -503,16 +508,22 @@ export class KycService { return KycStepMapper.toStepBase(kycStep); } - async updateLegalData(kycHash: string, stepId: number, data: KycLegalEntityData, fileType: FileType) { + async updateLegalData( + kycHash: string, + stepId: number, + data: KycLegalEntityData, + document: FileUploadData, + fileType: FileType, + ) { const user = await this.getUser(kycHash); const kycStep = user.getPendingStepOrThrow(stepId); // upload file - const { contentType, buffer } = Util.fromBase64(data.file); + const { contentType, buffer } = Util.fromBase64(document.file); const { url } = await this.documentService.uploadUserFile( user, fileType, - data.fileName, + this.fileName(fileType, document.fileName), buffer, contentType as ContentType, false, @@ -750,18 +761,23 @@ export class KycService { await this.updateProgress(user, false); } - async updateIdentManual(kycHash: string, stepId: number, dto: KycManualIdentData): Promise { + async updateIdentManual( + kycHash: string, + stepId: number, + dto: KycManualIdentData, + document: FileUploadData, + ): Promise { const user = await this.getUser(kycHash); const kycStep = user.getPendingStepOrThrow(stepId); dto.nationality = await this.countryService.getCountry(dto.nationality.id); if (!dto.nationality) throw new NotFoundException('Country not found'); - const { contentType, buffer } = Util.fromBase64(dto.document.file); + const { contentType, buffer } = Util.fromBase64(document.file); const { url } = await this.documentService.uploadUserFile( user, FileType.IDENTIFICATION, - `${Util.isoDateTime(new Date()).split('-').join('')}_manual-ident_${Util.randomId()}_${dto.document.fileName}`, + `${Util.isoDateTime(new Date()).split('-').join('')}_manual-ident_${Util.randomId()}_${document.fileName}`, buffer, contentType as ContentType, false, @@ -1283,4 +1299,8 @@ export class KycService { ); } } + + private fileName(type: FileType, fileName: string): string { + return `${Util.isoDateTime(new Date())}_${fileLabel[type]}_user-upload_${Util.randomId()}_${fileName}`; + } } diff --git a/src/subdomains/generic/user/models/user-data/dto/upload-file.dto.ts b/src/subdomains/generic/user/models/user-data/dto/upload-file.dto.ts index 4be6ed1a25..3bac09f21e 100644 --- a/src/subdomains/generic/user/models/user-data/dto/upload-file.dto.ts +++ b/src/subdomains/generic/user/models/user-data/dto/upload-file.dto.ts @@ -11,17 +11,13 @@ export class UploadFileDto { @IsEnum(FileSubType) documentSubType: FileSubType; - @IsNotEmpty() - @IsString() - originalName: string; - - @IsNotEmpty() + @IsOptional() @IsString() - contentType: ContentType; + originalName?: string; - @IsNotEmpty() + @IsOptional() @IsString() - data: string; + contentType?: ContentType; @ValidateIf((dto: UploadFileDto) => dto.documentType === FileType.NAME_CHECK) @IsNotEmpty() diff --git a/src/subdomains/generic/user/models/user-data/user-data.controller.ts b/src/subdomains/generic/user/models/user-data/user-data.controller.ts index 0bddcd7f1f..d7d0043efb 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.controller.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.controller.ts @@ -1,10 +1,26 @@ -import { Body, Controller, Delete, Get, Param, Post, Put, Query, Res, StreamableFile, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + Res, + StreamableFile, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiExcludeEndpoint, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { Util } from 'src/shared/utils/util'; +import { ContentType } from 'src/subdomains/generic/kyc/enums/content-type.enum'; import { KycDocumentService } from 'src/subdomains/generic/kyc/services/integration/kyc-document.service'; import { KycLogService } from 'src/subdomains/generic/kyc/services/kyc-log.service'; import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service'; @@ -112,22 +128,30 @@ export class UserDataController { @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) - async uploadKycFile(@Param('id') id: string, @Body() dto: UploadFileDto): Promise { + @UseInterceptors(FileInterceptor('file')) + async uploadKycFile( + @Param('id') id: string, + @Body() dto: UploadFileDto, + @UploadedFile() file: Express.Multer.File, + ): Promise { const userData = await this.userDataService.getUserData(+id); + const fileName = dto.originalName || file.originalname; + const contentType = dto.contentType || (file.mimetype as ContentType); + const { url } = await this.documentService.uploadUserFile( userData, dto.documentType, - dto.originalName, - Buffer.from(dto.data, 'base64'), - dto.contentType, + fileName, + file.buffer, + contentType, true, undefined, dto.documentSubType, { document: dto.documentType.toString(), creationTime: new Date().toISOString(), - fileName: dto.originalName, + fileName: fileName, }, ); diff --git a/src/subdomains/supporting/support-issue/dto/create-support-message.dto.ts b/src/subdomains/supporting/support-issue/dto/create-support-message.dto.ts index 51a4590d68..5008a22b68 100644 --- a/src/subdomains/supporting/support-issue/dto/create-support-message.dto.ts +++ b/src/subdomains/supporting/support-issue/dto/create-support-message.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { Util } from 'src/shared/utils/util'; export class CreateSupportMessageDto { @@ -14,16 +14,4 @@ export class CreateSupportMessageDto { @IsString() @Transform(Util.sanitize) message?: string; - - @ApiPropertyOptional({ description: 'Base64 encoded file' }) - @IsOptional() - @IsString() - file?: string; - - @ApiPropertyOptional({ description: 'Name of the file' }) - @ValidateIf((l: CreateSupportMessageDto) => l.file != null) - @IsNotEmpty() - @IsString() - @Transform(Util.sanitize) - fileName?: string; } diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index 4cf573667b..267ed67fe7 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -7,6 +7,7 @@ import { } from '@nestjs/common'; import { Config } from 'src/config/config'; import { BlobContent } from 'src/integration/infrastructure/azure-storage.service'; +import { FileUploadData } from 'src/shared/dto/file-upload.dto'; import { Util } from 'src/shared/utils/util'; import { ContentType } from 'src/subdomains/generic/kyc/enums/content-type.enum'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; @@ -46,7 +47,10 @@ export class SupportIssueService { private readonly supportLogService: SupportLogService, ) {} - async createTransactionRequestIssue(dto: CreateSupportIssueBaseDto): Promise { + async createTransactionRequestIssue( + dto: CreateSupportIssueBaseDto, + document?: FileUploadData, + ): Promise { if (!dto?.transaction?.orderUid) throw new BadRequestException('JWT Token or quoteUid missing'); const transactionRequest = await this.transactionRequestService.getTransactionRequestByUid( dto.transaction.orderUid, @@ -54,17 +58,25 @@ export class SupportIssueService { ); if (!transactionRequest) throw new NotFoundException('TransactionRequest not found'); - return this.createIssueInternal(transactionRequest.userData, dto); + return this.createIssueInternal(transactionRequest.userData, dto, document); } - async createIssue(userDataId: number, dto: CreateSupportIssueDto): Promise { + async createIssue( + userDataId: number, + dto: CreateSupportIssueDto, + document?: FileUploadData, + ): Promise { const userData = await this.userDataService.getUserData(userDataId, { wallet: true }); if (!userData) throw new NotFoundException('UserData not found'); - return this.createIssueInternal(userData, dto); + return this.createIssueInternal(userData, dto, document); } - async createIssueInternal(userData: UserData, dto: CreateSupportIssueDto): Promise { + async createIssueInternal( + userData: UserData, + dto: CreateSupportIssueDto, + document?: FileUploadData, + ): Promise { // mail is required if (!userData.mail) throw new BadRequestException('Mail is missing'); @@ -132,7 +144,7 @@ export class SupportIssueService { } const entity = existingIssue ?? (await this.supportIssueRepo.save(newIssue)); - const supportMessage = await this.createMessageInternal(entity, dto); + const supportMessage = await this.createMessageInternal(entity, dto, document); const issue = SupportIssueDtoMapper.mapSupportIssue(entity); issue.messages.push(supportMessage); @@ -155,21 +167,30 @@ export class SupportIssueService { return this.supportIssueRepo.save(entity); } - async createMessage(id: string, dto: CreateSupportMessageDto, userDataId?: number): Promise { + async createMessage( + id: string, + dto: CreateSupportMessageDto, + userDataId?: number, + document?: FileUploadData, + ): Promise { const issue = await this.supportIssueRepo.findOne({ where: this.getIssueSearch(id, userDataId), relations: { userData: { wallet: true } }, }); if (!issue) throw new NotFoundException('Support issue not found'); - return this.createMessageInternal(issue, { ...dto, author: CustomerAuthor }); + return this.createMessageInternal(issue, { ...dto, author: CustomerAuthor }, document); } - async createMessageSupport(id: number, dto: CreateSupportMessageDto): Promise { + async createMessageSupport( + id: number, + dto: CreateSupportMessageDto, + document?: FileUploadData, + ): Promise { const issue = await this.supportIssueRepo.findOne({ where: { id }, relations: { userData: { wallet: true } } }); if (!issue) throw new NotFoundException('Support issue not found'); - return this.createMessageInternal(issue, dto); + return this.createMessageInternal(issue, dto, document); } async getIssues(userDataId: number): Promise { @@ -218,20 +239,26 @@ export class SupportIssueService { // --- HELPER METHODS --- // - private async createMessageInternal(issue: SupportIssue, dto: CreateSupportMessageDto): Promise { + private async createMessageInternal( + issue: SupportIssue, + dto: CreateSupportMessageDto, + document?: FileUploadData, + ): Promise { if (!dto.author) throw new BadRequestException('Author for message is missing'); if (dto.message?.length > 4000) throw new BadRequestException('Message has too many characters'); const entity = this.messageRepo.create({ ...dto, issue }); // upload document - if (dto.file) { - const { contentType, buffer } = Util.fromBase64(dto.file); + if (document) { + const { contentType, buffer } = Util.fromBase64(document.file); entity.fileUrl = await this.documentService.uploadUserFile( entity.userData.id, entity.issue.id, - `${Util.isoDateTime(new Date())}_${dto.author?.toLowerCase() ?? 'support'}_${Util.randomId()}_${dto.fileName}`, + `${Util.isoDateTime(new Date())}_${dto.author?.toLowerCase() ?? 'support'}_${Util.randomId()}_${ + document.fileName + }`, buffer, contentType as ContentType, ); diff --git a/src/subdomains/supporting/support-issue/support-issue.controller.ts b/src/subdomains/supporting/support-issue/support-issue.controller.ts index ffd996f8b6..dc98a5fbc8 100644 --- a/src/subdomains/supporting/support-issue/support-issue.controller.ts +++ b/src/subdomains/supporting/support-issue/support-issue.controller.ts @@ -1,5 +1,17 @@ -import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Put, + Query, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; import { BlobContent } from 'src/integration/infrastructure/azure-storage.service'; import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; @@ -26,11 +38,17 @@ export class SupportIssueController { @Post() @ApiBearerAuth() @UseGuards(OptionalJwtAuthGuard) - async createIssue(@Body() dto: CreateSupportIssueDto, @GetJwt() jwt?: JwtPayload): Promise { + @UseInterceptors(FileInterceptor('file')) + async createIssue( + @Body() dto: CreateSupportIssueDto, + @GetJwt() jwt?: JwtPayload, + @UploadedFile() file?: Express.Multer.File, + ): Promise { + const document = file ? { fileName: file.originalname, file: file.buffer.toString('base64') } : undefined; const input: CreateSupportIssueDto = { ...dto, author: CustomerAuthor, department: Department.SUPPORT }; return jwt?.account - ? this.supportIssueService.createIssue(jwt.account, input) - : this.supportIssueService.createTransactionRequestIssue(input); + ? this.supportIssueService.createIssue(jwt.account, input, document) + : this.supportIssueService.createTransactionRequestIssue(input, document); } @Post('support') @@ -65,14 +83,17 @@ export class SupportIssueController { @Post(':id/message') @ApiBearerAuth() @UseGuards(OptionalJwtAuthGuard) + @UseInterceptors(FileInterceptor('file')) async createSupportMessage( - @GetJwt() jwt: JwtPayload, @Param('id') id: string, @Body() dto: CreateSupportMessageDto, + @GetJwt() jwt?: JwtPayload, + @UploadedFile() file?: Express.Multer.File, ): Promise { + const document = file ? { fileName: file.originalname, file: file.buffer.toString('base64') } : undefined; return [UserRole.SUPPORT, UserRole.COMPLIANCE, UserRole.ADMIN].includes(jwt.role) - ? this.supportIssueService.createMessageSupport(+id, dto) - : this.supportIssueService.createMessage(id, dto, jwt?.account); + ? this.supportIssueService.createMessageSupport(+id, dto, document) + : this.supportIssueService.createMessage(id, dto, jwt?.account, document); } @Get(':id/message/:messageId/file')