22 BadRequestException ,
33 ConflictException ,
44 Controller ,
5+ ForbiddenException ,
56 Get ,
67 NotFoundException ,
78 Param ,
@@ -15,11 +16,14 @@ import {
1516} from '@nestjs/common' ;
1617import { FileInterceptor } from '@nestjs/platform-express' ;
1718import { ConfigService } from '@nestjs/config' ;
19+ import { ApiBearerAuth , ApiConsumes , ApiOperation , ApiResponse , ApiTags } from '@nestjs/swagger' ;
1820import { Request , Response } from 'express' ;
1921import { createHash } from 'crypto' ;
2022import { promises as fs } from 'fs' ;
2123import { extname , join } from 'path' ;
2224import * as multer from 'multer' ;
25+ import * as PDFDocument from 'pdfkit' ;
26+ import * as ExcelJS from 'exceljs' ;
2327
2428import { DocumentsService } from './documents.service' ;
2529import { DocumentStatus } from './entities/document.entity' ;
@@ -38,10 +42,11 @@ const fileFilter: multer.FileFilterCallback = (_req, file, callback) => {
3842 if ( ALLOWED_MIME_TYPES . includes ( file . mimetype ) ) {
3943 return callback ( null , true ) ;
4044 }
41-
4245 return callback ( new BadRequestException ( 'Only PDF, PNG, or JPEG files are allowed' ) , false ) ;
4346} ;
4447
48+ @ApiTags ( 'Documents' )
49+ @ApiBearerAuth ( 'JWT-auth' )
4550@Controller ( 'documents' )
4651export class DocumentsController {
4752 constructor (
@@ -68,38 +73,31 @@ export class DocumentsController {
6873
6974 @Post ( 'upload' )
7075 @UseGuards ( JwtAuthGuard )
71- @UseInterceptors (
72- FileInterceptor ( 'file' , {
73- storage : multerStorage ,
74- fileFilter ,
75- limits : { fileSize : MAX_FILE_SIZE_BYTES } ,
76- } ) ,
77- )
76+ @UseInterceptors ( FileInterceptor ( 'file' , { storage : multerStorage , fileFilter , limits : { fileSize : MAX_FILE_SIZE_BYTES } } ) )
77+ @ ApiOperation ( { summary : 'Upload a document for analysis' } )
78+ @ ApiConsumes ( 'multipart/form-data' )
79+ @ ApiResponse ( { status : 202 , description : 'Document accepted for processing' } )
80+ @ ApiResponse ( { status : 200 , description : 'Document already exists (duplicate)' } )
81+ @ ApiResponse ( { status : 400 , description : 'Invalid file type or missing file' } )
82+ @ ApiResponse ( { status : 401 , description : 'Unauthenticated' } )
7883 async uploadDocument (
7984 @UploadedFile ( ) file : Express . Multer . File ,
8085 @Req ( ) req : Request & { user ?: User } ,
8186 @Res ( ) res : Response ,
8287 ) {
83- if ( ! file ) {
84- throw new BadRequestException ( 'File is required' ) ;
85- }
88+ if ( ! file ) throw new BadRequestException ( 'File is required' ) ;
8689
8790 const user = req . user ;
88- if ( ! user ) {
89- throw new BadRequestException ( 'Authenticated user is required' ) ;
90- }
91+ if ( ! user ) throw new BadRequestException ( 'Authenticated user is required' ) ;
9192
9293 const fileHash = createHash ( 'sha256' ) . update ( file . buffer ) . digest ( 'hex' ) ;
9394 const existing = await this . documentsService . findByFileHash ( fileHash ) ;
94- if ( existing ) {
95- return res . status ( 200 ) . send ( existing ) ;
96- }
95+ if ( existing ) return res . status ( 200 ) . send ( existing ) ;
9796
9897 const uploadDir = this . configService . get < string > ( 'UPLOAD_DIR' ) || './uploads' ;
9998 await fs . mkdir ( uploadDir , { recursive : true } ) ;
10099
101- const extension = extname ( file . originalname ) || '' ;
102- const filename = `${ fileHash } ${ extension } ` ;
100+ const filename = `${ fileHash } ${ extname ( file . originalname ) || '' } ` ;
103101 const targetPath = join ( uploadDir , filename ) ;
104102 await fs . writeFile ( targetPath , file . buffer ) ;
105103
@@ -117,39 +115,101 @@ export class DocumentsController {
117115 return res . status ( 202 ) . send ( document ) ;
118116 }
119117
118+ // BE-39: Excel export — static route must come before dynamic :id routes
119+ @Get ( 'export/excel' )
120+ @UseGuards ( JwtAuthGuard )
121+ @ApiOperation ( { summary : 'Export all user documents as Excel (.xlsx)' } )
122+ @ApiResponse ( { status : 200 , description : 'Excel file download' } )
123+ async exportExcel ( @Req ( ) req : Request & { user ?: User } , @Res ( ) res : Response ) {
124+ const documents = await this . documentsService . findByOwner ( req . user ! . id ) ;
125+
126+ const workbook = new ExcelJS . Workbook ( ) ;
127+ const sheet = workbook . addWorksheet ( 'Documents' ) ;
128+ sheet . columns = [
129+ { header : 'ID' , key : 'id' , width : 36 } ,
130+ { header : 'Title' , key : 'title' , width : 30 } ,
131+ { header : 'Status' , key : 'status' , width : 12 } ,
132+ { header : 'Risk Score' , key : 'riskScore' , width : 12 } ,
133+ { header : 'Risk Flags' , key : 'riskFlags' , width : 40 } ,
134+ { header : 'Uploaded At' , key : 'createdAt' , width : 24 } ,
135+ ] ;
136+ documents . forEach ( ( doc ) =>
137+ sheet . addRow ( {
138+ ...doc ,
139+ riskFlags : doc . riskFlags ?. join ( ', ' ) ?? '' ,
140+ } ) ,
141+ ) ;
142+
143+ res . setHeader ( 'Content-Type' , 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) ;
144+ res . setHeader ( 'Content-Disposition' , 'attachment; filename="documents.xlsx"' ) ;
145+ await workbook . xlsx . write ( res ) ;
146+ res . end ( ) ;
147+ }
148+
120149 @Post ( ':id/verify' )
121150 @UseGuards ( JwtAuthGuard )
122- async verifyDocument ( @Param ( 'id' ) id : string , @Res ( ) res : Response ) {
151+ @ApiOperation ( { summary : 'Queue a document for Stellar blockchain verification' } )
152+ @ApiResponse ( { status : 202 , description : 'Verification queued' } )
153+ @ApiResponse ( { status : 403 , description : 'Not the document owner' } )
154+ @ApiResponse ( { status : 404 , description : 'Document not found' } )
155+ @ApiResponse ( { status : 409 , description : 'Already verified' } )
156+ async verifyDocument (
157+ @Param ( 'id' ) id : string ,
158+ @Req ( ) req : Request & { user ?: User } ,
159+ @Res ( ) res : Response ,
160+ ) {
123161 const document = await this . documentsService . findById ( id ) ;
124- if ( ! document ) {
125- throw new NotFoundException ( 'Document not found' ) ;
126- }
127-
128- if ( document . status === DocumentStatus . VERIFIED ) {
129- throw new ConflictException ( 'Document has already been verified' ) ;
130- }
162+ if ( ! document ) throw new NotFoundException ( 'Document not found' ) ;
163+ if ( document . ownerId !== req . user ?. id ) throw new ForbiddenException ( 'Access denied' ) ;
164+ if ( document . status === DocumentStatus . VERIFIED ) throw new ConflictException ( 'Document has already been verified' ) ;
131165
132166 await this . queueService . enqueueAnchor ( document . id ) ;
133-
134- return res . status ( 202 ) . json ( {
135- message : 'Verification queued' ,
136- documentId : document . id ,
137- } ) ;
167+ return res . status ( 202 ) . json ( { message : 'Verification queued' , documentId : document . id } ) ;
138168 }
139169
140170 @Get ( ':id/verification' )
141171 @UseGuards ( JwtAuthGuard )
172+ @ApiOperation ( { summary : 'Get the latest verification record for a document' } )
173+ @ApiResponse ( { status : 200 , description : 'Verification record' } )
174+ @ApiResponse ( { status : 404 , description : 'Document or record not found' } )
142175 async getVerification ( @Param ( 'id' ) id : string ) {
143176 const document = await this . documentsService . findById ( id ) ;
144- if ( ! document ) {
145- throw new NotFoundException ( 'Document not found' ) ;
146- }
177+ if ( ! document ) throw new NotFoundException ( 'Document not found' ) ;
147178
148179 const record = await this . verificationService . findLatestByDocument ( id ) ;
149- if ( ! record ) {
150- throw new NotFoundException ( 'No verification record found for this document' ) ;
151- }
180+ if ( ! record ) throw new NotFoundException ( 'No verification record found for this document' ) ;
152181
153182 return record ;
154183 }
184+
185+ // BE-39: PDF export
186+ @Get ( ':id/export/pdf' )
187+ @UseGuards ( JwtAuthGuard )
188+ @ApiOperation ( { summary : 'Export a single document report as PDF' } )
189+ @ApiResponse ( { status : 200 , description : 'PDF file download' } )
190+ @ApiResponse ( { status : 404 , description : 'Document not found' } )
191+ async exportPdf ( @Param ( 'id' ) id : string , @Res ( ) res : Response ) {
192+ const document = await this . documentsService . findById ( id ) ;
193+ if ( ! document ) throw new NotFoundException ( 'Document not found' ) ;
194+
195+ const verification = await this . verificationService . findLatestByDocument ( id ) ;
196+
197+ const doc = new PDFDocument ( { margin : 50 } ) ;
198+ res . setHeader ( 'Content-Type' , 'application/pdf' ) ;
199+ res . setHeader ( 'Content-Disposition' , `attachment; filename="${ document . title } .pdf"` ) ;
200+ doc . pipe ( res ) ;
201+
202+ doc . fontSize ( 18 ) . text ( 'Document Report' , { align : 'center' } ) . moveDown ( ) ;
203+ doc . fontSize ( 12 )
204+ . text ( `Title: ${ document . title } ` )
205+ . text ( `Uploaded: ${ document . createdAt . toISOString ( ) } ` )
206+ . text ( `Status: ${ document . status } ` )
207+ . text ( `Risk Score: ${ document . riskScore ?? 'N/A' } ` )
208+ . text ( `Risk Flags: ${ document . riskFlags ?. join ( ', ' ) || 'None' } ` )
209+ . moveDown ( )
210+ . text ( `Stellar Tx Hash: ${ verification ?. stellarTxHash ?? 'Not anchored' } ` )
211+ . text ( `Stellar Ledger: ${ verification ?. stellarLedger ?? 'N/A' } ` ) ;
212+
213+ doc . end ( ) ;
214+ }
155215}
0 commit comments