Re_Backend/src/controllers/document.controller.ts

215 lines
8.0 KiB
TypeScript

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<void> {
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);
}
}
}