feat: version 4.15.0 -- Equipos: pestaña Licenciamiento (Windows y Office)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-30 11:47:57 -05:00
parent b2c25c4fe5
commit 34183d15ce
8 changed files with 125 additions and 3 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ async function saveRaw(deviceid, text, format) {
return name;
}
function parseInWorker(text, format) {
export function parseInWorker(text, format) {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL('./parseWorker.js', import.meta.url), {
workerData: { text, format }, resourceLimits: { maxOldGenerationSizeMb: 256 },
+34 -1
View File
@@ -7,7 +7,7 @@ import { XMLParser } from 'fast-xml-parser';
const XML_ARRAY_SECTIONS = new Set([
'cpus', 'memories', 'storages', 'drives', 'networks', 'monitors', 'softwares', 'printers',
'users', 'local_users', 'local_groups', 'videos', 'sounds', 'batteries', 'controllers', 'ports', 'slots', 'antivirus',
'users', 'local_users', 'local_groups', 'licenseinfos', 'videos', 'sounds', 'batteries', 'controllers', 'ports', 'slots', 'antivirus',
]);
const xmlParser = new XMLParser({ ignoreAttributes: true, parseTagValue: false, trimValues: true });
@@ -74,6 +74,38 @@ function isComponent(name, category) {
return COMPONENT_CATEGORIES.has((category || '').toLowerCase()) || COMPONENT_NAME.test(name || '');
}
// Licenciamiento de Windows y Office. La clave de producto NUNCA se guarda completa en la BD: solo
// el ultimo bloque (la completa sigue solo en el reporte crudo, de descarga restringida).
const maskKey = (v) => {
const key = str(v, 64);
if (!key) return null;
return key.replace(/[A-Z0-9]/gi, '*').slice(0, -5) + key.slice(-5);
};
export function extractLicenses(c) {
const hw = c.hardware || {};
const os = c.operatingsystem || {};
const winPid = str(hw.winprodid, 64);
const isWindows = /windows/i.test(`${os.name || ''} ${os.full_name || ''} ${hw.osname || ''}`);
const windows = isWindows ? {
edition: str(os.full_name ?? hw.osname),
version: str(os.version ?? hw.osversion, 120),
product_id: winPid,
key: maskKey(hw.winprodkey),
oem: winPid ? /OEM/i.test(winPid) : null,
owner: str(hw.winowner, 190),
company: str(hw.wincompany, 190),
} : null;
const office = asArray(c.licenseinfos)
.filter((x) => /office/i.test(`${x.name || ''} ${x.fullname || ''}`))
.map((x) => ({
name: str(x.name), full_name: str(x.fullname), product_id: str(x.productid, 64), key: maskKey(x.key),
oem: x.oem == null || x.oem === '' ? null : String(x.oem) === '1',
msdn: /msdn/i.test(`${x.name || ''} ${x.fullname || ''}`),
}));
return { windows, office };
}
const LAPTOP_CHASSIS = /laptop|notebook|portable|hand held|sub notebook|convertible|tablet|detachable|netbook/i;
const SERVER_CHASSIS = /server|rack|blade|main server|multi-system/i;
@@ -197,6 +229,7 @@ export function normalizeInventory(parsed) {
for (const key of ['printers', 'network_device', 'antivirus', 'videos', 'batteries', 'sounds']) {
if (c[key] && (!Array.isArray(c[key]) || c[key].length)) extra[key] = c[key];
}
extra.licenses = extractLicenses(c);
return { device, cpus, memories, disks, volumes, networks, monitors, software, accounts, extra };
}
@@ -229,6 +229,8 @@ export async function updateManual(id, { status, assignedUserId, departmentId, n
export const saveCpuYear = (id, year, cpuSummary) => pool.query('UPDATE devices SET cpu_year = ?, cpu_year_for = ? WHERE id = ?', [year, cpuSummary, id]);
export const saveExtra = (id, extra) => pool.query('UPDATE devices SET extra_json = ? WHERE id = ?', [JSON.stringify(extra), id]);
export async function findRawFile(id) {
const [[row]] = await pool.query('SELECT raw_file FROM devices WHERE id = ?', [id]);
return row?.raw_file || null;
+16
View File
@@ -8,6 +8,7 @@ import { logAudit } from '../../utils/audit.js';
import { encrypt, decrypt } from '../../utils/crypto.js';
import { askStructured } from '../../utils/ai.js';
import * as repo from './repository.js';
import { parseInWorker } from './ingest.js';
import { CAN_EDIT_DEVICES, CAN_MANAGE_DEVICES } from '../../middleware/auth.js';
const STATUSES = ['active', 'retired'];
@@ -24,9 +25,24 @@ export const listDevices = (filters) => repo.findAll({
export async function getDevice(id) {
const device = await repo.findById(id);
if (!device) throw new HttpError(404, 'Equipo no encontrado');
if (device.extra?.licenses === undefined && device.raw_file) await backfillLicenses(device);
return device;
}
// Equipos inventariados antes de la pestaña Licenciamiento: se sacan del ultimo reporte crudo una
// sola vez (si falla queda vacio y se completa con el proximo reporte del agente).
async function backfillLicenses(device) {
let licenses = { windows: null, office: [] };
try {
const full = path.join(env.storageRoot, 'inventories', path.basename(device.raw_file));
const text = await fs.promises.readFile(full, 'utf8');
const parsed = await parseInWorker(text, path.extname(full) === '.json' ? 'json' : 'xml');
licenses = parsed.normalized?.extra?.licenses || licenses;
} catch { /* sin crudo legible: queda vacio */ }
device.extra = { ...(device.extra || {}), licenses };
await repo.saveExtra(device.id, device.extra);
}
export async function updateDevice(id, data, actor) {
const current = await getDevice(id);
const status = data.status ?? current.status;