From dda603ae434fa02c4e3b664be90c94aa47b26d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ccaneryy=E2=80=9D?= Date: Mon, 29 Jun 2026 21:47:36 +0300 Subject: [PATCH] feat(documents): add DTO and Swagger docs for document access endpoints --- .../documents-download.controller.ts | 48 ++++++++++++++++++- src/documents/documents.controller.ts | 21 ++++++++ src/documents/dto/document-access.dto.ts | 47 ++++++++++++++++-- src/documents/dto/document.dto.ts | 6 +++ 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/src/documents/documents-download.controller.ts b/src/documents/documents-download.controller.ts index 8e5eaa5c..d1463d75 100644 --- a/src/documents/documents-download.controller.ts +++ b/src/documents/documents-download.controller.ts @@ -1,15 +1,30 @@ // @ts-nocheck -import { Body, Controller, Get, Param, Post, Query, Res, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiParam, + ApiQuery, + ApiBody, + ApiResponse, +} from '@nestjs/swagger'; import { Response } from 'express'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { AuthUserPayload } from '../auth/types/auth-user.type'; -import { DownloadDocumentDto, RequestSignedUploadDto } from './dto/document-access.dto'; +import { + DownloadDocumentDto, + RequestSignedUploadDto, + SignedUploadUrlResponseDto, +} from './dto/document-access.dto'; import { DocumentsService } from './documents.service'; import { SignedUrlService } from './signed-url/signed-url.service'; import { SignedUrlOperation } from './signed-url/signed-url-provider.interface'; +@ApiTags('Documents') +@ApiBearerAuth() @Controller('documents') @UseGuards(JwtAuthGuard) export class DocumentsDownloadController { @@ -24,6 +39,23 @@ export class DocumentsDownloadController { * Then we redirect to a short-lived signed GET URL. */ @Get(':id/download') + @ApiOperation({ + summary: 'Download a document', + description: 'Authorizes access and redirects to a short-lived signed download URL.', + }) + @ApiParam({ name: 'id', description: 'Document ID' }) + @ApiQuery({ + name: 'versionId', + required: false, + description: 'Optional version ID to download a specific version', + type: String, + }) + @ApiResponse({ + status: HttpStatus.FOUND, + description: 'Redirects to a signed download URL', + }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Document or version not found' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Access denied' }) async download( @Param('id') id: string, @Query() query: DownloadDocumentDto, @@ -61,6 +93,18 @@ export class DocumentsDownloadController { * Client uploads directly to object store, then calls document metadata create. */ @Post('signed-upload-url') + @ApiOperation({ + summary: 'Request a signed upload URL', + description: 'Returns a pre-signed URL for client-side direct upload to object storage.', + }) + @ApiBody({ type: RequestSignedUploadDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Signed upload URL generated', + type: SignedUploadUrlResponseDto, + }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid request data' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Access denied' }) async requestSignedUploadUrl( @Body() dto: RequestSignedUploadDto, @CurrentUser() user: AuthUserPayload, diff --git a/src/documents/documents.controller.ts b/src/documents/documents.controller.ts index b3b61612..6a26c797 100644 --- a/src/documents/documents.controller.ts +++ b/src/documents/documents.controller.ts @@ -11,7 +11,9 @@ import { Query, Res, UseGuards, + HttpStatus, } from '@nestjs/common'; +import { ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; import { Response } from 'express'; import { DocumentsService } from './documents.service'; import { @@ -130,6 +132,25 @@ export class DocumentsController { // ── #404 / #569 Bulk Download (with authorization) ────────────────────── @Post('bulk-download') + @ApiOperation({ + summary: 'Bulk download documents', + description: 'Downloads multiple authorized documents as a ZIP archive.', + }) + @ApiBody({ type: BulkDownloadDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'ZIP archive stream', + content: { + 'application/zip': { + schema: { type: 'string', format: 'binary' }, + }, + }, + }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'No documents found' }) + @ApiResponse({ + status: HttpStatus.FORBIDDEN, + description: 'Access denied to one or more documents', + }) bulkDownload( @Body() dto: BulkDownloadDto, @Res() res: Response, diff --git a/src/documents/dto/document-access.dto.ts b/src/documents/dto/document-access.dto.ts index d68e521f..1fefb67d 100644 --- a/src/documents/dto/document-access.dto.ts +++ b/src/documents/dto/document-access.dto.ts @@ -1,43 +1,82 @@ // @ts-nocheck -import { IsOptional, IsString, IsUUID } from 'class-validator'; +import { IsOptional, IsString, IsUUID, IsNumber, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; export class DownloadDocumentDto { + @ApiPropertyOptional({ + description: 'Optional document version ID to download a specific version', + format: 'uuid', + }) @IsOptional() @IsUUID() versionId?: string; } export class RequestSignedUploadDto { + @ApiPropertyOptional({ description: 'Property ID the document belongs to', format: 'uuid' }) + @IsOptional() @IsUUID() propertyId?: string; + @ApiPropertyOptional({ + description: 'Transaction ID the document is associated with', + format: 'uuid', + }) + @IsOptional() @IsUUID() - // transactionId can be null depending on document context; optional in controller transactionId?: string; + @ApiProperty({ description: 'Original file name', example: 'contract.pdf' }) @IsString() fileName: string; + @ApiProperty({ description: 'MIME type of the file', example: 'application/pdf' }) @IsString() mimeType: string; + @ApiProperty({ description: 'File size in bytes', example: 102400 }) + @Type(() => Number) + @IsNumber() + @Min(1) fileSizeBytes: number; + @ApiPropertyOptional({ + description: 'Existing document ID when replacing a document', + format: 'uuid', + }) @IsOptional() @IsUUID() - documentId?: string; // if updating an existing doc + documentId?: string; + @ApiPropertyOptional({ description: 'Document category', example: 'CONTRACT' }) @IsOptional() @IsString() category?: string; + @ApiPropertyOptional({ description: 'Document description' }) @IsOptional() @IsString() description?: string; + @ApiPropertyOptional({ description: 'Dispute ID the document is linked to', format: 'uuid' }) @IsOptional() @IsUUID() - // optional dispute binding disputeId?: string; } + +export class SignedUploadUrlResponseDto { + @ApiProperty({ description: 'Pre-signed URL for direct client upload' }) + url: string; + + @ApiProperty({ description: 'Object storage key for the uploaded file' }) + objectKey: string; + + @ApiProperty({ + description: 'Expiration time of the signed URL', + type: String, + format: 'date-time', + }) + expiresAt: Date; +} diff --git a/src/documents/dto/document.dto.ts b/src/documents/dto/document.dto.ts index 76204d00..904e0c54 100644 --- a/src/documents/dto/document.dto.ts +++ b/src/documents/dto/document.dto.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { IsString, IsOptional, IsArray, IsDateString, IsBoolean, IsIn } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export const DOCUMENT_TYPE_ENUM = [ 'TITLE_DEED', @@ -85,6 +86,11 @@ export class SignDocumentDto { } export class BulkDownloadDto { + @ApiProperty({ + description: 'Document IDs to include in the ZIP archive', + type: [String], + example: ['550e8400-e29b-41d4-a716-446655440000'], + }) @IsArray() @IsString({ each: true }) documentIds: string[];