Registro y elevación de componentes base (ADN, Scripts, Servicios)

This commit is contained in:
Ricardo Monla
2026-02-15 20:37:53 -03:00
parent 9e9bf62c68
commit c865a7772b
25279 changed files with 3354766 additions and 0 deletions
@@ -0,0 +1,40 @@
const { executeQuery } = require('./database');
async function checkTasks() {
try {
console.log('📊 VERIFICANDO TAREAS EN BASE DE DATOS');
console.log('=====================================');
const result = await executeQuery(
'SELECT dtic_id, title, status, priority, created_at FROM dtic.tareas ORDER BY created_at DESC LIMIT 10'
);
if (result.rows.length === 0) {
console.log('❌ No se encontraron tareas en la base de datos');
return;
}
console.log(`✅ Encontradas ${result.rows.length} tareas:`);
console.log('');
result.rows.forEach((task, index) => {
console.log(`${index + 1}. ${task.dtic_id} - ${task.title}`);
console.log(` Estado: ${task.status} | Prioridad: ${task.priority}`);
console.log(` Creado: ${new Date(task.created_at).toLocaleString('es-AR')}`);
console.log('');
});
// Verificar total
const countResult = await executeQuery('SELECT COUNT(*) as total FROM dtic.tareas');
console.log(`📈 Total de tareas en BD: ${countResult.rows[0].total}`);
} catch (error) {
console.error('❌ Error verificando datos:', error);
}
}
if (require.main === module) {
checkTasks();
}
module.exports = { checkTasks };
@@ -0,0 +1,50 @@
const { executeQuery } = require('./database');
const bcrypt = require('bcryptjs');
async function checkPassword() {
try {
console.log('🔐 VERIFICANDO CONTRASEÑA DEL USUARIO');
console.log('=====================================');
const result = await executeQuery(
'SELECT password_hash FROM dtic.tecnicos WHERE email = $1',
['rmonla@frlr.utn.edu.ar']
);
if (result.rows.length === 0) {
console.log('❌ Usuario no encontrado');
return;
}
const hash = result.rows[0].password_hash;
console.log(`Hash encontrado: ${hash ? 'SÍ' : 'NO'}`);
if (hash) {
const testPasswords = ['utn123', 'password', 'admin123', '123456'];
console.log('');
console.log('🧪 Probando contraseñas comunes:');
for (const password of testPasswords) {
const isValid = await bcrypt.compare(password, hash);
console.log(` "${password}": ${isValid ? '✅ VÁLIDA' : '❌ Inválida'}`);
}
console.log('');
console.log('💡 Si ninguna contraseña funciona, el usuario necesita reset de contraseña');
console.log(' O la contraseña fue cambiada desde la última vez que se configuró');
} else {
console.log('⚠️ El usuario no tiene contraseña configurada');
}
} catch (error) {
console.error('❌ Error verificando contraseña:', error);
}
}
if (require.main === module) {
checkPassword();
}
module.exports = { checkPassword };
@@ -0,0 +1,52 @@
const { executeQuery } = require('./database');
async function checkUser() {
try {
console.log('🔍 VERIFICANDO USUARIO: rmonla@frlr.utn.edu.ar');
console.log('==============================================');
const result = await executeQuery(
'SELECT id, dtic_id, email, first_name, last_name, password_hash, is_active, role FROM dtic.tecnicos WHERE email = $1',
['rmonla@frlr.utn.edu.ar']
);
if (result.rows.length === 0) {
console.log('❌ Usuario NO encontrado en la base de datos');
console.log('');
console.log('🔧 POSIBLES SOLUCIONES:');
console.log('1. El usuario no existe - crear usuario');
console.log('2. Email incorrecto - verificar ortografía');
console.log('3. Base de datos vacía - cargar datos iniciales');
return;
}
const user = result.rows[0];
console.log('✅ Usuario encontrado:');
console.log(` ID: ${user.id}`);
console.log(` DTIC ID: ${user.dtic_id}`);
console.log(` Email: ${user.email}`);
console.log(` Nombre: ${user.first_name} ${user.last_name}`);
console.log(` Rol: ${user.role}`);
console.log(` Activo: ${user.is_active ? 'SÍ' : 'NO'}`);
console.log(` Tiene contraseña: ${!!user.password_hash ? 'SÍ' : 'NO'}`);
if (!user.is_active) {
console.log('');
console.log('⚠️ USUARIO INACTIVO - No puede iniciar sesión');
}
if (!user.password_hash) {
console.log('');
console.log('⚠️ USUARIO SIN CONTRASEÑA - Necesita configuración inicial');
}
} catch (error) {
console.error('❌ Error verificando usuario:', error);
}
}
if (require.main === module) {
checkUser();
}
module.exports = { checkUser };
@@ -0,0 +1,31 @@
const { Pool } = require('pg');
require('dotenv').config();
// Configuración de PostgreSQL
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Establecer search_path para incluir el esquema dtic
pool.on('connect', (client) => {
client.query('SET search_path TO dtic, public');
});
// Función helper para ejecutar queries
const executeQuery = async (query, params = []) => {
const client = await pool.connect();
try {
const result = await client.query(query, params);
return result;
} catch (error) {
console.error('Database query error:', error);
throw error;
} finally {
client.release();
}
};
module.exports = { pool, executeQuery };
@@ -0,0 +1,245 @@
const fs = require('fs');
const path = require('path');
const { executeQuery } = require('./database');
// Función para leer todos los archivos de bitácoras
function readBitacoraFiles() {
// Dentro del contenedor Docker, el path es diferente
const bitacorasDir = path.join('/host', '_bitacoras_anteriores');
console.log('📂 Buscando bitácoras en:', bitacorasDir);
const files = fs.readdirSync(bitacorasDir);
const bitacoraFiles = files.filter(file => file.startsWith('bitacora_'));
console.log(`📄 Encontrados ${bitacoraFiles.length} archivos de bitácora`);
const data = {
recursos: [],
usuarios: [],
tareas: []
};
bitacoraFiles.forEach(file => {
const filePath = path.join(bitacorasDir, file);
const content = fs.readFileSync(filePath, 'utf8');
// Extraer recursos mencionados
extractRecursos(content, data.recursos);
// Extraer usuarios mencionados
extractUsuarios(content, data.usuarios);
// Extraer tareas mencionadas
extractTareas(content, data.tareas);
});
return data;
}
// Función para extraer recursos del contenido
function extractRecursos(content, recursos) {
// Patrones para identificar recursos
const resourcePatterns = [
/srvv-([A-Z0-9-]+)/g, // srvv-SITIO, srvv-DTIC, etc.
/dtic-([A-Z0-9-]+)/g, // dtic-BITÁCORAS, dtic-UPTIME, etc.
/srv-([A-Z0-9-]+)/g, // srv-PMOX1, srv-NS8, etc.
/pcv-([A-Z0-9-]+)/g, // pcv-DASU1, pcv-DASU2, etc.
/x_srvv-([A-Z0-9-]+)/g // x_srvv-TORII, etc.
];
resourcePatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(content)) !== null) {
const resourceName = match[0];
if (!recursos.find(r => r.name === resourceName)) {
recursos.push({
name: resourceName,
category: categorizeResource(resourceName),
description: `Recurso identificado en bitácoras históricas`,
status: 'available',
location: 'DTIC - UTN La Rioja'
});
}
}
});
}
// Función para categorizar recursos
function categorizeResource(name) {
if (name.startsWith('srvv-') || name.startsWith('srv-')) {
return 'hardware'; // Servidores virtuales o físicos
} else if (name.startsWith('dtic-')) {
return 'facilities'; // Servicios DTIC
} else if (name.startsWith('pcv-')) {
return 'network'; // Componentes de red
} else {
return 'tools'; // Otros
}
}
// Función para extraer usuarios del contenido
function extractUsuarios(content, usuarios) {
// Patrones para identificar usuarios
const userPatterns = [
/rmonla/g, // Ricardo MONLA
/jessisanchez/g, // Jessica Sánchez
/silviaromero/g, // Silvia Romero
/sofiaemelivega/g, // Sofía Emeli Vega
/silromero/g // Silvia Romero (variante)
];
userPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(content)) !== null) {
const userId = match[0];
if (!usuarios.find(u => u.email === `${userId}@frlr.utn.edu.ar`)) {
const userInfo = getUserInfo(userId);
usuarios.push({
first_name: userInfo.firstName,
last_name: userInfo.lastName,
email: `${userId}@frlr.utn.edu.ar`,
department: 'DTIC',
position: userInfo.position
});
}
}
});
}
// Función para obtener información de usuario
function getUserInfo(userId) {
const userMap = {
'rmonla': { firstName: 'Ricardo', lastName: 'MONLA', position: 'Responsable DTIC' },
'jessisanchez': { firstName: 'Jessica', lastName: 'SÁNCHEZ', position: 'Administradora' },
'silviaromero': { firstName: 'Silvia', lastName: 'ROMERO', position: 'Administradora' },
'sofiaemelivega': { firstName: 'Sofía Emeli', lastName: 'VEGA', position: 'Usuario' },
'silromero': { firstName: 'Silvia', lastName: 'ROMERO', position: 'Administradora' }
};
return userMap[userId] || { firstName: userId, lastName: 'Usuario', position: 'Usuario' };
}
// Función para extraer tareas del contenido
function extractTareas(content, tareas) {
// Buscar patrones de tareas en las bitácoras
const taskPatterns = [
/Actualización.*sistema operativo/g,
/Configuración.*DNS/g,
/Backup.*VM/g,
/Instalación.*software/g,
/Mantenimiento.*servidor/g,
/Resolución.*problema/g,
/Creación.*usuario/g,
/Actualización.*servicio/g
];
taskPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(content)) !== null) {
const taskTitle = match[0];
if (!tareas.find(t => t.title === taskTitle)) {
tareas.push({
title: taskTitle,
description: `Tarea identificada en bitácoras históricas`,
status: 'completed',
priority: 'medium'
});
}
}
});
}
// Función principal para procesar los datos
async function processRealData() {
try {
console.log('🔍 Extrayendo datos reales de las bitácoras...');
const data = readBitacoraFiles();
console.log(`📊 Datos extraídos:`);
console.log(` - Recursos: ${data.recursos.length}`);
console.log(` - Usuarios: ${data.usuarios.length}`);
console.log(` - Tareas: ${data.tareas.length}`);
// Limpiar datos existentes
console.log('🧹 Eliminando datos existentes...');
await clearExistingData();
// Insertar nuevos datos
console.log('📥 Insertando datos reales...');
await insertRealData(data);
console.log('✅ Proceso completado exitosamente!');
} catch (error) {
console.error('❌ Error procesando datos reales:', error);
}
}
// Función para limpiar datos existentes
async function clearExistingData() {
try {
// Eliminar en orden correcto (por dependencias)
await executeQuery('DELETE FROM dtic.recurso_historial');
await executeQuery('DELETE FROM dtic.recurso_asignaciones');
await executeQuery('DELETE FROM dtic.tareas');
await executeQuery('DELETE FROM dtic.recursos');
await executeQuery('DELETE FROM dtic.usuarios_relacionados');
console.log('✅ Datos existentes eliminados');
} catch (error) {
console.error('❌ Error eliminando datos existentes:', error);
throw error;
}
}
// Función para insertar datos reales
async function insertRealData(data) {
try {
// Insertar recursos
for (let i = 0; i < data.recursos.length; i++) {
const recurso = data.recursos[i];
const dticId = `REC-${String(i + 1).padStart(4, '0')}`;
await executeQuery(`
INSERT INTO dtic.recursos (dtic_id, name, description, category, status, location)
VALUES ($1, $2, $3, $4, $5, $6)
`, [dticId, recurso.name, recurso.description, recurso.category, recurso.status, recurso.location]);
}
// Insertar usuarios
for (let i = 0; i < data.usuarios.length; i++) {
const usuario = data.usuarios[i];
const dticId = `USR-${String(i + 1).padStart(4, '0')}`;
await executeQuery(`
INSERT INTO dtic.usuarios_relacionados (dtic_id, first_name, last_name, email, department, position)
VALUES ($1, $2, $3, $4, $5, $6)
`, [dticId, usuario.first_name, usuario.last_name, usuario.email, usuario.department, usuario.position]);
}
// Insertar tareas
for (let i = 0; i < data.tareas.length; i++) {
const tarea = data.tareas[i];
const dticId = `TAR-${String(i + 1).padStart(4, '0')}`;
await executeQuery(`
INSERT INTO dtic.tareas (dtic_id, title, description, status, priority)
VALUES ($1, $2, $3, $4, $5)
`, [dticId, tarea.title, tarea.description, tarea.status, tarea.priority]);
}
console.log('✅ Datos reales insertados correctamente');
} catch (error) {
console.error('❌ Error insertando datos reales:', error);
throw error;
}
}
// Ejecutar si se llama directamente
if (require.main === module) {
processRealData();
}
module.exports = { processRealData, readBitacoraFiles };
@@ -0,0 +1,173 @@
const fs = require('fs');
const path = require('path');
const pdf = require('pdf-parse');
const { executeQuery } = require('./database');
async function loadResourcesFromPDF() {
try {
// Path to the PDF file
const pdfPath = path.join(__dirname, '../../../_basurero/RECURSOs.pdf');
// Read and parse the PDF
const dataBuffer = fs.readFileSync(pdfPath);
const data = await pdf(dataBuffer);
const text = data.text;
console.log('Extracted text from PDF:');
console.log(text);
// Parse the text into resource objects
const resources = parseResources(text);
console.log(`Parsed ${resources.length} resources from PDF`);
// Insert resources into database in batch to avoid pool exhaustion
const { insertedCount, skippedCount } = await insertResourcesBatch(resources);
console.log(`Total resources processed: ${resources.length}`);
console.log(`Successfully inserted: ${insertedCount}`);
console.log(`Skipped (duplicates): ${skippedCount}`);
} catch (error) {
console.error('Error loading resources from PDF:', error);
}
}
function parseResources(text) {
const resources = [];
const lines = text.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('lstRECUsDETALLES'));
let currentEntry = '';
for (const line of lines) {
if (line.includes('|')) {
if (currentEntry) {
const resource = parseResourceLine(currentEntry);
if (resource) resources.push(resource);
}
currentEntry = line;
} else {
currentEntry += '\n' + line;
}
}
if (currentEntry) {
const resource = parseResourceLine(currentEntry);
if (resource) resources.push(resource);
}
return resources;
}
function parseResourceLine(entry) {
// Example: ap-GOB-02 | GOB-PasilloAp 2 en Gobierno; SN: F3608; #AP001
const parts = entry.split('|');
if (parts.length < 2) return null;
const name = parts[0].trim();
const rest = parts.slice(1).join('|').trim();
// Extract ID from end
const idMatch = rest.match(/#(\w+)$/);
const dtic_id = idMatch ? idMatch[1] : null;
if (!dtic_id) return null; // Skip entries without valid dtic_id
// Remove ID from rest
const details = rest.replace(/\s*#(\w+)$/, '').trim();
const resource = {
dtic_id: dtic_id,
name: name,
description: details,
category: 'hardware', // default
status: 'available', // default
location: null,
serial_number: null,
model: null,
technical_specs: {}
};
// Infer category from name prefix
const code = name.toLowerCase();
if (code.startsWith('ap-') || code.startsWith('sw-') || code.startsWith('rut-')) {
resource.category = 'network';
} else if (code.startsWith('pc-') || code.startsWith('imp-') || code.startsWith('tv') || code.startsWith('tbl-') || code.startsWith('dsp-') || code.startsWith('dvr-')) {
resource.category = 'hardware';
} else if (code.startsWith('srv') || code.startsWith('pcv-') || code.startsWith('netb-')) {
resource.category = 'software';
} else if (code.startsWith('cam-') || code.startsWith('cel-')) {
resource.category = 'security';
} else if (code.startsWith('not-')) {
resource.category = 'hardware'; // notebooks
} else if (code.startsWith('tel-')) {
resource.category = 'hardware';
}
// Parse details for SN, MOD, USR, location
const snMatch = details.match(/SN:\s*([^;]+)/);
if (snMatch) resource.serial_number = snMatch[1].trim();
const modMatch = details.match(/MOD(?:ELO)?:\s*([^;]+)/);
if (modMatch) resource.model = modMatch[1].trim();
const usrMatch = details.match(/USR:\s*([^;]+)/);
if (usrMatch) {
resource.status = 'assigned';
resource.technical_specs.usr = usrMatch[1].trim();
}
// Location from description, e.g., GOB-Pasillo
const locMatch = details.match(/^([^-]+)-/);
if (locMatch) resource.location = locMatch[1].trim();
return resource;
}
async function insertResourcesBatch(resources) {
const { pool } = require('./database');
const client = await pool.connect();
try {
await client.query('BEGIN');
const values = resources.map((resource, index) => {
const baseIndex = index * 9;
return `($${baseIndex + 1}, $${baseIndex + 2}, $${baseIndex + 3}, $${baseIndex + 4}, $${baseIndex + 5}, $${baseIndex + 6}, $${baseIndex + 7}, $${baseIndex + 8}, $${baseIndex + 9})`;
}).join(', ');
const query = `
INSERT INTO recursos (dtic_id, name, description, category, status, location, technical_specs, serial_number, model)
VALUES ${values}
ON CONFLICT (dtic_id) DO NOTHING
`;
const params = resources.flatMap(resource => [
resource.dtic_id,
resource.name,
resource.description,
resource.category,
resource.status,
resource.location,
JSON.stringify(resource.technical_specs),
resource.serial_number,
resource.model
]);
const result = await client.query(query, params);
const insertedCount = result.rowCount;
const skippedCount = resources.length - insertedCount;
await client.query('COMMIT');
return { insertedCount, skippedCount };
} catch (error) {
await client.query('ROLLBACK');
console.error('Error inserting resources batch:', error);
throw error;
} finally {
client.release();
}
}
// Run the script
if (require.main === module) {
loadResourcesFromPDF();
}
module.exports = { loadResourcesFromPDF };
@@ -0,0 +1,250 @@
const jwt = require('jsonwebtoken');
const { pool } = require('../database');
// Función helper para ejecutar queries
const executeQuery = async (query, params = []) => {
const client = await pool.connect();
try {
const result = await client.query(query, params);
return result;
} finally {
client.release();
}
};
/**
* Middleware de autenticación JWT
*/
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: 'Token de acceso requerido'
});
}
// Verificar token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Verificar que el usuario aún existe y está activo
const query = 'SELECT id, is_active, role FROM dtic.tecnicos WHERE id = $1';
const result = await executeQuery(query, [decoded.id]);
if (result.rows.length === 0) {
return res.status(401).json({
success: false,
message: 'Usuario no encontrado'
});
}
const user = result.rows[0];
if (!user.is_active) {
return res.status(401).json({
success: false,
message: 'Usuario inactivo'
});
}
// Agregar información del usuario a la request
req.user = {
id: decoded.id,
dtic_id: decoded.dtic_id,
email: decoded.email,
role: user.role,
department: decoded.department
};
next();
} catch (error) {
console.error('Error en autenticación:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Token inválido'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expirado'
});
}
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
};
/**
* Middleware para verificar permisos por rol
*/
const requirePermission = (requiredRole) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Usuario no autenticado'
});
}
const roleHierarchy = {
'viewer': 1,
'technician': 2,
'admin': 3
};
const userRoleLevel = roleHierarchy[req.user.role] || 0;
const requiredRoleLevel = roleHierarchy[requiredRole] || 0;
if (userRoleLevel < requiredRoleLevel) {
return res.status(403).json({
success: false,
message: 'Permisos insuficientes'
});
}
next();
};
};
/**
* Middleware para verificar propiedad de recursos
*/
const requireOwnership = (resourceType) => {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Usuario no autenticado'
});
}
// Los administradores pueden acceder a todo
if (req.user.role === 'admin') {
return next();
}
const resourceId = req.params.id;
try {
let query;
let params;
switch (resourceType) {
case 'technician':
// Los técnicos solo pueden acceder a su propio perfil
if (req.user.id !== parseInt(resourceId)) {
return res.status(403).json({
success: false,
message: 'No tienes permisos para acceder a este recurso'
});
}
break;
case 'task':
// Verificar si el usuario es el asignado a la tarea
query = 'SELECT technician_id FROM dtic.tareas WHERE id = $1';
params = [resourceId];
break;
default:
return res.status(400).json({
success: false,
message: 'Tipo de recurso no válido'
});
}
if (query) {
const result = await executeQuery(query, params);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
if (result.rows[0].technician_id !== req.user.id) {
return res.status(403).json({
success: false,
message: 'No tienes permisos para acceder a este recurso'
});
}
}
next();
} catch (error) {
console.error('Error verificando propiedad:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
};
};
/**
* Middleware para logging de auditoría
*/
const auditLog = (action) => {
return async (req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
// Solo loguear si la operación fue exitosa
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const auditData = {
user_id: req.user?.id,
action: action,
entity_type: req.route?.path?.split('/')[1] || 'unknown',
entity_id: req.params?.id ? parseInt(req.params.id) : null,
old_values: null, // Para actualizaciones, se podría implementar comparación
new_values: req.method === 'POST' || req.method === 'PUT' ? req.body : null,
ip_address: req.ip,
user_agent: req.get('User-Agent')
};
// Insertar en auditoría (de forma asíncrona, no bloqueante)
executeQuery(`
INSERT INTO dtic.audit_log
(user_id, action, entity_type, entity_id, old_values, new_values, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, [
auditData.user_id,
auditData.action,
auditData.entity_type,
auditData.entity_id,
auditData.old_values ? JSON.stringify(auditData.old_values) : null,
auditData.new_values ? JSON.stringify(auditData.new_values) : null,
auditData.ip_address,
auditData.user_agent
]).catch(err => console.error('Error en audit log:', err));
} catch (error) {
console.error('Error creando audit log:', error);
}
}
originalSend.call(this, data);
};
next();
};
};
module.exports = {
authenticateToken,
requirePermission,
requireOwnership,
auditLog
};
@@ -0,0 +1,306 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const { executeQuery } = require('../database');
const router = express.Router();
// POST /api/auth/login - Iniciar sesión
router.post('/login', [
body('email').isEmail().normalizeEmail(),
body('password').notEmpty().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos de entrada inválidos',
errors: errors.array()
});
}
const { email, password } = req.body;
// Buscar usuario por email
const query = `
SELECT id, dtic_id, first_name, last_name, email, password_hash, role, department, is_active
FROM dtic.tecnicos
WHERE email = $1
`;
const result = await executeQuery(query, [email]);
if (result.rows.length === 0) {
return res.status(401).json({
success: false,
message: 'Credenciales inválidas'
});
}
const user = result.rows[0];
// Verificar si el usuario está activo
if (!user.is_active) {
return res.status(401).json({
success: false,
message: 'Usuario inactivo'
});
}
// Verificar contraseña
if (!user.password_hash) {
return res.status(401).json({
success: false,
message: 'Usuario no tiene contraseña configurada'
});
}
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Credenciales inválidas'
});
}
// Generar token JWT
const token = jwt.sign(
{
id: user.id,
dtic_id: user.dtic_id,
email: user.email,
role: user.role,
department: user.department
},
process.env.JWT_SECRET,
{ expiresIn: '8h' }
);
// Actualizar último acceso
await executeQuery(
'UPDATE dtic.tecnicos SET updated_at = CURRENT_TIMESTAMP WHERE id = $1',
[user.id]
);
res.json({
success: true,
message: 'Inicio de sesión exitoso',
data: {
token,
user: {
id: user.id,
dtic_id: user.dtic_id,
first_name: user.first_name,
last_name: user.last_name,
email: user.email,
role: user.role,
department: user.department
}
}
});
} catch (error) {
console.error('Error en login:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/auth/register - Registrar nuevo usuario (solo admin)
router.post('/register', [
body('first_name').trim().isLength({ min: 2, max: 50 }),
body('last_name').trim().isLength({ min: 2, max: 50 }),
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('department').isIn(['dtic', 'sistemas', 'redes', 'seguridad']),
body('role').isIn(['admin', 'technician', 'viewer'])
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { first_name, last_name, email, password, department, role } = req.body;
// Verificar email único
const emailCheck = await executeQuery('SELECT id FROM dtic.tecnicos WHERE email = $1', [email]);
if (emailCheck.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'El email ya está registrado'
});
}
// Generar DTIC ID
const dticIdResult = await executeQuery('SELECT generate_dtic_id($1) as new_id', ['TEC']);
const dticId = dticIdResult.rows[0].new_id;
// Hashear contraseña
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Insertar usuario
const query = `
INSERT INTO dtic.tecnicos
(dtic_id, first_name, last_name, email, password_hash, department, role, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`;
const result = await executeQuery(query, [
dticId, first_name, last_name.toUpperCase(), email, hashedPassword, department, role, true
]);
res.status(201).json({
success: true,
message: 'Usuario registrado exitosamente',
data: {
user_id: result.rows[0].id,
dtic_id: dticId
}
});
} catch (error) {
console.error('Error en registro:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/auth/logout - Cerrar sesión
router.post('/logout', (req, res) => {
// En una implementación real, podrías mantener una lista negra de tokens
// Por ahora, simplemente respondemos con éxito
res.json({
success: true,
message: 'Sesión cerrada exitosamente'
});
});
// GET /api/auth/me - Obtener información del usuario actual
router.get('/me', async (req, res) => {
try {
// El middleware de auth debería haber establecido req.user
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: 'Token no proporcionado'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const query = `
SELECT id, dtic_id, first_name, last_name, email, role, department, is_active, created_at, updated_at
FROM dtic.tecnicos
WHERE id = $1
`;
const result = await executeQuery(query, [decoded.id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Usuario no encontrado'
});
}
const user = result.rows[0];
res.json({
success: true,
message: 'Información de usuario obtenida exitosamente',
data: { user }
});
} catch (error) {
console.error('Error obteniendo usuario actual:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Token inválido'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expirado'
});
}
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/auth/refresh - Refrescar token
router.post('/refresh', async (req, res) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
success: false,
message: 'Token requerido'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET, { ignoreExpiration: true });
// Verificar que el usuario aún existe y está activo
const query = 'SELECT id, is_active FROM dtic.tecnicos WHERE id = $1';
const result = await executeQuery(query, [decoded.id]);
if (result.rows.length === 0 || !result.rows[0].is_active) {
return res.status(401).json({
success: false,
message: 'Usuario no válido'
});
}
// Generar nuevo token
const newToken = jwt.sign(
{
id: decoded.id,
dtic_id: decoded.dtic_id,
email: decoded.email,
role: decoded.role,
department: decoded.department
},
process.env.JWT_SECRET,
{ expiresIn: '8h' }
);
res.json({
success: true,
message: 'Token refrescado exitosamente',
data: { token: newToken }
});
} catch (error) {
console.error('Error refrescando token:', error);
res.status(401).json({
success: false,
message: 'Token inválido'
});
}
});
module.exports = router;
@@ -0,0 +1,605 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { executeQuery } = require('../database');
const auth = require('../middleware/auth');
const router = express.Router();
// Middleware de autenticación (temporalmente comentado para desarrollo)
// router.use(auth);
// Función helper para generar DTIC ID para recursos
const generateDTICId = async (prefix = 'REC') => {
const query = `
SELECT dtic.generate_dtic_id($1) as new_id
`;
const result = await executeQuery(query, [prefix]);
return result.rows[0].new_id;
};
// GET /api/recursos - Obtener lista de recursos con filtros
router.get('/', [
query('page').optional().isInt({ min: 1 }).toInt(),
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
query('search').optional().isString().trim(),
query('category').optional().isIn(['hardware', 'software', 'network', 'security', 'tools', 'facilities']),
query('status').optional().isIn(['available', 'assigned', 'maintenance', 'retired']),
query('location').optional().isString().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const {
page = 1,
limit = 12,
search,
category,
status,
location
} = req.query;
let query = `
SELECT
r.id, r.dtic_id, r.name, r.description, r.category, r.status,
r.location, r.technical_specs, r.serial_number, r.model,
r.created_at, r.updated_at,
COALESCE(json_agg(DISTINCT jsonb_build_object('id', u.id, 'first_name', u.first_name, 'last_name', u.last_name, 'full_name', CONCAT(u.first_name, ' ', u.last_name))) FILTER (WHERE ra.user_id IS NOT NULL), '[]'::json) as assigned_users,
COALESCE(json_agg(DISTINCT jsonb_build_object('id', t.id, 'title', t.title, 'status', t.status)) FILTER (WHERE t.id IS NOT NULL), '[]'::json) as related_tasks
FROM recursos r
LEFT JOIN recurso_asignaciones ra ON ra.recurso_id = r.id AND ra.activo = true
LEFT JOIN usuarios_relacionados u ON u.id = ra.user_id
LEFT JOIN tarea_recursos tr ON tr.recurso_id = r.id AND tr.activo = true
LEFT JOIN tareas t ON t.id = tr.tarea_id
WHERE 1=1
`;
const params = [];
const conditions = [];
// Filtros
if (search) {
conditions.push("(r.name ILIKE $1 OR r.description ILIKE $1 OR r.dtic_id ILIKE $1 OR r.serial_number ILIKE $1 OR r.model ILIKE $1)");
params.push(`%${search}%`);
}
if (category) {
conditions.push("r.category = $" + (params.length + 1));
params.push(category);
}
if (status) {
conditions.push("r.status = $" + (params.length + 1));
params.push(status);
}
if (location) {
conditions.push("r.location ILIKE $" + (params.length + 1));
params.push(`%${location}%`);
}
if (conditions.length > 0) {
query += " AND " + conditions.join(" AND ");
}
query += " GROUP BY r.id, r.dtic_id, r.name, r.description, r.category, r.status, r.location, r.technical_specs, r.serial_number, r.model, r.created_at, r.updated_at ORDER BY r.created_at DESC";
// Paginación
const offset = (page - 1) * limit;
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
params.push(limit, offset);
const result = await executeQuery(query, params);
// Obtener total para paginación
let countQuery = "SELECT COUNT(*) as total FROM dtic.recursos r WHERE 1=1";
const countParams = [];
if (conditions.length > 0) {
countQuery += " AND " + conditions.join(" AND ");
countParams.push(...params.slice(0, -2));
}
const countResult = await executeQuery(countQuery, countParams);
const total = parseInt(countResult.rows[0].total);
res.json({
success: true,
message: 'Recursos obtenidos exitosamente',
data: {
recursos: result.rows,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
console.error('Error obteniendo recursos:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// GET /api/recursos/:id - Obtener recurso específico
router.get('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
const { include_history } = req.query;
let query = `
SELECT r.*
FROM dtic.recursos r
WHERE r.id = $1
`;
const result = await executeQuery(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
const recurso = result.rows[0];
// Obtener usuarios asignados
const assignedUsersQuery = `
SELECT
u.id, u.dtic_id, u.first_name, u.last_name,
CONCAT(u.first_name, ' ', u.last_name) as full_name,
ra.assigned_at, ra.assigned_by
FROM dtic.recurso_asignaciones ra
JOIN dtic.usuarios_relacionados u ON u.id = ra.user_id
WHERE ra.recurso_id = $1 AND ra.activo = true
ORDER BY ra.assigned_at DESC
`;
const assignedUsersResult = await executeQuery(assignedUsersQuery, [id]);
recurso.assigned_users = assignedUsersResult.rows;
// Obtener historial si se solicita
if (include_history === 'true') {
const historyQuery = `
SELECT
h.*,
t.dtic_id as tecnico_dtic_id,
CONCAT(t.first_name, ' ', t.last_name) as tecnico_name,
u.dtic_id as usuario_dtic_id,
CONCAT(u.first_name, ' ', u.last_name) as usuario_name
FROM dtic.recurso_historial h
LEFT JOIN dtic.tecnicos t ON t.id = h.tecnico_id
LEFT JOIN dtic.usuarios_relacionados u ON u.id = h.usuario_id
WHERE h.recurso_id = $1
ORDER BY h.created_at DESC
LIMIT 20
`;
const historyResult = await executeQuery(historyQuery, [id]);
recurso.history = historyResult.rows;
}
res.json({
success: true,
message: 'Recurso obtenido exitosamente',
data: { recurso }
});
} catch (error) {
console.error('Error obteniendo recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/recursos - Crear nuevo recurso
router.post('/', [
body('name').trim().isLength({ min: 2, max: 200 }),
body('description').optional().trim().isLength({ max: 1000 }),
body('category').isIn(['hardware', 'software', 'network', 'security', 'tools', 'facilities']),
body('status').optional().isIn(['available', 'assigned', 'maintenance', 'retired']),
body('location').optional().trim().isLength({ max: 200 }),
body('technical_specs').optional(),
body('serial_number').optional().trim().isLength({ max: 100 }),
body('model').optional().trim().isLength({ max: 100 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const {
name, description, category, status = 'available',
location, technical_specs, serial_number, model
} = req.body;
// Generar DTIC ID
const dticId = await generateDTICId('REC');
// Insertar recurso
const query = `
INSERT INTO dtic.recursos
(dtic_id, name, description, category, status, location, technical_specs, serial_number, model)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id
`;
const result = await executeQuery(query, [
dticId, name, description, category, status, location,
technical_specs, serial_number, model
]);
res.status(201).json({
success: true,
message: 'Recurso creado exitosamente',
data: {
recurso_id: result.rows[0].id,
dtic_id: dticId
}
});
} catch (error) {
console.error('Error creando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// PUT /api/recursos/:id - Actualizar recurso
router.put('/:id', [
param('id').isInt({ min: 1 }).toInt(),
body('name').optional().trim().isLength({ min: 2, max: 200 }),
body('description').optional().trim().isLength({ max: 1000 }),
body('category').optional().isIn(['hardware', 'software', 'network', 'security', 'tools', 'facilities']),
body('status').optional().isIn(['available', 'assigned', 'maintenance', 'retired']),
body('location').optional().trim().isLength({ max: 200 }),
body('technical_specs').optional(),
body('serial_number').optional().trim().isLength({ max: 100 }),
body('model').optional().trim().isLength({ max: 100 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const {
name, description, category, status, location,
technical_specs, serial_number, model
} = req.body;
// Verificar que el recurso existe
const existingQuery = 'SELECT * FROM dtic.recursos WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
// Construir query de actualización
const updateFields = [];
const params = [];
const allowedFields = [
'name', 'description', 'category', 'status', 'location',
'technical_specs', 'serial_number', 'model'
];
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
updateFields.push(`${field} = $${params.length + 1}`);
params.push(req.body[field]);
}
});
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
message: 'No se proporcionaron campos para actualizar'
});
}
params.push(id);
const query = `
UPDATE dtic.recursos
SET ${updateFields.join(', ')}
WHERE id = $${params.length}
`;
await executeQuery(query, params);
res.json({
success: true,
message: 'Recurso actualizado exitosamente'
});
} catch (error) {
console.error('Error actualizando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// DELETE /api/recursos/:id - Eliminar recurso
router.delete('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
// Verificar que el recurso existe
const existingQuery = 'SELECT * FROM dtic.recursos WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
const recurso = existingResult.rows[0];
if (recurso.status === 'assigned') {
// Verificar si tiene usuarios asignados activos
const assignedUsersQuery = `
SELECT COUNT(*) as count
FROM dtic.recurso_asignaciones
WHERE recurso_id = $1 AND activo = true
`;
const assignedResult = await executeQuery(assignedUsersQuery, [id]);
if (parseInt(assignedResult.rows[0].count) > 0) {
return res.status(409).json({
success: false,
message: 'No se puede eliminar el recurso porque tiene usuarios asignados'
});
}
}
// Cambiar estado a retirado (eliminación lógica)
await executeQuery('UPDATE dtic.recursos SET status = $1 WHERE id = $2', ['retired', id]);
res.json({
success: true,
message: 'Recurso retirado exitosamente'
});
} catch (error) {
console.error('Error eliminando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/recursos/:id/asignar - Asignar recurso a usuario
router.post('/:id/asignar', [
param('id').isInt({ min: 1 }).toInt(),
body('usuario_id').isInt({ min: 1 }),
body('tecnico_id').optional().isInt({ min: 1 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { usuario_id, tecnico_id } = req.body;
// Verificar que el recurso existe
const recursoQuery = 'SELECT * FROM dtic.recursos WHERE id = $1';
const recursoResult = await executeQuery(recursoQuery, [id]);
if (recursoResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
// Verificar que el usuario existe
const usuarioQuery = 'SELECT * FROM dtic.usuarios_relacionados WHERE id = $1';
const usuarioResult = await executeQuery(usuarioQuery, [usuario_id]);
if (usuarioResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Usuario no encontrado'
});
}
// Verificar que no esté ya asignado
const existingQuery = `
SELECT id FROM dtic.recurso_asignaciones
WHERE recurso_id = $1 AND user_id = $2 AND activo = true
`;
const existingResult = await executeQuery(existingQuery, [id, usuario_id]);
if (existingResult.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'El recurso ya está asignado a este usuario'
});
}
// Crear asignación
const assignQuery = `
INSERT INTO dtic.recurso_asignaciones
(recurso_id, user_id, assigned_by, activo)
VALUES ($1, $2, $3, true)
`;
await executeQuery(assignQuery, [id, usuario_id, tecnico_id]);
// Actualizar estado del recurso si estaba disponible
if (recursoResult.rows[0].status === 'available') {
await executeQuery('UPDATE dtic.recursos SET status = $1 WHERE id = $2', ['assigned', id]);
}
// Registrar en historial
const historyQuery = `
INSERT INTO dtic.recurso_historial
(recurso_id, action, usuario_id, tecnico_id, details)
VALUES ($1, $2, $3, $4, $5)
`;
await executeQuery(historyQuery, [
id, 'assigned', usuario_id, tecnico_id,
`Recurso asignado a ${usuarioResult.rows[0].first_name} ${usuarioResult.rows[0].last_name}`
]);
res.json({
success: true,
message: 'Recurso asignado exitosamente'
});
} catch (error) {
console.error('Error asignando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/recursos/:id/desasignar - Desasignar recurso de usuario
router.post('/:id/desasignar', [
param('id').isInt({ min: 1 }).toInt(),
body('usuario_id').isInt({ min: 1 }),
body('tecnico_id').optional().isInt({ min: 1 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { usuario_id, tecnico_id } = req.body;
// Verificar que la asignación existe
const assignmentQuery = `
SELECT ra.*, u.first_name, u.last_name
FROM dtic.recurso_asignaciones ra
JOIN dtic.usuarios_relacionados u ON u.id = ra.user_id
WHERE ra.recurso_id = $1 AND ra.user_id = $2 AND ra.activo = true
`;
const assignmentResult = await executeQuery(assignmentQuery, [id, usuario_id]);
if (assignmentResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Asignación no encontrada'
});
}
// Desactivar asignación
const updateQuery = `
UPDATE dtic.recurso_asignaciones
SET activo = false, unassigned_at = CURRENT_TIMESTAMP, unassigned_by = $1
WHERE recurso_id = $2 AND user_id = $3 AND activo = true
`;
await executeQuery(updateQuery, [tecnico_id, id, usuario_id]);
// Verificar si quedan usuarios asignados
const remainingQuery = `
SELECT COUNT(*) as count
FROM dtic.recurso_asignaciones
WHERE recurso_id = $1 AND activo = true
`;
const remainingResult = await executeQuery(remainingQuery, [id]);
// Si no quedan usuarios asignados, cambiar estado a disponible
if (parseInt(remainingResult.rows[0].count) === 0) {
await executeQuery('UPDATE dtic.recursos SET status = $1 WHERE id = $2', ['available', id]);
}
// Registrar en historial
const usuario = assignmentResult.rows[0];
const historyQuery = `
INSERT INTO dtic.recurso_historial
(recurso_id, action, usuario_id, tecnico_id, details)
VALUES ($1, $2, $3, $4, $5)
`;
await executeQuery(historyQuery, [
id, 'unassigned', usuario_id, tecnico_id,
`Recurso desasignado de ${usuario.first_name} ${usuario.last_name}`
]);
res.json({
success: true,
message: 'Recurso desasignado exitosamente'
});
} catch (error) {
console.error('Error desasignando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
module.exports = router;
@@ -0,0 +1,384 @@
const express = require('express');
const { body, param, validationResult } = require('express-validator');
const { exec } = require('child_process');
const fs = require('fs').promises;
const path = require('path');
const url = require('url');
const auth = require('../middleware/auth');
const router = express.Router();
// Middleware de autenticación y permisos de admin
router.use(auth.authenticateToken);
router.use(auth.requirePermission('admin'));
// Función helper para parsear DATABASE_URL
const parseDatabaseUrl = (databaseUrl) => {
const parsedUrl = url.parse(databaseUrl);
return {
host: parsedUrl.hostname,
port: parsedUrl.port,
database: parsedUrl.pathname.substring(1),
username: parsedUrl.auth.split(':')[0],
password: parsedUrl.auth.split(':')[1]
};
};
// POST /api/system/backup - Crear backup de la base de datos
router.post('/backup', async (req, res) => {
try {
const dbConfig = parseDatabaseUrl(process.env.DATABASE_URL);
const timestamp = new Date().toISOString().replace(/[:.]/g, '').replace('T', '_').split('.')[0];
const backupFileName = `dtic_bitacoras_backup_${timestamp}.sql`;
const backupPath = path.join('/host', '_app-bitacoras', 'backups', backupFileName);
// Usar Docker para ejecutar pg_dump en el contenedor de PostgreSQL
const tempBackupName = `temp_backup_${Date.now()}.sql`;
const dockerPgDumpCommand = `docker exec -e PGPASSWORD=${dbConfig.password} dtic_bitacoras_postgres pg_dump -h localhost -p 5432 -U ${dbConfig.username} -d ${dbConfig.database} -f /tmp/${tempBackupName}`;
// Ejecutar pg_dump dentro del contenedor PostgreSQL
await new Promise((resolve, reject) => {
const child = exec(dockerPgDumpCommand, (error, stdout, stderr) => {
if (error) {
console.error('Error en pg_dump dentro del contenedor:', error);
console.error('Stderr:', stderr);
reject(new Error(`Error creando backup: ${error.message}`));
return;
}
console.log('Backup creado exitosamente en contenedor:', backupFileName);
resolve();
});
});
// Copiar el backup desde el contenedor al host
const dockerCpCommand = `docker cp dtic_bitacoras_postgres:/tmp/${tempBackupName} ${backupPath}`;
await new Promise((resolve, reject) => {
const child = exec(dockerCpCommand, (error, stdout, stderr) => {
if (error) {
console.error('Error copiando backup desde contenedor:', error);
console.error('Stderr:', stderr);
reject(new Error(`Error copiando backup: ${error.message}`));
return;
}
console.log('Backup copiado exitosamente al host:', backupPath);
resolve();
});
});
// Limpiar archivo temporal del contenedor
const dockerRmCommand = `docker exec dtic_bitacoras_postgres rm -f /tmp/${tempBackupName}`;
await new Promise((resolve, reject) => {
const child = exec(dockerRmCommand, (error, stdout, stderr) => {
if (error) {
console.warn('Advertencia: No se pudo limpiar archivo temporal del contenedor:', error.message);
// No rechazamos aquí, ya que el backup ya se copió exitosamente
}
resolve();
});
});
res.json({
success: true,
message: 'Backup creado exitosamente',
data: {
backup_file: backupFileName,
backup_path: backupPath,
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error('Error creando backup:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor al crear backup'
});
}
});
// POST /api/system/restore - Restaurar base de datos desde backup
router.post('/restore', [
body('backup_file').isString().notEmpty().withMessage('Nombre del archivo de backup es requerido')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { backup_file } = req.body;
const dbConfig = parseDatabaseUrl(process.env.DATABASE_URL);
const backupPath = path.join('/host', '_app-bitacoras', 'backups', backup_file);
// Verificar que el archivo existe
try {
await fs.access(backupPath);
} catch (error) {
return res.status(404).json({
success: false,
message: 'Archivo de backup no encontrado'
});
}
// Usar Docker para ejecutar psql en el contenedor de PostgreSQL
const tempBackupName = `temp_restore_${Date.now()}.sql`;
const dockerCpCommand = `docker cp ${backupPath} dtic_bitacoras_postgres:/tmp/${tempBackupName}`;
await new Promise((resolve, reject) => {
const child = exec(dockerCpCommand, (error, stdout, stderr) => {
if (error) {
console.error('Error copiando backup al contenedor:', error);
console.error('Stderr:', stderr);
reject(new Error(`Error copiando backup: ${error.message}`));
return;
}
console.log('Backup copiado al contenedor exitosamente');
resolve();
});
});
// Ejecutar psql dentro del contenedor PostgreSQL
const dockerPsqlCommand = `docker exec -e PGPASSWORD=${dbConfig.password} dtic_bitacoras_postgres psql -h localhost -p 5432 -U ${dbConfig.username} -d ${dbConfig.database} -f /tmp/${tempBackupName}`;
await new Promise((resolve, reject) => {
const child = exec(dockerPsqlCommand, (error, stdout, stderr) => {
if (error) {
console.error('Error en psql dentro del contenedor:', error);
console.error('Stderr:', stderr);
reject(new Error(`Error restaurando backup: ${error.message}`));
return;
}
console.log('Backup restaurado exitosamente:', backup_file);
resolve();
});
});
// Limpiar archivo temporal del contenedor
const dockerRmCommand = `docker exec dtic_bitacoras_postgres rm -f /tmp/${tempBackupName}`;
await new Promise((resolve, reject) => {
const child = exec(dockerRmCommand, (error, stdout, stderr) => {
if (error) {
console.warn('Advertencia: No se pudo limpiar archivo temporal del contenedor:', error.message);
// No rechazamos aquí, ya que la restauración ya se completó exitosamente
}
resolve();
});
});
res.json({
success: true,
message: 'Base de datos restaurada exitosamente',
data: {
backup_file: backup_file,
restored_at: new Date().toISOString()
}
});
} catch (error) {
console.error('Error restaurando backup:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor al restaurar backup'
});
}
});
// GET /api/system/backups - Listar archivos de backup disponibles
router.get('/backups', async (req, res) => {
console.log('GET /backups - Starting list backups request');
try {
const backupsDir = path.join('/host', '_app-bitacoras', 'backups');
console.log('GET /backups - Backups dir:', backupsDir);
// Leer directorio de backups
const files = await fs.readdir(backupsDir);
console.log('GET /backups - Files in dir:', files);
// Filtrar archivos .sql y obtener información
const backupFiles = await Promise.all(
files
.filter(file => file.endsWith('.sql'))
.map(async (file) => {
const filePath = path.join(backupsDir, file);
const stats = await fs.stat(filePath);
// Extraer timestamp del nombre del archivo
const timestampMatch = file.match(/dtic_bitacoras_backup_(\d{4}-?\d{2}-?\d{2})_(\d{6,9})(Z?)\.sql/);
let createdAt = null;
if (timestampMatch) {
const dateStr = timestampMatch[1];
const timeStr = timestampMatch[2];
const z = timestampMatch[3];
let year, month, day;
if (dateStr.includes('-')) {
[year, month, day] = dateStr.split('-');
} else {
year = dateStr.substring(0, 4);
month = dateStr.substring(4, 6);
day = dateStr.substring(6, 8);
}
let hour, minute, second, ms = '000';
if (timeStr.length === 6) {
hour = timeStr.substring(0, 2);
minute = timeStr.substring(2, 4);
second = timeStr.substring(4, 6);
} else if (timeStr.length === 9) {
hour = timeStr.substring(0, 2);
minute = timeStr.substring(2, 4);
second = timeStr.substring(4, 6);
ms = timeStr.substring(6, 9);
}
createdAt = `${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}Z`;
}
return {
filename: file,
size: stats.size,
created_at: createdAt || stats.birthtime.toISOString(),
modified_at: stats.mtime.toISOString()
};
})
);
// Ordenar por fecha de creación descendente
backupFiles.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
res.json({
success: true,
message: 'Archivos de backup obtenidos exitosamente',
data: {
backups: backupFiles,
total: backupFiles.length
}
});
} catch (error) {
console.error('Error listando backups:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor al listar backups'
});
}
});
// DELETE /api/system/backups/:filename - Eliminar archivo de backup
router.delete('/backups/:filename', [
param('filename').isString().matches(/^dtic_bitacoras_backup_(?:\d{4}-\d{2}-\d{2}|\d{8})_\d{6,9}Z?\.sql$/).withMessage('Nombre de archivo de backup inválido')
], async (req, res) => {
console.log('DELETE /backups/:filename - Starting deletion request');
console.log('Filename param:', req.params.filename);
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('DELETE /backups/:filename - Validation errors:', errors.array());
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { filename } = req.params;
const backupsDir = path.join('/host', '_app-bitacoras', 'backups');
const filePath = path.join(backupsDir, filename);
console.log('DELETE /backups/:filename - Backups dir:', backupsDir);
console.log('DELETE /backups/:filename - File path:', filePath);
// Verificar que el archivo existe
try {
await fs.access(filePath);
console.log('DELETE /backups/:filename - File exists');
} catch (error) {
console.log('DELETE /backups/:filename - File does not exist, error:', error.message);
return res.status(404).json({
success: false,
message: 'Archivo de backup no encontrado'
});
}
// Eliminar el archivo
await fs.unlink(filePath);
console.log('DELETE /backups/:filename - File deleted successfully');
res.json({
success: true,
message: 'Archivo de backup eliminado exitosamente',
data: {
filename: filename,
deleted_at: new Date().toISOString()
}
});
} catch (error) {
console.error('Error eliminando backup:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor al eliminar backup'
});
}
});
// GET /api/system/backups/:filename/download - Descargar archivo de backup
router.get('/backups/:filename/download', [
param('filename').isString().matches(/^dtic_bitacoras_backup_(?:\d{4}-\d{2}-\d{2}|\d{8})_\d{6,9}Z?\.sql$/).withMessage('Nombre de archivo de backup inválido')
], async (req, res) => {
console.log('GET /backups/:filename/download - Starting download request');
console.log('Filename param:', req.params.filename);
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('GET /backups/:filename/download - Validation errors:', errors.array());
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { filename } = req.params;
const backupsDir = path.join('/host', '_app-bitacoras', 'backups');
const filePath = path.join(backupsDir, filename);
console.log('GET /backups/:filename/download - Backups dir:', backupsDir);
console.log('GET /backups/:filename/download - File path:', filePath);
// Verificar que el archivo existe
try {
await fs.access(filePath);
console.log('GET /backups/:filename/download - File exists');
} catch (error) {
console.log('GET /backups/:filename/download - File does not exist, error:', error.message);
return res.status(404).json({
success: false,
message: 'Archivo de backup no encontrado'
});
}
// Enviar el archivo como descarga
console.log('GET /backups/:filename/download - Starting file download');
res.download(filePath, filename, (error) => {
if (error) {
console.error('Error descargando backup:', error);
// Si la respuesta ya fue enviada, no hacer nada
if (!res.headersSent) {
res.status(500).json({
success: false,
message: 'Error interno del servidor al descargar backup'
});
}
} else {
console.log('GET /backups/:filename/download - File download completed successfully');
}
});
} catch (error) {
console.error('Error procesando descarga de backup:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor al procesar descarga'
});
}
});
module.exports = router;
@@ -0,0 +1,488 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { Pool } = require('pg');
const router = express.Router();
// Configuración de PostgreSQL
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Función helper para ejecutar queries
const executeQuery = async (query, params = []) => {
const client = await pool.connect();
try {
const result = await client.query(query, params);
return result;
} finally {
client.release();
}
};
// GET /api/tareas/:id/recursos - Obtener recursos asignados a una tarea
router.get('/tareas/:id/recursos', [
param('id').isInt({ min: 1 }).toInt(),
query('include_details').optional().isBoolean()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { include_details } = req.query;
// Verificar que la tarea existe
const taskQuery = 'SELECT id, title FROM dtic.tareas WHERE id = $1';
const taskResult = await executeQuery(taskQuery, [id]);
if (taskResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Tarea no encontrada'
});
}
let query = `
SELECT
tr.id,
tr.tarea_id,
tr.recurso_id,
tr.assigned_by,
tr.assigned_at,
tr.estimated_hours,
tr.actual_hours,
tr.notes,
tr.activo,
r.dtic_id as recurso_dtic_id,
r.name as recurso_name,
r.category as recurso_category,
r.status as recurso_status,
r.location as recurso_location,
r.model as recurso_model,
t.first_name || ' ' || t.last_name as assigned_by_name
FROM dtic.tarea_recursos tr
JOIN dtic.recursos r ON r.id = tr.recurso_id
LEFT JOIN dtic.tecnicos t ON t.id = tr.assigned_by
WHERE tr.tarea_id = $1 AND tr.activo = true
ORDER BY tr.assigned_at DESC
`;
const result = await executeQuery(query, [id]);
// Si se solicitan detalles adicionales
if (include_details === 'true') {
for (let assignment of result.rows) {
// Obtener historial de esta asignación
const historyQuery = `
SELECT action, created_at, details
FROM dtic.tarea_recurso_historial
WHERE tarea_recurso_id = $1
ORDER BY created_at DESC
LIMIT 5
`;
const historyResult = await executeQuery(historyQuery, [assignment.id]);
assignment.history = historyResult.rows;
}
}
res.json({
success: true,
message: 'Recursos asignados obtenidos exitosamente',
data: {
task: taskResult.rows[0],
assignments: result.rows
}
});
} catch (error) {
console.error('Error obteniendo recursos asignados:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/tareas/:id/recursos - Asignar recursos a una tarea
router.post('/tareas/:id/recursos', [
param('id').isInt({ min: 1 }).toInt(),
body('recurso_id').isInt({ min: 1 }),
body('estimated_hours').optional().isFloat({ min: 0 }),
body('notes').optional().isString().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { recurso_id, estimated_hours, notes } = req.body;
// Verificar que la tarea existe
const taskQuery = 'SELECT id, title FROM dtic.tareas WHERE id = $1';
const taskResult = await executeQuery(taskQuery, [id]);
if (taskResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Tarea no encontrada'
});
}
// Verificar que el recurso existe y está disponible
const resourceQuery = 'SELECT id, name, status FROM dtic.recursos WHERE id = $1';
const resourceResult = await executeQuery(resourceQuery, [recurso_id]);
if (resourceResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
if (resourceResult.rows[0].status !== 'available') {
return res.status(409).json({
success: false,
message: 'El recurso no está disponible para asignación'
});
}
// Verificar que no esté ya asignado a esta tarea
const existingQuery = `
SELECT id FROM dtic.tarea_recursos
WHERE tarea_id = $1 AND recurso_id = $2 AND activo = true
`;
const existingResult = await executeQuery(existingQuery, [id, recurso_id]);
if (existingResult.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'El recurso ya está asignado a esta tarea'
});
}
// Obtener ID del técnico actual (debería venir del middleware de auth)
const assigned_by = req.user?.id || 1; // Temporal hasta implementar auth
// Insertar asignación
const insertQuery = `
INSERT INTO dtic.tarea_recursos
(tarea_id, recurso_id, assigned_by, estimated_hours, notes)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
`;
const insertResult = await executeQuery(insertQuery, [
id, recurso_id, assigned_by, estimated_hours, notes
]);
// TODO: Registrar en historial - tabla tarea_recurso_historial no existe
// const historyQuery = `
// INSERT INTO dtic.tarea_recurso_historial
// (tarea_recurso_id, action, tecnico_id, details)
// VALUES ($1, 'assigned', $2, $3)
// `;
// await executeQuery(historyQuery, [
// insertResult.rows[0].id,
// assigned_by,
// `Recurso asignado a tarea: ${taskResult.rows[0].title}`
// ]);
res.status(201).json({
success: true,
message: 'Recurso asignado exitosamente a la tarea',
data: {
assignment_id: insertResult.rows[0].id,
task: taskResult.rows[0],
resource: resourceResult.rows[0]
}
});
} catch (error) {
console.error('Error asignando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// PUT /api/tareas/:id/recursos/:recursoId - Actualizar asignación
router.put('/tareas/:id/recursos/:recursoId', [
param('id').isInt({ min: 1 }).toInt(),
param('recursoId').isInt({ min: 1 }).toInt(),
body('estimated_hours').optional().isFloat({ min: 0 }),
body('actual_hours').optional().isFloat({ min: 0 }),
body('notes').optional().isString().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id, recursoId } = req.params;
const { estimated_hours, actual_hours, notes } = req.body;
// Verificar que la asignación existe y está activa
const assignmentQuery = `
SELECT tr.*, t.title as task_title, r.name as resource_name
FROM dtic.tarea_recursos tr
JOIN dtic.tareas t ON t.id = tr.tarea_id
JOIN dtic.recursos r ON r.id = tr.recurso_id
WHERE tr.tarea_id = $1 AND tr.recurso_id = $2 AND tr.activo = true
`;
const assignmentResult = await executeQuery(assignmentQuery, [id, recursoId]);
if (assignmentResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Asignación no encontrada o inactiva'
});
}
const assignment = assignmentResult.rows[0];
// Obtener técnico actual
const tecnico_id = req.user?.id || 1;
// Construir query de actualización
const updateFields = [];
const params = [];
const oldValues = {};
if (estimated_hours !== undefined) {
updateFields.push(`estimated_hours = $${params.length + 1}`);
params.push(estimated_hours);
oldValues.estimated_hours = assignment.estimated_hours;
}
if (actual_hours !== undefined) {
updateFields.push(`actual_hours = $${params.length + 1}`);
params.push(actual_hours);
oldValues.actual_hours = assignment.actual_hours;
}
if (notes !== undefined) {
updateFields.push(`notes = $${params.length + 1}`);
params.push(notes);
oldValues.notes = assignment.notes;
}
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
message: 'No se proporcionaron campos para actualizar'
});
}
params.push(id, recursoId);
const updateQuery = `
UPDATE dtic.tarea_recursos
SET ${updateFields.join(', ')}
WHERE tarea_id = $${params.length - 1} AND recurso_id = $${params.length} AND activo = true
`;
await executeQuery(updateQuery, params);
// TODO: Registrar en historial - tabla tarea_recurso_historial no existe
// const historyQuery = `
// INSERT INTO dtic.tarea_recurso_historial
// (tarea_recurso_id, action, tecnico_id, old_values, new_values, details)
// VALUES ($1, 'updated', $2, $3, $4, $5)
// `;
// const newValues = { estimated_hours, actual_hours, notes };
// await executeQuery(historyQuery, [
// assignment.id,
// tecnico_id,
// JSON.stringify(oldValues),
// JSON.stringify(newValues),
// `Asignación actualizada: ${assignment.task_title} - ${assignment.resource_name}`
// ]);
res.json({
success: true,
message: 'Asignación actualizada exitosamente'
});
} catch (error) {
console.error('Error actualizando asignación:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// DELETE /api/tareas/:id/recursos/:recursoId - Desasignar recurso de tarea
router.delete('/tareas/:id/recursos/:recursoId', [
param('id').isInt({ min: 1 }).toInt(),
param('recursoId').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const { id, recursoId } = req.params;
// Verificar que la asignación existe y está activa
const assignmentQuery = `
SELECT tr.*, t.title as task_title, r.name as resource_name
FROM dtic.tarea_recursos tr
JOIN dtic.tareas t ON t.id = tr.tarea_id
JOIN dtic.recursos r ON r.id = tr.recurso_id
WHERE tr.tarea_id = $1 AND tr.recurso_id = $2 AND tr.activo = true
`;
const assignmentResult = await executeQuery(assignmentQuery, [id, recursoId]);
if (assignmentResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Asignación no encontrada o ya inactiva'
});
}
const assignment = assignmentResult.rows[0];
const tecnico_id = req.user?.id || 1;
// Desactivar asignación
const updateQuery = `
UPDATE dtic.tarea_recursos
SET activo = false, unassigned_by = $1, unassigned_at = CURRENT_TIMESTAMP
WHERE tarea_id = $2 AND recurso_id = $3 AND activo = true
`;
await executeQuery(updateQuery, [tecnico_id, id, recursoId]);
// TODO: Registrar en historial - tabla tarea_recurso_historial no existe
// const historyQuery = `
// INSERT INTO dtic.tarea_recurso_historial
// (tarea_recurso_id, action, tecnico_id, details)
// VALUES ($1, 'unassigned', $2, $3)
// `;
// await executeQuery(historyQuery, [
// assignment.id,
// tecnico_id,
// `Recurso desasignado de tarea: ${assignment.task_title}`
// ]);
res.json({
success: true,
message: 'Recurso desasignado exitosamente de la tarea'
});
} catch (error) {
console.error('Error desasignando recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// GET /api/recursos/:id/tareas - Obtener tareas que usan un recurso
router.get('/recursos/:id/tareas', [
param('id').isInt({ min: 1 }).toInt(),
query('include_completed').optional().isBoolean()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { include_completed } = req.query;
// Verificar que el recurso existe
const resourceQuery = 'SELECT id, name FROM dtic.recursos WHERE id = $1';
const resourceResult = await executeQuery(resourceQuery, [id]);
if (resourceResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Recurso no encontrado'
});
}
let statusCondition = 'tr.activo = true';
if (include_completed === 'true') {
statusCondition = 'true'; // Incluir todas las asignaciones
}
const query = `
SELECT
tr.id,
tr.tarea_id,
tr.assigned_at,
tr.estimated_hours,
tr.actual_hours,
tr.notes,
tr.activo,
t.dtic_id as tarea_dtic_id,
t.title as tarea_title,
t.status as tarea_status,
t.priority as tarea_priority,
tec.first_name || ' ' || tec.last_name as tecnico_name
FROM dtic.tarea_recursos tr
JOIN dtic.tareas t ON t.id = tr.tarea_id
LEFT JOIN dtic.tecnicos tec ON tec.id = t.technician_id
WHERE tr.recurso_id = $1 AND ${statusCondition}
ORDER BY tr.assigned_at DESC
`;
const result = await executeQuery(query, [id]);
res.json({
success: true,
message: 'Tareas que usan el recurso obtenidas exitosamente',
data: {
resource: resourceResult.rows[0],
assignments: result.rows
}
});
} catch (error) {
console.error('Error obteniendo tareas del recurso:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
module.exports = router;
@@ -0,0 +1,417 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { executeQuery } = require('../database');
const router = express.Router();
// Función helper para generar DTIC ID para tareas
const generateDTICId = async (prefix = 'TAR') => {
// Generar ID simple con formato TAR-XXXX
const random = Math.floor(Math.random() * 10000);
return `${prefix}-${random.toString().padStart(4, '0')}`;
};
// GET /api/tareas - Obtener lista de tareas
router.get('/', [
query('page').optional().isInt({ min: 1 }).toInt(),
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
query('technician_id').optional().isInt().toInt(),
query('status').optional().isIn(['pending', 'in_progress', 'completed', 'cancelled']),
query('priority').optional().isIn(['low', 'medium', 'high', 'urgent'])
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const {
page = 1,
limit = 20,
technician_id,
status,
priority
} = req.query;
let query = `
SELECT
t.id, t.dtic_id, t.title, t.description, t.status, t.priority,
t.created_at, t.updated_at, t.due_date, t.completed_at,
tec.id as technician_id, tec.first_name, tec.last_name,
CONCAT(tec.first_name, ' ', tec.last_name) as technician_name,
COALESCE(
json_agg(
json_build_object(
'id', tr.recurso_id,
'name', r.name,
'category', r.category,
'dtic_id', r.dtic_id,
'estimated_hours', tr.estimated_hours,
'actual_hours', tr.actual_hours,
'notes', tr.notes
)
) FILTER (WHERE tr.id IS NOT NULL AND tr.activo = true),
'[]'::json
) as assigned_resources
FROM dtic.tareas t
LEFT JOIN dtic.tecnicos tec ON tec.id = t.technician_id
LEFT JOIN dtic.tarea_recursos tr ON tr.tarea_id = t.id AND tr.activo = true
LEFT JOIN dtic.recursos r ON r.id = tr.recurso_id
WHERE 1=1
`;
const params = [];
const conditions = [];
if (technician_id) {
conditions.push("t.technician_id = $" + (params.length + 1));
params.push(technician_id);
}
if (status) {
conditions.push("t.status = $" + (params.length + 1));
params.push(status);
}
if (priority) {
conditions.push("t.priority = $" + (params.length + 1));
params.push(priority);
}
if (conditions.length > 0) {
query += " AND " + conditions.join(" AND ");
}
query += " GROUP BY t.id, tec.id, tec.first_name, tec.last_name ORDER BY t.created_at DESC";
// Paginación
const offset = (page - 1) * limit;
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
params.push(limit, offset);
const result = await executeQuery(query, params);
// Obtener total para paginación
let countQuery = "SELECT COUNT(*) as total FROM dtic.tareas WHERE 1=1";
const countParams = conditions.length > 0 ? params.slice(0, -2) : [];
if (conditions.length > 0) {
countQuery += " AND " + conditions.join(" AND ");
}
const countResult = await executeQuery(countQuery, countParams);
const total = parseInt(countResult.rows[0].total);
res.json({
success: true,
message: 'Tareas obtenidas exitosamente',
data: {
tasks: result.rows,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
console.error('Error obteniendo tareas:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// GET /api/tareas/:id - Obtener tarea específica
router.get('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
const query = `
SELECT
t.*,
tec.id as technician_id, tec.first_name, tec.last_name,
CONCAT(tec.first_name, ' ', tec.last_name) as technician_name,
COALESCE(
json_agg(
json_build_object(
'id', tr.recurso_id,
'name', r.name,
'category', r.category,
'dtic_id', r.dtic_id,
'estimated_hours', tr.estimated_hours,
'actual_hours', tr.actual_hours,
'notes', tr.notes
)
) FILTER (WHERE tr.id IS NOT NULL AND tr.activo = true),
'[]'::json
) as assigned_resources
FROM dtic.tareas t
LEFT JOIN dtic.tecnicos tec ON tec.id = t.technician_id
LEFT JOIN dtic.tarea_recursos tr ON tr.tarea_id = t.id AND tr.activo = true
LEFT JOIN dtic.recursos r ON r.id = tr.recurso_id
WHERE t.id = $1
GROUP BY t.id, tec.id, tec.first_name, tec.last_name
`;
const result = await executeQuery(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Tarea no encontrada'
});
}
res.json({
success: true,
message: 'Tarea obtenida exitosamente',
data: { task: result.rows[0] }
});
} catch (error) {
console.error('Error obteniendo tarea:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/tareas - Crear nueva tarea
router.post('/', [
body('title').trim().isLength({ min: 3, max: 200 }),
body('description').optional().trim().isLength({ max: 1000 }),
body('technician_id').optional().isInt({ min: 1 }).toInt(),
body('priority').optional().isIn(['low', 'medium', 'high', 'urgent']),
body('due_date').optional().isISO8601()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { title, description, technician_id, priority = 'medium', due_date } = req.body;
// Verificar que el técnico existe si se especifica
if (technician_id) {
const techQuery = 'SELECT id FROM dtic.tecnicos WHERE id = $1 AND is_active = true';
const techResult = await executeQuery(techQuery, [technician_id]);
if (techResult.rows.length === 0) {
return res.status(400).json({
success: false,
message: 'Técnico no encontrado o inactivo'
});
}
}
// Generar DTIC ID
const dticId = await generateDTICId('TAR');
// Insertar tarea
const query = `
INSERT INTO dtic.tareas
(dtic_id, title, description, technician_id, priority, due_date, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
`;
const result = await executeQuery(query, [
dticId, title, description, technician_id, priority, due_date, 'pending'
]);
res.status(201).json({
success: true,
message: 'Tarea creada exitosamente',
data: {
task_id: result.rows[0].id,
dtic_id: dticId
}
});
} catch (error) {
console.error('Error creando tarea:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// PUT /api/tareas/:id - Actualizar tarea
router.put('/:id', [
param('id').isInt({ min: 1 }).toInt(),
body('title').optional().trim().isLength({ min: 3, max: 200 }),
body('description').optional().trim().isLength({ max: 1000 }),
body('technician_id').optional().isInt({ min: 1 }).toInt(),
body('status').optional().isIn(['pending', 'in_progress', 'completed', 'cancelled']),
body('priority').optional().isIn(['low', 'medium', 'high', 'urgent']),
body('due_date').optional().isISO8601()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { title, description, technician_id, status, priority, due_date } = req.body;
// Verificar que la tarea existe
const existingQuery = 'SELECT * FROM dtic.tareas WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Tarea no encontrada'
});
}
// Verificar técnico si se está cambiando
if (technician_id) {
const techQuery = 'SELECT id FROM dtic.tecnicos WHERE id = $1 AND is_active = true';
const techResult = await executeQuery(techQuery, [technician_id]);
if (techResult.rows.length === 0) {
return res.status(400).json({
success: false,
message: 'Técnico no encontrado o inactivo'
});
}
}
// Construir query de actualización
const updateFields = [];
const params = [];
if (title !== undefined) {
updateFields.push(`title = $${params.length + 1}`);
params.push(title);
}
if (description !== undefined) {
updateFields.push(`description = $${params.length + 1}`);
params.push(description);
}
if (technician_id !== undefined) {
updateFields.push(`technician_id = $${params.length + 1}`);
params.push(technician_id);
}
if (status !== undefined) {
updateFields.push(`status = $${params.length + 1}`);
params.push(status);
// Si se marca como completada, actualizar completed_at
if (status === 'completed') {
updateFields.push(`completed_at = CURRENT_TIMESTAMP`);
}
}
if (priority !== undefined) {
updateFields.push(`priority = $${params.length + 1}`);
params.push(priority);
}
if (due_date !== undefined) {
updateFields.push(`due_date = $${params.length + 1}`);
params.push(due_date);
}
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
message: 'No se proporcionaron campos para actualizar'
});
}
params.push(id);
const query = `
UPDATE dtic.tareas
SET ${updateFields.join(', ')}
WHERE id = $${params.length}
`;
await executeQuery(query, params);
res.json({
success: true,
message: 'Tarea actualizada exitosamente'
});
} catch (error) {
console.error('Error actualizando tarea:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// DELETE /api/tareas/:id - Eliminar tarea
router.delete('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
// Verificar que la tarea existe
const existingQuery = 'SELECT * FROM dtic.tareas WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Tarea no encontrada'
});
}
// Eliminar tarea
await executeQuery('DELETE FROM dtic.tareas WHERE id = $1', [id]);
res.json({
success: true,
message: 'Tarea eliminada exitosamente'
});
} catch (error) {
console.error('Error eliminando tarea:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
module.exports = router;
@@ -0,0 +1,562 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { pool } = require('../database');
const auth = require('../middleware/auth');
const router = express.Router();
// Middleware de autenticación (temporalmente comentado para desarrollo)
// router.use(auth);
// Función helper para ejecutar queries
const executeQuery = async (query, params = []) => {
const client = await pool.connect();
try {
const result = await client.query(query, params);
return result;
} finally {
client.release();
}
};
// Función helper para generar DTIC ID
const generateDTICId = async (prefix = 'TEC') => {
// Generar ID simple sin función de PostgreSQL por ahora
const timestamp = Date.now().toString().slice(-4);
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
return `${prefix}-${timestamp}${random}`;
};
// Función helper para validar email único
const isEmailUnique = async (email, excludeId = null) => {
let query = 'SELECT id FROM dtic.tecnicos WHERE email = $1';
const params = [email];
if (excludeId) {
query += ' AND id != $2';
params.push(excludeId);
}
const result = await executeQuery(query, params);
return result.rows.length === 0;
};
// GET /api/tecnicos - Obtener lista de técnicos con filtros
router.get('/', [
query('page').optional().isInt({ min: 1 }).toInt(),
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
query('search').optional().isString().trim(),
query('department').optional().isIn(['dtic', 'sistemas', 'redes', 'seguridad']),
query('role').optional().isIn(['admin', 'technician', 'viewer']),
query('status').optional().isIn(['active', 'inactive', 'all'])
], async (req, res) => {
try {
// Validar parámetros
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const {
page = 1,
limit = 12,
search,
department,
role,
status
} = req.query;
// Construir query base
let query = `
SELECT
tec.id, tec.dtic_id, tec.first_name, tec.last_name,
CONCAT(tec.first_name, ' ', tec.last_name) as full_name,
tec.email, tec.phone, tec.department, tec.role, tec.is_active,
tec.created_at, tec.updated_at,
COUNT(t.id) as active_tasks
FROM dtic.tecnicos tec
LEFT JOIN dtic.tareas t ON t.technician_id = tec.id AND t.status IN ('pending', 'in_progress')
WHERE 1=1
`;
const params = [];
const conditions = [];
// Filtros
if (search) {
conditions.push("(tec.first_name ILIKE $1 OR tec.last_name ILIKE $1 OR tec.email ILIKE $1 OR tec.dtic_id ILIKE $1)");
params.push(`%${search}%`);
}
if (department) {
conditions.push("tec.department = $" + (params.length + 1));
params.push(department);
}
if (role) {
conditions.push("tec.role = $" + (params.length + 1));
params.push(role);
}
if (status) {
if (status === 'active') {
conditions.push("tec.is_active = $" + (params.length + 1));
params.push(true);
} else if (status === 'inactive') {
conditions.push("tec.is_active = $" + (params.length + 1));
params.push(false);
}
// 'all' no agrega condición (muestra todos)
}
// Por defecto mostrar todos los técnicos (activos e inactivos)
if (conditions.length > 0) {
query += " AND " + conditions.join(" AND ");
}
query += " GROUP BY tec.id ORDER BY tec.is_active DESC, tec.last_name, tec.first_name";
// Paginación
const offset = (page - 1) * limit;
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
params.push(limit, offset);
// Ejecutar query principal
console.log('[DEBUG] Executing query:', query);
console.log('[DEBUG] With params:', params);
const result = await executeQuery(query, params);
// Obtener total para paginación
let countQuery = "SELECT COUNT(*) as total FROM dtic.tecnicos tec WHERE 1=1";
const countParams = [];
if (conditions.length > 0) {
countQuery += " AND " + conditions.join(" AND ");
countParams.push(...params.slice(0, -2));
}
const countResult = await executeQuery(countQuery, countParams);
const total = parseInt(countResult.rows[0].total);
res.json({
success: true,
message: 'Técnicos obtenidos exitosamente',
data: {
tecnicos: result.rows,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
console.error('Error obteniendo técnicos:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// GET /api/tecnicos/:id - Obtener técnico específico
router.get('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
const { include_tasks } = req.query;
let query = `
SELECT
t.*,
COUNT(ts.id) as total_tasks,
COUNT(CASE WHEN ts.status = 'completed' THEN 1 END) as completed_tasks,
COUNT(CASE WHEN ts.status IN ('pending', 'in_progress') THEN 1 END) as active_tasks
FROM dtic.tecnicos t
LEFT JOIN dtic.tareas ts ON ts.technician_id = t.id
WHERE t.id = $1
GROUP BY t.id
`;
const result = await executeQuery(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Técnico no encontrado'
});
}
const technician = result.rows[0];
// Incluir tareas recientes si se solicita
if (include_tasks === 'true') {
const tasksQuery = `
SELECT id, dtic_id, title, status, priority, created_at, due_date
FROM dtic.tareas
WHERE technician_id = $1
ORDER BY created_at DESC
LIMIT 5
`;
const tasksResult = await executeQuery(tasksQuery, [id]);
technician.recent_tasks = tasksResult.rows;
}
res.json({
success: true,
message: 'Técnico obtenido exitosamente',
data: { technician }
});
} catch (error) {
console.error('Error obteniendo técnico:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/tecnicos - Crear nuevo técnico
router.post('/', [
body('first_name').trim().isLength({ min: 2, max: 50 }),
body('last_name').trim().isLength({ min: 2, max: 50 }),
body('email').isEmail().normalizeEmail(),
body('department').isIn(['dtic', 'sistemas', 'redes', 'seguridad']),
body('role').isIn(['admin', 'technician', 'viewer']),
body('phone').optional()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { first_name, last_name, email, department, role, phone } = req.body;
// Verificar email único
if (!(await isEmailUnique(email))) {
return res.status(409).json({
success: false,
message: 'El email ya está registrado'
});
}
// Generar DTIC ID
const dticId = await generateDTICId('TEC');
// Insertar técnico (apellido siempre en mayúsculas)
const query = `
INSERT INTO dtic.tecnicos
(dtic_id, first_name, last_name, email, phone, department, role, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`;
const result = await executeQuery(query, [
dticId, first_name, last_name.toUpperCase(), email, phone, department, role, true
]);
res.status(201).json({
success: true,
message: 'Técnico creado exitosamente',
data: {
technician_id: result.rows[0].id,
dtic_id: dticId
}
});
} catch (error) {
console.error('Error creando técnico:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// PUT /api/tecnicos/:id - Actualizar técnico
router.put('/:id', [
param('id').isInt({ min: 1 }).toInt(),
query('action').optional().isIn(['change_password']),
body('first_name').optional().trim().isLength({ min: 2, max: 50 }),
body('last_name').optional().trim().isLength({ min: 2, max: 50 }),
body('email').optional().isEmail().normalizeEmail(),
body('department').optional().isIn(['dtic', 'sistemas', 'redes', 'seguridad']),
body('role').optional().isIn(['admin', 'technician', 'viewer']),
body('phone').optional(),
body('is_active').optional().isBoolean(),
body('new_password').optional().isLength({ min: 8 }),
body('force_change').optional().isBoolean()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { action } = req.query;
const { first_name, last_name, email, department, role, phone, is_active, new_password, force_change } = req.body;
// Verificar si es cambio de contraseña
if (action === 'change_password') {
return await changeTechnicianPassword(id, { new_password, force_change }, res);
}
// Verificar que el técnico existe
const existingQuery = 'SELECT * FROM dtic.tecnicos WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Técnico no encontrado'
});
}
// Verificar email único si se está cambiando
if (email && email !== existingResult.rows[0].email) {
if (!(await isEmailUnique(email, id))) {
return res.status(409).json({
success: false,
message: 'El email ya está registrado por otro técnico'
});
}
}
// Construir query de actualización
const updateFields = [];
const params = [];
if (first_name !== undefined) {
updateFields.push(`first_name = $${params.length + 1}`);
params.push(first_name);
}
if (last_name !== undefined) {
updateFields.push(`last_name = $${params.length + 1}`);
params.push(last_name.toUpperCase()); // Siempre convertir apellido a mayúsculas
}
if (email !== undefined) {
updateFields.push(`email = $${params.length + 1}`);
params.push(email);
}
if (department !== undefined) {
updateFields.push(`department = $${params.length + 1}`);
params.push(department);
}
if (role !== undefined) {
updateFields.push(`role = $${params.length + 1}`);
params.push(role);
}
if (phone !== undefined) {
updateFields.push(`phone = $${params.length + 1}`);
params.push(phone);
}
if (is_active !== undefined) {
updateFields.push(`is_active = $${params.length + 1}`);
params.push(is_active);
}
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
message: 'No se proporcionaron campos para actualizar'
});
}
params.push(id);
const query = `
UPDATE dtic.tecnicos
SET ${updateFields.join(', ')}
WHERE id = $${params.length}
`;
await executeQuery(query, params);
res.json({
success: true,
message: 'Técnico actualizado exitosamente'
});
} catch (error) {
console.error('Error actualizando técnico:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// DELETE /api/tecnicos/:id - Eliminar técnico
router.delete('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
// Verificar que el técnico existe
const existingQuery = 'SELECT * FROM dtic.tecnicos WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Técnico no encontrado'
});
}
const technician = existingResult.rows[0];
if (technician.is_active) {
// Verificar tareas activas antes de desactivar
const tasksQuery = `
SELECT COUNT(*) as count
FROM dtic.tareas
WHERE technician_id = $1 AND status IN ('pending', 'in_progress')
`;
const tasksResult = await executeQuery(tasksQuery, [id]);
if (parseInt(tasksResult.rows[0].count) > 0) {
return res.status(409).json({
success: false,
message: 'No se puede desactivar el técnico porque tiene tareas activas'
});
}
// Desactivar técnico
await executeQuery('UPDATE dtic.tecnicos SET is_active = false WHERE id = $1', [id]);
res.json({
success: true,
message: 'Técnico desactivado exitosamente'
});
} else {
// Verificar tareas activas antes de eliminar permanentemente
const tasksQuery = `
SELECT COUNT(*) as count
FROM dtic.tareas
WHERE technician_id = $1 AND status IN ('pending', 'in_progress')
`;
const tasksResult = await executeQuery(tasksQuery, [id]);
if (parseInt(tasksResult.rows[0].count) > 0) {
return res.status(409).json({
success: false,
message: 'No se puede eliminar permanentemente el técnico porque tiene tareas activas'
});
}
// Eliminar permanentemente
await executeQuery('DELETE FROM dtic.tecnicos WHERE id = $1', [id]);
res.json({
success: true,
message: 'Técnico eliminado permanentemente del sistema'
});
}
} catch (error) {
console.error('Error eliminando técnico:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// Función para cambiar contraseña de técnico
const changeTechnicianPassword = async (id, { new_password, force_change }, res) => {
try {
// Verificar que el técnico existe
const existingQuery = 'SELECT * FROM dtic.tecnicos WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Técnico no encontrado'
});
}
// Validar contraseña
if (!new_password || new_password.length < 8) {
return res.status(400).json({
success: false,
message: 'La contraseña debe tener al menos 8 caracteres'
});
}
// Validar fortaleza de contraseña
const hasUpperCase = /[A-Z]/.test(new_password);
const hasLowerCase = /[a-z]/.test(new_password);
const hasNumbers = /\d/.test(new_password);
const hasNonalphas = /[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]/.test(new_password);
if (!(hasUpperCase && hasLowerCase && hasNumbers && hasNonalphas)) {
return res.status(400).json({
success: false,
message: 'La contraseña debe contener mayúsculas, minúsculas, números y caracteres especiales'
});
}
// Hashear nueva contraseña
const bcrypt = require('bcryptjs');
const hashedPassword = await bcrypt.hash(new_password, 12);
// Actualizar contraseña
const query = 'UPDATE dtic.tecnicos SET password_hash = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2';
await executeQuery(query, [hashedPassword, id]);
// Log de auditoría
console.log(`[AUDIT] Contraseña cambiada para técnico ID: ${id}, force_change: ${force_change || false}`);
res.json({
success: true,
message: 'Contraseña cambiada exitosamente'
});
} catch (error) {
console.error('Error cambiando contraseña:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
};
module.exports = router;
@@ -0,0 +1,414 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
const auth = require('../middleware/auth');
const router = express.Router();
// Middleware de autenticación (temporalmente comentado para desarrollo)
// router.use(auth);
// Función helper para ejecutar queries
const executeQuery = async (query, params = []) => {
try {
const result = await pool.query(query, params);
return result;
} catch (error) {
console.error('Database query error:', error);
throw error;
}
};
// Función helper para generar DTIC ID para usuarios relacionados
const generateDTICId = async (prefix = 'USR') => {
const query = `
SELECT generate_dtic_id($1) as new_id
`;
const result = await executeQuery(query, [prefix]);
return result.rows[0].new_id;
};
// GET /api/usuarios_relacionados - Obtener lista de usuarios relacionados
router.get('/', [
query('page').optional().isInt({ min: 1 }).toInt(),
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
query('search').optional().isString().trim(),
query('department').optional().isString().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Parámetros inválidos',
errors: errors.array()
});
}
const {
page = 1,
limit = 20,
search,
department
} = req.query;
let query = `
SELECT
u.id, u.dtic_id, u.first_name, u.last_name,
CONCAT(u.first_name, ' ', u.last_name) as full_name,
u.email, u.phone, u.department, u.position,
u.created_at, u.updated_at,
COUNT(DISTINCT ra.recurso_id) as assigned_resources_count
FROM dtic.usuarios_relacionados u
LEFT JOIN dtic.recurso_asignaciones ra ON ra.user_id = u.id AND ra.activo = true
WHERE 1=1
`;
const params = [];
const conditions = [];
// Filtros
if (search) {
conditions.push("(u.first_name ILIKE $1 OR u.last_name ILIKE $1 OR u.email ILIKE $1 OR u.dtic_id ILIKE $1)");
params.push(`%${search}%`);
}
if (department) {
conditions.push("u.department ILIKE $" + (params.length + 1));
params.push(`%${department}%`);
}
if (conditions.length > 0) {
query += " AND " + conditions.join(" AND ");
}
query += " GROUP BY u.id ORDER BY u.last_name, u.first_name";
// Paginación
const offset = (page - 1) * limit;
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
params.push(limit, offset);
const result = await executeQuery(query, params);
// Obtener total para paginación
let countQuery = "SELECT COUNT(*) as total FROM dtic.usuarios_relacionados u WHERE 1=1";
const countParams = [];
if (conditions.length > 0) {
countQuery += " AND " + conditions.join(" AND ");
countParams.push(...params.slice(0, -2));
}
const countResult = await executeQuery(countQuery, countParams);
const total = parseInt(countResult.rows[0].total);
res.json({
success: true,
message: 'Usuarios relacionados obtenidos exitosamente',
data: {
usuarios: result.rows,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
console.error('Error obteniendo usuarios relacionados:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// GET /api/usuarios_relacionados/:id - Obtener usuario asignado específico
router.get('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
const { include_resources } = req.query;
let query = `
SELECT
u.*,
COUNT(DISTINCT ra.recurso_id) as total_assigned_resources
FROM dtic.usuarios_relacionados u
LEFT JOIN dtic.recurso_asignaciones ra ON ra.user_id = u.id AND ra.activo = true
WHERE u.id = $1
GROUP BY u.id
`;
const result = await executeQuery(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Usuario asignado no encontrado'
});
}
const usuario = result.rows[0];
// Obtener recursos relacionados si se solicita
if (include_resources === 'true') {
const resourcesQuery = `
SELECT
r.id, r.dtic_id, r.name, r.category, r.status, r.location,
ra.assigned_at
FROM dtic.recurso_asignaciones ra
JOIN dtic.recursos r ON r.id = ra.recurso_id
WHERE ra.user_id = $1 AND ra.activo = true
ORDER BY ra.assigned_at DESC
`;
const resourcesResult = await executeQuery(resourcesQuery, [id]);
usuario.assigned_resources = resourcesResult.rows;
}
res.json({
success: true,
message: 'Usuario asignado obtenido exitosamente',
data: { usuario }
});
} catch (error) {
console.error('Error obteniendo usuario asignado:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// POST /api/usuarios_relacionados - Crear nuevo usuario asignado
router.post('/', [
body('first_name').trim().isLength({ min: 2, max: 50 }),
body('last_name').trim().isLength({ min: 2, max: 50 }),
body('email').optional().isEmail().normalizeEmail(),
body('phone').optional().isMobilePhone('es-AR'),
body('department').optional().trim().isLength({ max: 100 }),
body('position').optional().trim().isLength({ max: 100 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { first_name, last_name, email, phone, department, position } = req.body;
// Verificar email único si se proporciona
if (email) {
const emailCheck = await executeQuery('SELECT id FROM dtic.usuarios_relacionados WHERE email = $1', [email]);
if (emailCheck.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'El email ya está registrado'
});
}
}
// Generar DTIC ID
const dticId = await generateDTICId('USR');
// Insertar usuario relacionado
const query = `
INSERT INTO dtic.usuarios_relacionados
(dtic_id, first_name, last_name, email, phone, department, position)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
`;
const result = await executeQuery(query, [
dticId, first_name, last_name.toUpperCase(), email, phone, department, position
]);
res.status(201).json({
success: true,
message: 'Usuario asignado creado exitosamente',
data: {
usuario_id: result.rows[0].id,
dtic_id: dticId
}
});
} catch (error) {
console.error('Error creando usuario asignado:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// PUT /api/usuarios_relacionados/:id - Actualizar usuario asignado
router.put('/:id', [
param('id').isInt({ min: 1 }).toInt(),
body('first_name').optional().trim().isLength({ min: 2, max: 50 }),
body('last_name').optional().trim().isLength({ min: 2, max: 50 }),
body('email').optional().isEmail().normalizeEmail(),
body('phone').optional().isMobilePhone('es-AR'),
body('department').optional().trim().isLength({ max: 100 }),
body('position').optional().trim().isLength({ max: 100 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Datos inválidos',
errors: errors.array()
});
}
const { id } = req.params;
const { first_name, last_name, email, phone, department, position } = req.body;
// Verificar que el usuario existe
const existingQuery = 'SELECT * FROM dtic.usuarios_relacionados WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Usuario asignado no encontrado'
});
}
// Verificar email único si se está cambiando
if (email && email !== existingResult.rows[0].email) {
const emailCheck = await executeQuery('SELECT id FROM dtic.usuarios_relacionados WHERE email = $1 AND id != $2', [email, id]);
if (emailCheck.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'El email ya está registrado por otro usuario'
});
}
}
// Construir query de actualización
const updateFields = [];
const params = [];
const allowedFields = ['first_name', 'last_name', 'email', 'phone', 'department', 'position'];
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
updateFields.push(`${field} = $${params.length + 1}`);
params.push(field === 'last_name' ? req.body[field].toUpperCase() : req.body[field]);
}
});
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
message: 'No se proporcionaron campos para actualizar'
});
}
params.push(id);
const query = `
UPDATE dtic.usuarios_relacionados
SET ${updateFields.join(', ')}
WHERE id = $${params.length}
`;
await executeQuery(query, params);
res.json({
success: true,
message: 'Usuario asignado actualizado exitosamente'
});
} catch (error) {
console.error('Error actualizando usuario asignado:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
// DELETE /api/usuarios_relacionados/:id - Eliminar usuario asignado
router.delete('/:id', [
param('id').isInt({ min: 1 }).toInt()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'ID inválido',
errors: errors.array()
});
}
const { id } = req.params;
// Verificar que el usuario existe
const existingQuery = 'SELECT * FROM dtic.usuarios_relacionados WHERE id = $1';
const existingResult = await executeQuery(existingQuery, [id]);
if (existingResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Usuario asignado no encontrado'
});
}
// Verificar que no tenga recursos relacionados activos
const resourcesQuery = `
SELECT COUNT(*) as count
FROM dtic.recurso_asignaciones
WHERE user_id = $1 AND activo = true
`;
const resourcesResult = await executeQuery(resourcesQuery, [id]);
if (parseInt(resourcesResult.rows[0].count) > 0) {
return res.status(409).json({
success: false,
message: 'No se puede eliminar el usuario porque tiene recursos relacionados'
});
}
// Eliminar usuario asignado
await executeQuery('DELETE FROM dtic.usuarios_relacionados WHERE id = $1', [id]);
res.json({
success: true,
message: 'Usuario asignado eliminado exitosamente'
});
} catch (error) {
console.error('Error eliminando usuario asignado:', error);
res.status(500).json({
success: false,
message: 'Error interno del servidor'
});
}
});
module.exports = router;
@@ -0,0 +1,97 @@
const { executeQuery } = require('./database');
const sampleTasks = [
{
title: 'Revisar configuración de red',
description: 'Verificar configuración de switches y routers en sala de servidores',
priority: 'high',
status: 'pending'
},
{
title: 'Actualizar software antivirus',
description: 'Actualizar definiciones de virus en todos los equipos',
priority: 'medium',
status: 'in_progress'
},
{
title: 'Configurar backup automático',
description: 'Implementar sistema de backup diario para bases de datos críticas',
priority: 'urgent',
status: 'pending'
},
{
title: 'Instalar actualizaciones Windows',
description: 'Aplicar parches de seguridad en servidores Windows',
priority: 'medium',
status: 'completed'
},
{
title: 'Diagnosticar problema de impresora',
description: 'Resolver error de conexión en impresora multifunción',
priority: 'low',
status: 'pending'
},
{
title: 'Configurar VPN para usuarios remotos',
description: 'Establecer conexiones seguras para trabajo remoto',
priority: 'high',
status: 'in_progress'
},
{
title: 'Auditoría de seguridad mensual',
description: 'Revisar logs y vulnerabilidades del sistema',
priority: 'medium',
status: 'pending'
},
{
title: 'Migración de datos a nuevo servidor',
description: 'Transferir archivos y configuraciones al servidor actualizado',
priority: 'urgent',
status: 'pending'
}
];
async function insertSampleData() {
try {
console.log('Cleaning existing sample tasks...');
// Limpiar tareas existentes que podrían tener IDs incorrectos
await executeQuery('DELETE FROM dtic.tareas WHERE dtic_id LIKE \'TAR%\' AND title IN ($1, $2, $3, $4, $5, $6, $7, $8)', [
'Revisar configuración de red',
'Actualizar software antivirus',
'Configurar backup automático',
'Instalar actualizaciones Windows',
'Diagnosticar problema de impresora',
'Configurar VPN para usuarios remotos',
'Auditoría de seguridad mensual',
'Migración de datos a nuevo servidor'
]);
console.log('Inserting sample tasks with correct DTIC IDs...');
for (const task of sampleTasks) {
const dticId = 'TAR-' + Math.floor(Math.random() * 10000).toString().padStart(4, '0');
await executeQuery(
'INSERT INTO dtic.tareas (dtic_id, title, description, priority, status, created_at) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)',
[dticId, task.title, task.description, task.priority, task.status]
);
console.log(`✓ Inserted task: ${task.title} (ID: ${dticId})`);
}
console.log('\n🎉 Sample tasks inserted successfully with correct DTIC IDs!');
console.log('You can now view the tasks in the TareasRefactored page.');
} catch (error) {
console.error('❌ Error inserting sample data:', error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
insertSampleData();
}
module.exports = { insertSampleData, sampleTasks };
+202
View File
@@ -0,0 +1,202 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
// Configuración de PostgreSQL
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Establecer search_path para incluir el esquema dtic
pool.on('connect', (client) => {
client.query('SET search_path TO dtic, public');
});
// Importar rutas
const authRoutes = require('./routes/auth');
const tecnicosRoutes = require('./routes/tecnicos');
const tareasRoutes = require('./routes/tareas');
const recursosRoutes = require('./routes/recursos');
const usuariosRelacionadosRoutes = require('./routes/usuarios_relacionados');
const tareaRecursosRoutes = require('./routes/tarea-recursos');
const systemRoutes = require('./routes/system');
// Configuración de la aplicación
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware de seguridad
app.use(helmet({
crossOriginResourcePolicy: { policy: "cross-origin" }
}));
// Rate limiting - más permisivo para desarrollo
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 1000, // límite de 1000 requests por windowMs (aumentado para desarrollo)
message: 'Demasiadas solicitudes desde esta IP, por favor intenta más tarde.'
});
app.use('/api/', limiter);
// Rate limiting más estricto para autenticación
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // límite de 5 requests por windowMs
message: 'Demasiados intentos de autenticación, por favor intenta más tarde.'
});
app.use('/api/auth/login', authLimiter);
// CORS - Allow all origins for development and remote access
app.use(cors({
origin: true, // Allow all origins
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));
// Compresión
app.use(compression());
// Body parsing with logging
app.use(express.json({
limit: '10mb',
verify: (req, res, buf) => {
console.log(`[PAYLOAD SIZE] ${req.method} ${req.path}: ${buf.length} bytes`);
}
}));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Logging básico
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// Rutas de la API
app.use('/api/auth', authRoutes);
app.use('/api/tecnicos', tecnicosRoutes);
app.use('/api/tareas', tareasRoutes);
app.use('/api/recursos', recursosRoutes);
app.use('/api/tarea-recursos', tareaRecursosRoutes); // Rutas para asignaciones tarea-recurso
app.use('/api/system', systemRoutes);
// Ruta para servir el archivo de configuración YAML
app.get('/api/config/entities.yml', (req, res) => {
const path = require('path');
const fs = require('fs');
// El archivo está montado en /host/_app-npm/frontend/src/config/entities.yml
const configPath = path.join('/host', '_app-npm', 'frontend', 'src', 'config', 'entities.yml');
console.log(`[CONFIG] Intentando leer archivo desde: ${configPath}`);
fs.readFile(configPath, 'utf8', (err, data) => {
if (err) {
console.error('Error leyendo archivo de configuración:', err);
console.error('Path attempted:', configPath);
return res.status(500).json({
success: false,
message: 'Error al leer el archivo de configuración',
details: process.env.NODE_ENV === 'development' ? err.message : undefined
});
}
console.log(`[CONFIG] Archivo de configuración leído exitosamente (${data.length} bytes)`);
res.setHeader('Content-Type', 'text/yaml');
res.send(data);
});
});
app.use('/api/usuarios_relacionados', usuariosRelacionadosRoutes);
// Manejo de errores 404
app.use('/api/*', (req, res) => {
res.status(404).json({
success: false,
message: 'Endpoint no encontrado'
});
});
// Middleware de manejo de errores
app.use((err, req, res, next) => {
console.error('Error:', err);
// Error de validación
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: 'Datos de entrada inválidos',
errors: err.errors
});
}
// Error de base de datos
if (err.code && err.code.startsWith('23')) {
return res.status(400).json({
success: false,
message: 'Error de integridad de datos'
});
}
// Error interno del servidor
res.status(500).json({
success: false,
message: process.env.NODE_ENV === 'production'
? 'Error interno del servidor'
: err.message
});
});
// Función para verificar conexión a la base de datos
const testDatabaseConnection = async () => {
try {
const client = await pool.connect();
console.log('✅ Conexión a PostgreSQL exitosa');
client.release();
} catch (err) {
console.error('❌ Error conectando a PostgreSQL:', err);
process.exit(1);
}
};
// Iniciar servidor
const startServer = async () => {
await testDatabaseConnection();
app.listen(PORT, () => {
console.log(`🚀 Servidor ejecutándose en puerto ${PORT}`);
console.log(`📊 Ambiente: ${process.env.NODE_ENV || 'development'}`);
});
};
// Manejo de señales para cerrar gracefully
process.on('SIGTERM', () => {
console.log('SIGTERM recibido, cerrando servidor...');
pool.end(() => {
console.log('Pool de conexiones cerrado');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT recibido, cerrando servidor...');
pool.end(() => {
console.log('Pool de conexiones cerrado');
process.exit(0);
});
});
// Exportar app para uso en otros módulos si es necesario
module.exports = { app, pool };
startServer();
@@ -0,0 +1,89 @@
const http = require('http');
async function testLogin() {
return new Promise((resolve, reject) => {
console.log('🧪 PROBANDO LOGIN DEL USUARIO');
console.log('==============================');
const postData = JSON.stringify({
email: 'rmonla@frlr.utn.edu.ar',
password: 'utn123'
});
const options = {
hostname: 'localhost',
port: 3001,
path: '/api/auth/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = http.request(options, (res) => {
let data = '';
console.log(`Estado HTTP: ${res.statusCode}`);
console.log(`Headers:`, JSON.stringify(res.headers, null, 2));
res.on('data', (chunk) => {
data += chunk;
console.log(`Recibiendo chunk: ${chunk.length} bytes`);
});
res.on('end', () => {
console.log(`Datos totales recibidos: ${data.length} bytes`);
console.log(`Contenido crudo: '${data}'`);
if (data.trim() === '') {
console.log('❌ RESPUESTA VACÍA - Esto explica el error de JSON.parse');
console.log('💡 Posibles causas:');
console.log(' - El servidor se cerró la conexión');
console.log(' - Error interno del servidor');
console.log(' - Problema de CORS');
console.log(' - El endpoint no existe');
resolve();
return;
}
try {
const responseData = JSON.parse(data);
console.log(`Respuesta exitosa: ${responseData.success}`);
if (responseData.success) {
console.log('✅ LOGIN EXITOSO');
console.log(`Token generado: ${responseData.data.token ? 'SÍ' : 'NO'}`);
console.log(`Usuario: ${responseData.data.user.first_name} ${responseData.data.user.last_name}`);
console.log(`Rol: ${responseData.data.user.role}`);
} else {
console.log('❌ LOGIN FALLIDO');
console.log(`Mensaje: ${responseData.message}`);
if (responseData.errors) {
console.log('Errores de validación:', responseData.errors);
}
}
resolve();
} catch (error) {
console.error('❌ Error parseando respuesta JSON:', error.message);
console.log('Contenido que causó el error:', data);
resolve();
}
});
});
req.on('error', (error) => {
console.error('❌ Error en la petición HTTP:', error.message);
resolve();
});
req.write(postData);
req.end();
});
}
if (require.main === module) {
testLogin();
}
module.exports = { testLogin };
@@ -0,0 +1,62 @@
const { executeQuery } = require('./database');
// Función para verificar la integridad de los datos
async function verifyDataIntegrity() {
try {
console.log('🔍 Verificando integridad de datos...\n');
// Verificar recursos
const recursos = await executeQuery('SELECT COUNT(*) as total, category, status FROM dtic.recursos GROUP BY category, status ORDER BY category, status');
console.log('📦 Recursos por categoría y estado:');
recursos.rows.forEach(row => {
console.log(` ${row.category} (${row.status}): ${row.total}`);
});
// Verificar usuarios
const usuarios = await executeQuery('SELECT COUNT(*) as total FROM dtic.usuarios_relacionados');
console.log(`👥 Total de usuarios relacionados: ${usuarios.rows[0].total}`);
// Verificar tareas
const tareas = await executeQuery('SELECT COUNT(*) as total, status, priority FROM dtic.tareas GROUP BY status, priority ORDER BY status, priority');
console.log('📋 Tareas por estado y prioridad:');
tareas.rows.forEach(row => {
console.log(` ${row.status} (${row.priority}): ${row.total}`);
});
// Verificar asignaciones
const asignaciones = await executeQuery('SELECT COUNT(*) as total FROM dtic.recurso_asignaciones WHERE activo = true');
console.log(`🔗 Asignaciones activas: ${asignaciones.rows[0].total}`);
// Verificar historial
const historial = await executeQuery('SELECT COUNT(*) as total FROM dtic.recurso_historial');
console.log(`📜 Registros de historial: ${historial.rows[0].total}`);
// Mostrar algunos ejemplos
console.log('\n📋 Ejemplos de datos:');
const sampleRecursos = await executeQuery('SELECT dtic_id, name, category FROM dtic.recursos LIMIT 5');
console.log('Recursos:');
sampleRecursos.rows.forEach(r => console.log(` ${r.dtic_id}: ${r.name} (${r.category})`));
const sampleUsuarios = await executeQuery('SELECT dtic_id, first_name, last_name, department FROM dtic.usuarios_relacionados LIMIT 5');
console.log('Usuarios:');
sampleUsuarios.rows.forEach(u => console.log(` ${u.dtic_id}: ${u.first_name} ${u.last_name} (${u.department})`));
const sampleTareas = await executeQuery('SELECT dtic_id, title, status FROM dtic.tareas LIMIT 5');
console.log('Tareas:');
sampleTareas.rows.forEach(t => console.log(` ${t.dtic_id}: ${t.title} (${t.status})`));
console.log('\n✅ Verificación completada exitosamente!');
} catch (error) {
console.error('❌ Error verificando integridad de datos:', error);
throw error;
}
}
// Ejecutar si se llama directamente
if (require.main === module) {
verifyDataIntegrity();
}
module.exports = { verifyDataIntegrity };