Files
help_desk/routes/api.js
T

355 lines
12 KiB
JavaScript

const express = require('express');
const router = express.Router();
const Ticket = require('../models/Ticket');
const Company = require('../models/Company');
const Department = require('../models/Department');
const User = require('../models/User');
/**
* Middleware para validar llamadas M2M mediante API Key o Token de Agente
*/
async function authenticateM2M(req, res, next) {
const apiKey = req.header('x-api-key');
const authHeader = req.header('Authorization');
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
const provided = apiKey || token;
if (!provided) {
return res.status(401).json({
ok: false,
success: false,
error: 'Autenticación requerida. Proporcione header x-api-key o Authorization: Bearer <token>'
});
}
// 1. Validar contra variables locales de entorno autorizadas
const validTokens = [
process.env.WEBCONTROL_API_KEY,
process.env.SIGMA_AGENTE_TOKEN
].filter(Boolean);
if (validTokens.includes(provided)) {
req.authSource = 'local_env';
return next();
}
// 2. Validar contra la base de datos central webControl si está disponible
try {
const mysql = require('mysql2/promise');
const conn = await mysql.createConnection({
host: '192.168.10.149',
user: 'admin',
password: 'Dx.1706%',
database: 'webControl'
});
const [rows] = await conn.query(
'SELECT * FROM api_key WHERE key_value = ? AND is_active = 1 AND revoked_at IS NULL',
[provided]
);
await conn.end();
if (rows.length > 0) {
req.authSource = 'webcontrol_db';
req.apiKeyData = rows[0];
return next();
}
} catch (e) {
console.warn('[API Auth] Error consultando webControl.api_key:', e.message);
}
return res.status(403).json({
ok: false,
success: false,
error: 'Credencial inválida o no autorizada para el servicio de Help Desk'
});
}
// Aplicar autenticación M2M a todas las rutas de /api
router.use(authenticateM2M);
/**
* Función auxiliar para resolver o crear el usuario técnico de integración
*/
async function getTechnicalUser(companyId) {
const email = `bot_sigma-robot@sigmac.app`;
let user = await User.findByEmail(email);
if (!user) {
const id = await User.create(
companyId,
'Bot Soporte (sigma-robot)',
email,
null,
'client',
'bot',
'sigma-robot-m2m'
);
user = await User.findById(id);
}
return user;
}
/**
* Función auxiliar para resolver la empresa por ID, slug o dominio de app_origen
*/
async function resolveCompany(empresa, app_origen, company_id) {
if (company_id) {
const c = await Company.findById(parseInt(company_id));
if (c) return c;
}
if (empresa) {
if (!isNaN(empresa)) {
const c = await Company.findById(parseInt(empresa));
if (c) return c;
} else {
const c = await Company.findBySlug(String(empresa).toLowerCase().trim());
if (c) return c;
}
}
if (app_origen) {
const originLower = String(app_origen).toLowerCase();
if (originLower.includes('domus')) {
const c = await Company.findBySlug('domus');
if (c) return c;
}
if (originLower.includes('xsystem') || originLower.includes('webcontrol')) {
const c = await Company.findBySlug('xsystem');
if (c) return c;
}
}
// Default a Telcotronics (ID 1)
return (await Company.findById(1)) || { id: 1, name: 'Telcotronics', slug: 'telcotronics' };
}
/**
* Función auxiliar para resolver departamento según tipo
*/
async function resolveDepartment(companyId, tipo, departamento) {
const depts = await Department.findAll(companyId);
if (depts.length === 0) return null;
if (departamento) {
const found = depts.find(d =>
d.nombre.toLowerCase().includes(String(departamento).toLowerCase())
);
if (found) return found.id;
}
const tipoLower = String(tipo || '').toLowerCase();
let preferredName = 'Soporte Técnico';
if (tipoLower === 'cobro' || tipoLower === 'facturacion') preferredName = 'Facturación';
if (tipoLower === 'consulta' || tipoLower === 'otro') preferredName = 'General';
const match = depts.find(d => d.nombre.toLowerCase().includes(preferredName.toLowerCase()));
return match ? match.id : depts[0].id;
}
/**
* GET /api/empresas — Lista de empresas del ecosistema para canalizar tickets
*/
router.get('/empresas', async (req, res) => {
try {
const companies = await Company.findAll();
const simplified = companies.map(c => ({
id: c.id,
name: c.name,
slug: c.slug,
plan: c.plan
}));
res.json({ ok: true, success: true, empresas: simplified });
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
});
/**
* POST /api/external/tickets y POST /api/tickets
* Crea un ticket M2M con soporte para idempotencia, solicitante y categorización
*/
async function handleCreateTicket(req, res) {
try {
const {
origen = 'sigma-robot',
origen_externo_id = null,
empresa,
company_id,
app_origen = null,
solicitante = {},
asunto,
mensaje,
descripcion,
prioridad = 'media',
tipo = 'consulta',
departamento,
department_id
} = req.body;
const ticketAsunto = asunto || (mensaje ? String(mensaje).substring(0, 60) + '...' : null);
const ticketMensaje = mensaje || descripcion;
if (!ticketAsunto || !ticketMensaje) {
return res.status(400).json({
ok: false,
success: false,
error: 'Los campos asunto y mensaje son obligatorios'
});
}
// 1. Resolver Empresa
const company = await resolveCompany(empresa, app_origen, company_id);
// 2. Idempotencia: Verificar si ya existe este origen_externo_id
if (origen_externo_id) {
const existing = await Ticket.findByOrigenExterno(company.id, origen, origen_externo_id);
if (existing) {
return res.status(200).json({
ok: true,
success: true,
duplicado: true,
ticket_id: existing.id,
estado: existing.status,
empresa: company.name,
url: `https://soporte.sigmac.app/agent/ticket/${existing.id}`
});
}
}
// 3. Resolver Usuario Técnico y Solicitante
const techUser = await getTechnicalUser(company.id);
const solNombre = solicitante.nombre || req.body.cliente_nombre || null;
const solEmail = solicitante.email || req.body.cliente_email || null;
const solTelefono = solicitante.telefono || req.body.cliente_telefono || null;
// 4. Resolver Departamento
const deptId = department_id ? parseInt(department_id) : await resolveDepartment(company.id, tipo, departamento);
// 5. Prioridad normalizada
const cleanPriority = ['baja', 'media', 'alta', 'critica'].includes(String(prioridad).toLowerCase())
? String(prioridad).toLowerCase()
: 'media';
// 6. Crear Ticket
const ticketId = await Ticket.create(
company.id,
techUser.id,
deptId,
ticketAsunto.trim(),
ticketMensaje.trim(),
cleanPriority,
{
origen,
origen_externo_id: origen_externo_id ? String(origen_externo_id) : null,
app_origen,
solicitante_nombre: solNombre,
solicitante_email: solEmail,
solicitante_telefono: solTelefono,
tipo
}
);
const url = `https://soporte.sigmac.app/agent/ticket/${ticketId}`;
return res.status(201).json({
ok: true,
success: true,
ticket_id: ticketId,
estado: 'abierto',
empresa: company.name,
company_id: company.id,
asunto: ticketAsunto.trim(),
prioridad: cleanPriority,
url
});
} catch (err) {
console.error('[API Tickets] Error:', err);
return res.status(500).json({ ok: false, success: false, error: err.message });
}
}
router.post('/external/tickets', handleCreateTicket);
router.post('/tickets', handleCreateTicket);
/**
* GET /api/external/tickets/:id y GET /api/tickets/:id
* Consulta el estado y última respuesta de un ticket
*/
async function handleGetTicket(req, res) {
try {
const ticketId = parseInt(req.params.id);
const ticket = await Ticket.findById(ticketId);
if (!ticket) {
return res.status(404).json({ ok: false, success: false, error: 'Ticket no encontrado' });
}
const respuestas = await Ticket.getResponses(ticketId);
const ultimaRespuesta = respuestas.length > 0 ? respuestas[respuestas.length - 1] : null;
res.json({
ok: true,
success: true,
ticket_id: ticket.id,
estado: ticket.status,
asunto: ticket.asunto,
prioridad: ticket.prioridad,
asignado_a: ticket.asignado_nombre || null,
actualizado: ticket.updated_at || ticket.created_at,
origen: ticket.origen,
solicitante: {
nombre: ticket.solicitante_nombre || ticket.usuario_nombre,
email: ticket.solicitante_email || ticket.usuario_email
},
ultima_respuesta: ultimaRespuesta ? {
autor: ultimaRespuesta.usuario_nombre,
mensaje: ultimaRespuesta.mensaje,
fecha: ultimaRespuesta.created_at
} : null,
url: `https://soporte.sigmac.app/agent/ticket/${ticket.id}`
});
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
}
router.get('/external/tickets/:id', handleGetTicket);
router.get('/tickets/:id', handleGetTicket);
/**
* POST /api/external/tickets/:id/notas y POST /api/tickets/:id/respuestas
* Agrega una nota o seguimiento al ticket
*/
async function handleAddNote(req, res) {
try {
const ticketId = parseInt(req.params.id);
const { texto, mensaje, autor = 'sigma-robot' } = req.body;
const contenido = texto || mensaje;
if (!contenido) {
return res.status(400).json({ ok: false, success: false, error: 'El campo texto es obligatorio' });
}
const ticket = await Ticket.findById(ticketId);
if (!ticket) {
return res.status(404).json({ ok: false, success: false, error: 'Ticket no encontrado' });
}
const techUser = await getTechnicalUser(ticket.company_id);
const notaCompleta = `[${autor}]: ${contenido}`;
const respId = await Ticket.addResponse(ticketId, techUser.id, notaCompleta);
res.json({
ok: true,
success: true,
message: 'Nota agregada exitosamente',
respuesta_id: respId,
ticket_id: ticketId
});
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
}
router.post('/external/tickets/:id/notas', handleAddNote);
router.post('/tickets/:id/respuestas', handleAddNote);
module.exports = router;