215 lines
8.0 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|