chore: estructura inicial del MVP

Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el
nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias,
dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-03 08:37:58 -05:00
commit 1d22adc42e
135 changed files with 10913 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import multer from 'multer';
import { env } from '../config/env.js';
const ALLOWED_MIME = new Set([
'application/pdf',
'image/png',
'image/jpeg',
'image/webp',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/plain',
'application/zip',
]);
const uploadsDir = path.join(env.storageRoot, 'attachments');
fs.mkdirSync(uploadsDir, { recursive: true });
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadsDir),
// Nombre generado por el servidor: evita path traversal y colisiones.
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).slice(0, 10);
cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`);
},
});
export const upload = multer({
storage,
limits: { fileSize: 15 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (!ALLOWED_MIME.has(file.mimetype)) {
return cb(new Error('Tipo de archivo no permitido'));
}
cb(null, true);
},
});
export { uploadsDir };