feat: version 2.15.0 -- instalador del agente GLPI, filtro de componentes y token recuperable

Instalador .bat generado en el navegador, casilla para ocultar actualizaciones/drivers/componentes
en Software, buscador por pestaña en el detalle, GUIDs como componentes y tokens de ingesta
cifrados (migraciones 024 y 025).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-20 08:17:53 -05:00
parent e25c2c3b35
commit d5f09dc91b
15 changed files with 151 additions and 12 deletions
@@ -0,0 +1,4 @@
-- Token de ingesta recuperable (cifrado AES-256-GCM con ENCRYPTION_KEY, igual que la clave SMTP)
-- para poder generar el instalador del agente sin volver a pegar el token. token_hash sigue siendo
-- lo que valida la ingesta. Tokens creados antes de esta migracion quedan con token_enc NULL.
ALTER TABLE inventory_tokens ADD COLUMN IF NOT EXISTS token_enc TEXT NULL;
@@ -15,5 +15,6 @@ export const getRaw = asyncHandler(async (req, res) => {
export const getTokens = asyncHandler(async (req, res) => ok(res, await service.listTokens()));
export const postToken = asyncHandler(async (req, res) => ok(res, await service.createToken(req.body.label, req.currentUser), 201));
export const getTokenSecret = asyncHandler(async (req, res) => ok(res, await service.revealToken(Number(req.params.id), req.currentUser)));
export const patchToken = asyncHandler(async (req, res) => ok(res, await service.setTokenActive(Number(req.params.id), !!req.body.isActive, req.currentUser)));
export const deleteToken = asyncHandler(async (req, res) => ok(res, await service.removeToken(Number(req.params.id), req.currentUser)));
+1 -1
View File
@@ -68,7 +68,7 @@ const COMPONENT_NAME = new RegExp([
'windowsappruntime', 'ui xaml', 'advertising sdk', 'vclibs', 'driver', 'controlador', 'firmware', 'chipset',
'management engine', 'rapid storage', 'serial io', 'hardware accelerated', 'compatibility fix', 'subsystem for linux',
'webview2', 'update health tools', 'pc health', 'comprobaci[oó]n de estado', 'desinstalar impresora', 'printer uninstall',
'^microsoftwindows\\.', '^[0-9a-f]{8}-[0-9a-f]{4}-', '^windows sdk', 'software development kit.*(windows|arm|x64|x86)',
'^microsoftwindows\\.', '^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-', '^windows sdk', 'software development kit.*(windows|arm|x64|x86)',
].join('|'), 'i');
function isComponent(name, category) {
return COMPONENT_CATEGORIES.has((category || '').toLowerCase()) || COMPONENT_NAME.test(name || '');
+9 -4
View File
@@ -27,17 +27,22 @@ export const touchToken = (id) => pool.query('UPDATE inventory_tokens SET last_u
export async function listTokens() {
const [rows] = await pool.query(
`SELECT t.id, t.label, t.is_active, t.last_used_at, t.created_at, u.name AS created_by_name
`SELECT t.id, t.label, t.is_active, t.last_used_at, t.created_at, u.name AS created_by_name, (t.token_enc IS NOT NULL) AS has_secret
FROM inventory_tokens t LEFT JOIN users u ON u.id = t.created_by ORDER BY t.id DESC`
);
return rows;
return rows.map((r) => ({ ...r, has_secret: !!r.has_secret }));
}
export async function createToken(label, hash, userId) {
const [r] = await pool.query('INSERT INTO inventory_tokens (label, token_hash, created_by) VALUES (?, ?, ?)', [label, hash, userId]);
export async function createToken(label, hash, enc, userId) {
const [r] = await pool.query('INSERT INTO inventory_tokens (label, token_hash, token_enc, created_by) VALUES (?, ?, ?, ?)', [label, hash, enc, userId]);
return r.insertId;
}
export async function findTokenSecret(id) {
const [[row]] = await pool.query('SELECT token_enc FROM inventory_tokens WHERE id = ?', [id]);
return row?.token_enc || null;
}
export const setTokenActive = (id, active) => pool.query('UPDATE inventory_tokens SET is_active = ? WHERE id = ?', [active ? 1 : 0, id]);
export const deleteToken = (id) => pool.query('DELETE FROM inventory_tokens WHERE id = ?', [id]);
+2 -1
View File
@@ -2,7 +2,7 @@ import { Router } from 'express';
import { requireAuth, requireRole, ROLES } from '../../middleware/auth.js';
import {
getDevices, getDevice, putDevice, getDashboard, getSoftware, getSoftwareDevices, getRaw,
getTokens, postToken, patchToken, deleteToken,
getTokens, postToken, patchToken, deleteToken, getTokenSecret,
} from './controller.js';
const router = Router();
@@ -17,6 +17,7 @@ router.get('/software', getSoftware);
router.get('/software/devices', getSoftwareDevices);
router.get('/tokens', requireRole(...CAN_MANAGE_TOKENS), getTokens);
router.post('/tokens', requireRole(...CAN_MANAGE_TOKENS), postToken);
router.get('/tokens/:id/secret', requireRole(...CAN_MANAGE_TOKENS), getTokenSecret);
router.patch('/tokens/:id', requireRole(...CAN_MANAGE_TOKENS), patchToken);
router.delete('/tokens/:id', requireRole(...CAN_MANAGE_TOKENS), deleteToken);
router.get('/', getDevices);
+10 -1
View File
@@ -4,6 +4,7 @@ import path from 'node:path';
import { env } from '../../config/env.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
import { encrypt, decrypt } from '../../utils/crypto.js';
import * as repo from './repository.js';
const STATUSES = ['active', 'retired'];
@@ -57,11 +58,19 @@ export async function createToken(label, actor) {
const clean = String(label || '').trim();
if (!clean) throw new HttpError(400, 'La etiqueta es obligatoria');
const token = crypto.randomBytes(24).toString('hex');
const id = await repo.createToken(clean.slice(0, 120), crypto.createHash('sha256').update(token).digest('hex'), actor.id);
const id = await repo.createToken(clean.slice(0, 120), crypto.createHash('sha256').update(token).digest('hex'), encrypt(token), actor.id);
await logAudit({ userId: actor.id, action: 'create', entityType: 'inventory_token', entityId: id, detail: { label: clean } });
return { id, label: clean, token };
}
// Devuelve el token en claro a admin/superadmin para armar el instalador. Queda en auditoria.
export async function revealToken(id, actor) {
const enc = await repo.findTokenSecret(id);
if (!enc) throw new HttpError(404, 'Este token se creo antes de poder recuperarse: genera uno nuevo');
await logAudit({ userId: actor.id, action: 'reveal', entityType: 'inventory_token', entityId: id });
return { token: decrypt(enc) };
}
export async function setTokenActive(id, active, actor) {
await repo.setTokenActive(id, active);
await logAudit({ userId: actor.id, action: active ? 'enable' : 'disable', entityType: 'inventory_token', entityId: id });