@@ -26,8 +26,10 @@ import {
2626 ApiOkResponse ,
2727 ApiOperation ,
2828 ApiParam ,
29+ ApiProduces ,
2930 ApiQuery ,
3031 ApiTags ,
32+ ApiUnauthorizedResponse ,
3133} from "@nestjs/swagger" ;
3234import { Request , Response } from "express" ;
3335import { AuditService } from "@/audit/audit.service" ;
@@ -47,6 +49,7 @@ import { type DocumentData, DocumentService } from "./document.service";
4749import { ApproveDocumentDto } from "./dto/approve-document.dto" ;
4850import { OcrResultResponseDto } from "./dto/ocr-result-response.dto" ;
4951import { UpdateDocumentDto } from "./dto/update-document.dto" ;
52+ import { getContentTypeFromFilename } from "./mime-from-filename" ;
5053
5154@ApiTags ( "Documents" )
5255@Controller ( "api/documents" )
@@ -388,15 +391,81 @@ export class DocumentController {
388391 }
389392 }
390393
394+ @Get ( "/:documentId/view" )
395+ @HttpCode ( HttpStatus . OK )
396+ @Identity ( { allowApiKey : true } )
397+ @ApiOperation ( {
398+ summary : "View normalized document as PDF" ,
399+ description :
400+ "Streams the normalized PDF used for in-app display and OCR. Always application/pdf." ,
401+ } )
402+ @ApiParam ( { name : "documentId" , description : "Document ID" } )
403+ @ApiProduces ( "application/pdf" )
404+ @ApiOkResponse ( {
405+ description : "Normalized PDF bytes (Content-Type: application/pdf)" ,
406+ } )
407+ @ApiNotFoundResponse ( {
408+ description : "Document not found or normalized PDF unavailable" ,
409+ } )
410+ @ApiUnauthorizedResponse ( { description : "Not authenticated" } )
411+ @ApiForbiddenResponse ( { description : "Access denied: not a group member" } )
412+ async viewDocument (
413+ @Param ( "documentId" ) documentId : string ,
414+ @Res ( ) res : Response ,
415+ @Req ( ) req : Request ,
416+ ) : Promise < void > {
417+ this . logger . debug ( `=== DocumentController.viewDocument ===` ) ;
418+ this . logger . debug ( `Document ID: ${ documentId } ` ) ;
419+
420+ const document = await this . documentService . findDocument ( documentId ) ;
421+ if ( ! document ) {
422+ throw new NotFoundException ( `Document not found: ${ documentId } ` ) ;
423+ }
424+
425+ identityCanAccessGroup ( req . resolvedIdentity , document . group_id ) ;
426+
427+ if ( ! document . normalized_file_path ) {
428+ throw new NotFoundException (
429+ `Normalized PDF is not available for document: ${ documentId } ` ,
430+ ) ;
431+ }
432+
433+ await this . auditService . recordEvent ( {
434+ event_type : "document_accessed" ,
435+ resource_type : "document" ,
436+ resource_id : documentId ,
437+ actor_id : req . resolvedIdentity . actorId ,
438+ document_id : documentId ,
439+ group_id : document . group_id ?? undefined ,
440+ payload : { action : "view" } ,
441+ } ) ;
442+
443+ const fileBuffer = await this . blobStorage . read (
444+ document . normalized_file_path ,
445+ ) ;
446+
447+ res . setHeader ( "Content-Type" , "application/pdf" ) ;
448+ res . setHeader ( "Content-Disposition" , 'inline; filename="document.pdf"' ) ;
449+ res . setHeader ( "Content-Length" , fileBuffer . length ) ;
450+ res . send ( fileBuffer ) ;
451+
452+ this . logger . debug ( "=== DocumentController.viewDocument completed ===" ) ;
453+ }
454+
391455 @Get ( "/:documentId/download" )
392456 @HttpCode ( HttpStatus . OK )
393- @Identity ( )
394- @ApiOperation ( { summary : "Download a document file by ID" } )
457+ @Identity ( { allowApiKey : true } )
458+ @ApiOperation ( {
459+ summary : "Download original uploaded file" ,
460+ description :
461+ "Serves the stored original blob (not the normalized PDF). Filename and type follow original_filename." ,
462+ } )
395463 @ApiParam ( { name : "documentId" , description : "Document ID" } )
396464 @ApiOkResponse ( {
397- description : "Returns the document file buffer as a download " ,
465+ description : "Returns the original file buffer" ,
398466 } )
399467 @ApiNotFoundResponse ( { description : "Document not found or file missing" } )
468+ @ApiUnauthorizedResponse ( { description : "Not authenticated" } )
400469 @ApiForbiddenResponse ( { description : "Access denied: not a group member" } )
401470 async downloadDocument (
402471 @Param ( "documentId" ) documentId : string ,
@@ -428,14 +497,8 @@ export class DocumentController {
428497 // Read file from blob storage using the blob key
429498 const fileBuffer = await this . blobStorage . read ( document . file_path ) ;
430499
431- // Set appropriate headers
432500 const fileName = document . original_filename || `document-${ documentId } ` ;
433- const mimeType =
434- document . file_type === "pdf"
435- ? "application/pdf"
436- : document . file_type === "image"
437- ? "image/jpeg"
438- : "application/octet-stream" ;
501+ const mimeType = getContentTypeFromFilename ( fileName ) ;
439502
440503 res . setHeader ( "Content-Type" , mimeType ) ;
441504 res . setHeader ( "Content-Disposition" , `inline; filename="${ fileName } "` ) ;
0 commit comments