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:
@@ -0,0 +1,58 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from '../config/env.js';
|
||||
import { pool } from '../config/db.js';
|
||||
import { fail } from '../utils/http.js';
|
||||
|
||||
export async function requireAuth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||
if (!token) return fail(res, 401, 'No autenticado');
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = jwt.verify(token, env.jwtSecret);
|
||||
} catch {
|
||||
return fail(res, 401, 'Token invalido o expirado');
|
||||
}
|
||||
|
||||
// Revalidar en cada request que la cuenta siga activa (un JWT firmado no basta).
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.is_active, u.department_id, r.code AS role_code
|
||||
FROM users u JOIN roles r ON r.id = u.role_id
|
||||
WHERE u.id = ? LIMIT 1`,
|
||||
[payload.sub]
|
||||
);
|
||||
const user = rows[0];
|
||||
if (!user || !user.is_active) return fail(res, 401, 'Cuenta inactiva o inexistente');
|
||||
|
||||
req.currentUser = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
roleCode: user.role_code,
|
||||
departmentId: user.department_id,
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
export function requireRole(...roleCodes) {
|
||||
return (req, res, next) => {
|
||||
if (!req.currentUser) return fail(res, 401, 'No autenticado');
|
||||
if (!roleCodes.includes(req.currentUser.roleCode)) {
|
||||
return fail(res, 403, 'No tienes permisos para esta accion');
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export const ROLES = {
|
||||
ADMIN: 'admin',
|
||||
IT_MANAGER: 'it_manager',
|
||||
PROJECT_LEAD: 'project_lead',
|
||||
COLLABORATOR: 'collaborator',
|
||||
VIEWER: 'viewer',
|
||||
};
|
||||
|
||||
export const CAN_MANAGE_CATALOGS = [ROLES.ADMIN];
|
||||
export const CAN_MANAGE_USERS = [ROLES.ADMIN];
|
||||
export const CAN_WRITE = [ROLES.ADMIN, ROLES.IT_MANAGER, ROLES.PROJECT_LEAD, ROLES.COLLABORATOR];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { HttpError } from '../utils/http.js';
|
||||
|
||||
export function notFoundHandler(req, res) {
|
||||
res.status(404).json({ success: false, data: null, error: 'Ruta no encontrada' });
|
||||
}
|
||||
|
||||
export function errorHandler(err, req, res, next) { // eslint-disable-line no-unused-vars
|
||||
if (err instanceof HttpError) {
|
||||
return res.status(err.status).json({ success: false, data: null, error: err.message });
|
||||
}
|
||||
console.error('[error]', err);
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
data: null,
|
||||
error: isProd ? 'Error interno' : err.message,
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user