Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions src/documents/documents-download.controller.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions src/documents/documents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
47 changes: 43 additions & 4 deletions src/documents/dto/document-access.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 6 additions & 0 deletions src/documents/dto/document.dto.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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[];
Expand Down
Loading