2025-06-15 16:32:34 +02:00
|
|
|
import { requireAuth } from '~/server/utils/auth';
|
2025-06-10 14:52:39 +02:00
|
|
|
import { uploadFile, getMinioClient } from '~/server/utils/minio';
|
2025-06-04 16:32:50 +02:00
|
|
|
import formidable from 'formidable';
|
|
|
|
|
import { promises as fs } from 'fs';
|
|
|
|
|
import mime from 'mime-types';
|
|
|
|
|
|
|
|
|
|
export default defineEventHandler(async (event) => {
|
2025-06-15 16:32:34 +02:00
|
|
|
// Check authentication (x-tag header OR Keycloak session)
|
|
|
|
|
await requireAuth(event);
|
2025-06-10 16:48:40 +02:00
|
|
|
|
2025-06-04 16:32:50 +02:00
|
|
|
try {
|
2025-06-10 14:52:39 +02:00
|
|
|
// Get the current path and bucket from query params
|
2025-06-04 16:32:50 +02:00
|
|
|
const query = getQuery(event);
|
|
|
|
|
const currentPath = (query.path as string) || '';
|
2025-06-10 14:52:39 +02:00
|
|
|
const bucket = (query.bucket as string) || 'client-portal'; // Default bucket
|
2025-06-04 16:32:50 +02:00
|
|
|
|
2025-06-10 16:48:40 +02:00
|
|
|
console.log('[Upload] Request received for bucket:', bucket, 'path:', currentPath);
|
|
|
|
|
|
2025-06-04 16:32:50 +02:00
|
|
|
// Parse multipart form data
|
|
|
|
|
const form = formidable({
|
|
|
|
|
maxFileSize: 50 * 1024 * 1024, // 50MB limit
|
|
|
|
|
keepExtensions: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [fields, files] = await form.parse(event.node.req);
|
|
|
|
|
|
|
|
|
|
// Handle multiple files
|
|
|
|
|
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
for (const uploadedFile of uploadedFiles) {
|
|
|
|
|
if (!uploadedFile) continue;
|
|
|
|
|
|
|
|
|
|
// Read file buffer
|
|
|
|
|
const fileBuffer = await fs.readFile(uploadedFile.filepath);
|
|
|
|
|
|
|
|
|
|
// Generate unique filename to prevent collisions
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
const sanitizedName = uploadedFile.originalFilename?.replace(/[^a-zA-Z0-9.-]/g, '_') || 'file';
|
|
|
|
|
const fileName = `${timestamp}-${sanitizedName}`;
|
|
|
|
|
|
|
|
|
|
// Construct full path including current folder
|
2025-06-10 15:27:57 +02:00
|
|
|
// Ensure currentPath ends with / if it's a folder
|
|
|
|
|
let normalizedPath = currentPath;
|
|
|
|
|
if (normalizedPath && !normalizedPath.endsWith('/')) {
|
|
|
|
|
normalizedPath += '/';
|
|
|
|
|
}
|
|
|
|
|
const fullPath = normalizedPath ? `${normalizedPath}${fileName}` : fileName;
|
2025-06-04 16:32:50 +02:00
|
|
|
|
|
|
|
|
// Get content type
|
|
|
|
|
const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/octet-stream';
|
|
|
|
|
|
2025-06-10 14:52:39 +02:00
|
|
|
// Upload to MinIO - handle different buckets
|
|
|
|
|
if (bucket === 'client-portal') {
|
|
|
|
|
await uploadFile(fullPath, fileBuffer, contentType);
|
|
|
|
|
} else {
|
|
|
|
|
// For other buckets, use the MinIO client directly
|
|
|
|
|
const client = getMinioClient();
|
2025-06-10 16:48:40 +02:00
|
|
|
|
|
|
|
|
// Ensure bucket exists
|
|
|
|
|
try {
|
|
|
|
|
await client.bucketExists(bucket);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`[Upload] Bucket ${bucket} doesn't exist, creating it...`);
|
|
|
|
|
await client.makeBucket(bucket, 'us-east-1');
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 14:52:39 +02:00
|
|
|
await client.putObject(bucket, fullPath, fileBuffer, fileBuffer.length, {
|
|
|
|
|
'Content-Type': contentType,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-04 16:32:50 +02:00
|
|
|
|
|
|
|
|
// Clean up temp file
|
|
|
|
|
await fs.unlink(uploadedFile.filepath);
|
|
|
|
|
|
|
|
|
|
results.push({
|
|
|
|
|
fileName: fullPath,
|
2025-06-10 14:52:39 +02:00
|
|
|
path: fullPath,
|
2025-06-04 16:32:50 +02:00
|
|
|
originalName: uploadedFile.originalFilename,
|
|
|
|
|
size: uploadedFile.size,
|
|
|
|
|
contentType,
|
2025-06-10 14:52:39 +02:00
|
|
|
bucket: bucket
|
2025-06-04 16:32:50 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Log audit event
|
|
|
|
|
await logAuditEvent(event, 'upload', fullPath, uploadedFile.size);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 14:52:39 +02:00
|
|
|
// Return the first file's info for single file uploads (backward compatibility)
|
|
|
|
|
if (results.length === 1) {
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
path: results[0].path,
|
|
|
|
|
fileName: results[0].fileName,
|
|
|
|
|
files: results,
|
|
|
|
|
message: `File uploaded successfully`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 16:32:50 +02:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
files: results,
|
|
|
|
|
message: `${results.length} file(s) uploaded successfully`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Failed to upload file:', error);
|
|
|
|
|
throw createError({
|
|
|
|
|
statusCode: 500,
|
|
|
|
|
statusMessage: error.message || 'Failed to upload file',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Audit logging helper
|
|
|
|
|
async function logAuditEvent(event: any, action: string, filePath: string, fileSize?: number) {
|
|
|
|
|
try {
|
|
|
|
|
const user = event.context.user || { email: 'anonymous' };
|
|
|
|
|
const auditLog = {
|
|
|
|
|
user_email: user.email,
|
|
|
|
|
action,
|
|
|
|
|
file_path: filePath,
|
|
|
|
|
file_size: fileSize,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
ip_address: getClientIP(event),
|
|
|
|
|
success: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// You can store this in your database or logging system
|
|
|
|
|
console.log('Audit log:', auditLog);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to log audit event:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getClientIP(event: any): string {
|
|
|
|
|
return event.node.req.headers['x-forwarded-for'] ||
|
|
|
|
|
event.node.req.connection.remoteAddress ||
|
|
|
|
|
'unknown';
|
|
|
|
|
}
|