From f65a7d293383affecbce47ddd28e4fa95002465b Mon Sep 17 00:00:00 2001 From: Abdalhamid Alhamad Date: Sun, 3 Aug 2025 14:21:14 +0300 Subject: [PATCH] feat: generate upload signed url for oci --- .../controllers/document.controller.ts | 45 +++---------------- .../generate-upload-signed-url.request.dto.ts | 18 ++++++++ src/document/dtos/request/index.ts | 1 + src/document/services/document.service.ts | 9 +++- src/document/services/oci.service.ts | 45 ++++++++++++++++++- 5 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 src/document/dtos/request/generate-upload-signed-url.request.dto.ts diff --git a/src/document/controllers/document.controller.ts b/src/document/controllers/document.controller.ts index 7b3eed1..2fc2a72 100644 --- a/src/document/controllers/document.controller.ts +++ b/src/document/controllers/document.controller.ts @@ -1,15 +1,9 @@ -import { Body, Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; -import { memoryStorage } from 'multer'; -import { IJwtPayload } from '~/auth/interfaces'; -import { AuthenticatedUser } from '~/common/decorators'; +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { AccessTokenGuard } from '~/common/guards'; import { ApiLangRequestHeader } from '~/core/decorators'; import { ResponseFactory } from '~/core/utils'; -import { UploadDocumentRequestDto } from '../dtos/request'; -import { DocumentMetaResponseDto } from '../dtos/response'; -import { DocumentType } from '../enums'; +import { GenerateUploadSignedUrlRequestDto } from '../dtos/request'; import { DocumentService } from '../services'; @Controller('document') @ApiTags('Document') @@ -19,34 +13,9 @@ import { DocumentService } from '../services'; export class DocumentController { constructor(private readonly documentService: DocumentService) {} - @Post() - @ApiConsumes('multipart/form-data') - @ApiBody({ - schema: { - type: 'object', - properties: { - document: { - type: 'string', - format: 'binary', - }, - documentType: { - type: 'string', - enum: Object.values(DocumentType).filter( - (value) => ![DocumentType.DEFAULT_AVATAR, DocumentType.DEFAULT_TASKS_LOGO].includes(value), - ), - }, - }, - required: ['document', 'documentType'], - }, - }) - @UseInterceptors(FileInterceptor('document', { storage: memoryStorage() })) - async createDocument( - @UploadedFile() file: Express.Multer.File, - @Body() uploadedDocumentRequest: UploadDocumentRequestDto, - @AuthenticatedUser() user: IJwtPayload, - ) { - const document = await this.documentService.createDocument(file, uploadedDocumentRequest, user.sub); - - return ResponseFactory.data(new DocumentMetaResponseDto(document)); + @Post('signed-url') + async generateSignedUrl(@Body() body: GenerateUploadSignedUrlRequestDto) { + const signedUrl = await this.documentService.generateUploadSignedUrl(body); + return ResponseFactory.data(signedUrl); } } diff --git a/src/document/dtos/request/generate-upload-signed-url.request.dto.ts b/src/document/dtos/request/generate-upload-signed-url.request.dto.ts new file mode 100644 index 0000000..38e83b2 --- /dev/null +++ b/src/document/dtos/request/generate-upload-signed-url.request.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsString } from 'class-validator'; +import { i18nValidationMessage as i18n } from 'nestjs-i18n'; +import { DocumentType } from '~/document/enums'; + +export class GenerateUploadSignedUrlRequestDto { + @ApiProperty({ enum: DocumentType }) + @IsEnum(DocumentType, { message: i18n('validation.IsEnum', { path: 'general', property: 'document.documentType' }) }) + documentType!: DocumentType; + + @ApiProperty({ type: String }) + @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'document.document.extension' }) }) + extension!: string; + + @ApiProperty({ type: String }) + @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'document.originalFileName' }) }) + originalFileName!: string; +} diff --git a/src/document/dtos/request/index.ts b/src/document/dtos/request/index.ts index 4071ca7..2dd4fad 100644 --- a/src/document/dtos/request/index.ts +++ b/src/document/dtos/request/index.ts @@ -1 +1,2 @@ +export * from './generate-upload-signed-url.request.dto'; export * from './upload-document.request.dto'; diff --git a/src/document/services/document.service.ts b/src/document/services/document.service.ts index 5378942..bdc72cd 100644 --- a/src/document/services/document.service.ts +++ b/src/document/services/document.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; -import { UploadDocumentRequestDto } from '../dtos/request'; +import { GenerateUploadSignedUrlRequestDto, UploadDocumentRequestDto } from '../dtos/request'; import { Document } from '../entities'; import { DocumentRepository } from '../repositories'; import { OciService } from './oci.service'; @@ -24,4 +24,11 @@ export class DocumentService { this.logger.log(`finding document with id ${id}`); return this.documentRepository.findDocumentById(id); } + + generateUploadSignedUrl(body: GenerateUploadSignedUrlRequestDto) { + this.logger.log( + `generating signed URL for document type ${body.documentType} with original file name ${body.originalFileName}`, + ); + return this.ociService.generateUploadPreSignedUrl(body); + } } diff --git a/src/document/services/oci.service.ts b/src/document/services/oci.service.ts index 3e65a89..9c6002d 100644 --- a/src/document/services/oci.service.ts +++ b/src/document/services/oci.service.ts @@ -8,7 +8,7 @@ import { CreatePreauthenticatedRequestDetails } from 'oci-objectstorage/lib/mode import path from 'path'; import { CacheService } from '~/common/modules/cache/services'; import { BUCKETS } from '../constants'; -import { UploadDocumentRequestDto } from '../dtos/request'; +import { GenerateUploadSignedUrlRequestDto, UploadDocumentRequestDto } from '../dtos/request'; import { UploadResponseDto } from '../dtos/response'; import { Document } from '../entities'; import { generateNewFileName } from '../utils'; @@ -106,4 +106,47 @@ export class OciService { return document.name; } } + + async generateUploadPreSignedUrl({ + documentType, + originalFileName, + extension, + }: GenerateUploadSignedUrlRequestDto): Promise { + const bucketName = BUCKETS[documentType]; + + if (!bucketName) { + this.logger.error('Could not find bucket name for document type', documentType); + throw new BadRequestException('DOCUMENT.TYPE_NOT_SUPPORTED'); + } + + const objectName = generateNewFileName(originalFileName); + const expiration = moment().add('1', 'hours').toDate(); + + try { + this.logger.debug(`Generating pre-signed upload URL for object ${objectName} in bucket ${bucketName}`); + + const response = await this.ociClient.createPreauthenticatedRequest({ + namespaceName: this.namespace, + bucketName, + createPreauthenticatedRequestDetails: { + name: `upload-${objectName}`, + accessType: CreatePreauthenticatedRequestDetails.AccessType.ObjectWrite, // 🔑 for upload + timeExpires: expiration, + objectName: `${objectName}${extension}`, // Ensure the object name includes the extension + }, + }); + + this.logger.log(`Generated upload URL for ${objectName}`); + + return plainToInstance(UploadResponseDto, { + name: objectName, + extension, + url: response.preauthenticatedRequest.fullPath, + documentType, + }); + } catch (error) { + this.logger.error('Error generating pre-signed upload URL', error); + throw new BadRequestException('UPLOAD.URL_GENERATION_FAILED'); + } + } }