import { Request, Response } from 'express'; import crypto from 'crypto'; import path from 'path'; import fs from 'fs'; import { Document } from '@models/Document'; import { User } from '@models/User'; import { WorkflowRequest } from '@models/WorkflowRequest'; import { ResponseHandler } from '@utils/responseHandler'; import { activityService } from '@services/activity.service'; import { gcsStorageService } from '@services/gcsStorage.service'; import type { AuthenticatedRequest } from '../types/express'; import { getRequestMetadata } from '@utils/requestUtils'; import { getConfigNumber, getConfigValue } from '@services/configReader.service'; import { logDocumentEvent, logWithContext } from '@utils/logger'; export class DocumentController { async upload(req: AuthenticatedRequest, res: Response): Promise { try { const userId = req.user?.userId; if (!userId) { ResponseHandler.error(res, 'Unauthorized', 401); return; } // Extract requestId from body (multer should parse form fields) // Try both req.body and req.body.requestId for compatibility const identifier = String((req.body?.requestId || req.body?.request_id || '').trim()); if (!identifier || identifier === 'undefined' || identifier === 'null') { logWithContext('error', 'RequestId missing or invalid in document upload', { body: req.body, bodyKeys: Object.keys(req.body || {}), userId: req.user?.userId }); ResponseHandler.error(res, 'requestId is required', 400); return; } // Helper to check if identifier is UUID const isUuid = (id: string): boolean => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(id); }; // Get workflow request - handle both UUID (requestId) and requestNumber let workflowRequest: WorkflowRequest | null = null; if (isUuid(identifier)) { workflowRequest = await WorkflowRequest.findByPk(identifier); } else { workflowRequest = await WorkflowRequest.findOne({ where: { requestNumber: identifier } }); } if (!workflowRequest) { logWithContext('error', 'Workflow request not found for document upload', { identifier, isUuid: isUuid(identifier), userId: req.user?.userId }); ResponseHandler.error(res, 'Workflow request not found', 404); return; } // Get the actual requestId (UUID) and requestNumber const requestId = (workflowRequest as any).requestId || (workflowRequest as any).request_id; const requestNumber = (workflowRequest as any).requestNumber || (workflowRequest as any).request_number; if (!requestNumber) { logWithContext('error', 'Request number not found for workflow', { requestId, workflowRequest: JSON.stringify(workflowRequest.toJSON()), userId: req.user?.userId }); ResponseHandler.error(res, 'Request number not found for workflow', 500); return; } const file = (req as any).file as Express.Multer.File | undefined; if (!file) { ResponseHandler.error(res, 'No file uploaded', 400); return; } // Validate file size against database configuration const maxFileSizeMB = await getConfigNumber('MAX_FILE_SIZE_MB', 10); const maxFileSizeBytes = maxFileSizeMB * 1024 * 1024; if (file.size > maxFileSizeBytes) { ResponseHandler.error( res, `File size exceeds the maximum allowed size of ${maxFileSizeMB}MB. Current size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`, 400 ); return; } // Validate file type against database configuration const allowedFileTypesStr = await getConfigValue('ALLOWED_FILE_TYPES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif'); const allowedFileTypes = allowedFileTypesStr.split(',').map(ext => ext.trim().toLowerCase()); const fileExtension = path.extname(file.originalname).replace('.', '').toLowerCase(); if (!allowedFileTypes.includes(fileExtension)) { ResponseHandler.error( res, `File type "${fileExtension}" is not allowed. Allowed types: ${allowedFileTypes.join(', ')}`, 400 ); return; } // Get file buffer const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from('')); const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex'); const extension = path.extname(file.originalname).replace('.', '').toLowerCase(); const category = (req.body?.category as string) || 'OTHER'; // Upload with automatic fallback to local storage const uploadResult = await gcsStorageService.uploadFileWithFallback({ buffer: fileBuffer, originalName: file.originalname, mimeType: file.mimetype, requestNumber: requestNumber, fileType: 'documents' }); const storageUrl = uploadResult.storageUrl; const gcsFilePath = uploadResult.filePath; // Clean up local temporary file if it exists (from multer disk storage) if (file.path && fs.existsSync(file.path)) { try { fs.unlinkSync(file.path); } catch (unlinkError) { logWithContext('warn', 'Failed to delete local temporary file', { filePath: file.path }); } } const doc = await Document.create({ requestId, uploadedBy: userId, fileName: path.basename(file.filename || file.originalname), originalFileName: file.originalname, fileType: extension, fileExtension: extension, fileSize: file.size, filePath: gcsFilePath, // Store GCS path or local path storageUrl: storageUrl, // Store GCS URL or local URL mimeType: file.mimetype, checksum, isGoogleDoc: false, googleDocUrl: null as any, category, version: 1, parentDocumentId: null as any, isDeleted: false, downloadCount: 0, } as any); // Log document upload event logDocumentEvent('uploaded', doc.documentId, { requestId, userId, fileName: file.originalname, fileType: extension, fileSize: file.size, category, }); // Get user details for activity logging const user = await User.findByPk(userId); const uploaderName = (user as any)?.displayName || (user as any)?.email || 'User'; // Log activity for document upload const requestMeta = getRequestMetadata(req); await activityService.log({ requestId, type: 'document_added', user: { userId, name: uploaderName }, timestamp: new Date().toISOString(), action: 'Document Added', details: `Added ${file.originalname} as supporting document by ${uploaderName}`, metadata: { fileName: file.originalname, fileSize: file.size, fileType: extension, category }, ipAddress: requestMeta.ipAddress, userAgent: requestMeta.userAgent }); ResponseHandler.success(res, doc, 'File uploaded', 201); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; const errorStack = error instanceof Error ? error.stack : undefined; logWithContext('error', 'Document upload failed', { userId: req.user?.userId, requestId: req.body?.requestId || req.body?.request_id, body: req.body, bodyKeys: Object.keys(req.body || {}), file: req.file ? { originalname: req.file.originalname, size: req.file.size, mimetype: req.file.mimetype, hasBuffer: !!req.file.buffer, hasPath: !!req.file.path } : 'No file', error: message, stack: errorStack }); ResponseHandler.error(res, 'Upload failed', 500, message); } } }