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:
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user