feat: generate upload signed url for oci

This commit is contained in:
Abdalhamid Alhamad
2025-08-03 14:21:14 +03:00
parent fce720237f
commit f65a7d2933
5 changed files with 78 additions and 40 deletions

View File

@ -1,15 +1,9 @@
import { Body, Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { memoryStorage } from 'multer';
import { IJwtPayload } from '~/auth/interfaces';
import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards'; import { AccessTokenGuard } from '~/common/guards';
import { ApiLangRequestHeader } from '~/core/decorators'; import { ApiLangRequestHeader } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils'; import { ResponseFactory } from '~/core/utils';
import { UploadDocumentRequestDto } from '../dtos/request'; import { GenerateUploadSignedUrlRequestDto } from '../dtos/request';
import { DocumentMetaResponseDto } from '../dtos/response';
import { DocumentType } from '../enums';
import { DocumentService } from '../services'; import { DocumentService } from '../services';
@Controller('document') @Controller('document')
@ApiTags('Document') @ApiTags('Document')
@ -19,34 +13,9 @@ import { DocumentService } from '../services';
export class DocumentController { export class DocumentController {
constructor(private readonly documentService: DocumentService) {} constructor(private readonly documentService: DocumentService) {}
@Post() @Post('signed-url')
@ApiConsumes('multipart/form-data') async generateSignedUrl(@Body() body: GenerateUploadSignedUrlRequestDto) {
@ApiBody({ const signedUrl = await this.documentService.generateUploadSignedUrl(body);
schema: { return ResponseFactory.data(signedUrl);
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));
} }
} }

View File

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

View File

@ -1 +1,2 @@
export * from './generate-upload-signed-url.request.dto';
export * from './upload-document.request.dto'; export * from './upload-document.request.dto';

View File

@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm'; import { FindOptionsWhere } from 'typeorm';
import { UploadDocumentRequestDto } from '../dtos/request'; import { GenerateUploadSignedUrlRequestDto, UploadDocumentRequestDto } from '../dtos/request';
import { Document } from '../entities'; import { Document } from '../entities';
import { DocumentRepository } from '../repositories'; import { DocumentRepository } from '../repositories';
import { OciService } from './oci.service'; import { OciService } from './oci.service';
@ -24,4 +24,11 @@ export class DocumentService {
this.logger.log(`finding document with id ${id}`); this.logger.log(`finding document with id ${id}`);
return this.documentRepository.findDocumentById(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);
}
} }

View File

@ -8,7 +8,7 @@ import { CreatePreauthenticatedRequestDetails } from 'oci-objectstorage/lib/mode
import path from 'path'; import path from 'path';
import { CacheService } from '~/common/modules/cache/services'; import { CacheService } from '~/common/modules/cache/services';
import { BUCKETS } from '../constants'; import { BUCKETS } from '../constants';
import { UploadDocumentRequestDto } from '../dtos/request'; import { GenerateUploadSignedUrlRequestDto, UploadDocumentRequestDto } from '../dtos/request';
import { UploadResponseDto } from '../dtos/response'; import { UploadResponseDto } from '../dtos/response';
import { Document } from '../entities'; import { Document } from '../entities';
import { generateNewFileName } from '../utils'; import { generateNewFileName } from '../utils';
@ -106,4 +106,47 @@ export class OciService {
return document.name; return document.name;
} }
} }
async generateUploadPreSignedUrl({
documentType,
originalFileName,
extension,
}: GenerateUploadSignedUrlRequestDto): Promise<UploadResponseDto> {
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');
}
}
} }