From 30bb546b8454df12decad74e95be839b16ddf5b2 Mon Sep 17 00:00:00 2001 From: Milton Gonzalez Date: Tue, 20 Oct 2026 11:50:44 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20version=202.17.0=20--=20Equipos:=20pest?= =?UTF-8?q?a=C3=B1a=20Usuarios=20(sesion,=20usuarios=20y=20grupos=20locale?= =?UTF-8?q?s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migracion 027 (device_accounts). Co-Authored-By: Claude Sonnet 5.5 --- BASE_DE_DATOS.md | 1 + CHANGELOG.md | 7 ++++++ SPEC.md | 2 +- VERSION | 2 +- .../migrations/027_device_accounts.sql | 12 +++++++++ backend/package.json | 2 +- backend/src/modules/devices/normalizer.js | 12 +++++++-- backend/src/modules/devices/repository.js | 1 + frontend/package.json | 2 +- frontend/src/i18n/messages.js | 14 +++++++++++ frontend/src/pages/Equipos/EquiposPage.jsx | 25 +++++++++++++++++++ 11 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 backend/database/migrations/027_device_accounts.sql diff --git a/BASE_DE_DATOS.md b/BASE_DE_DATOS.md index ea89b42..28951c3 100644 --- a/BASE_DE_DATOS.md +++ b/BASE_DE_DATOS.md @@ -44,6 +44,7 @@ | `vault_entries` | repositorio de credenciales privado por usuario (menú "Contraseñas"), contraseña/notas cifradas | | `devices` | inventario de equipos (menú "Equipos"), upsert por `deviceid` del agente GLPI; baja lógica con `status` | | `device_cpus` / `device_memories` / `device_disks` / `device_volumes` / `device_networks` / `device_monitors` / `device_software` | componentes de cada equipo, reemplazados completos en cada reporte | +| `device_accounts` | cuentas que reporta el agente: sesión iniciada, usuarios locales y grupos locales (`kind`), reemplazadas en cada reporte (migración `027`) | | `device_changes` | diff entre reportes (RAM, discos, software instalado/desinstalado) | | `device_viewers` | usuarios con acceso de solo lectura compartido al menú Equipos (`user_id`, `granted_by`) | | `inventory_tokens` | tokens de ingesta del agente (solo hash sha256) | diff --git a/CHANGELOG.md b/CHANGELOG.md index f307635..0125632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog - project.linkedcorp.net +## [2.17.0] - 2026-10-20 +### Added +- Equipos → detalle: pestaña **Usuarios** (después de General) con el usuario que tiene sesión + iniciada, el total y la lista de usuarios locales de Windows (con SID) y los grupos locales. + Solo cuentas locales del equipo (no dominio ni Microsoft 365). Migración `027` + (`device_accounts`); los equipos la llenan en su siguiente reporte. + ## [2.16.5] - 2026-10-20 ### Changed - Equipos → listado: se quita el botón "Información" del final de cada fila; el detalle del equipo diff --git a/SPEC.md b/SPEC.md index 6056776..37b3433 100644 --- a/SPEC.md +++ b/SPEC.md @@ -81,7 +81,7 @@ tiene cada cosa — sin que cargar la información sea tedioso para quien lo usa - Menú "Equipos" visible para `superadmin`, `admin` y `it_manager`. Inventario automático de hardware y software alimentado por el **agente GLPI oficial** instalado en cada equipo (sin servidor GLPI). Diseño en `docs/ARQUITECTURA-INVENTARIO.md`. -- Pestañas: Equipos (KPIs + listado + detalle General/Hardware/Red/Software/Historial), +- Pestañas: Equipos (KPIs + listado + detalle General/Usuarios/Hardware/Red/Software/Historial), Software (transversal: en cuántos equipos está instalado) y Agentes (tokens de ingesta, solo `superadmin`/`admin`) y Compartir (da acceso de solo lectura a otros usuarios, `superadmin`/`admin`). - Edición manual solo de: estado (activo/de baja), usuario asignado, departamento y notas; el diff --git a/VERSION b/VERSION index ffa5fee..d76bd2b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.16.5 +2.17.0 diff --git a/backend/database/migrations/027_device_accounts.sql b/backend/database/migrations/027_device_accounts.sql new file mode 100644 index 0000000..b4cb461 --- /dev/null +++ b/backend/database/migrations/027_device_accounts.sql @@ -0,0 +1,12 @@ +-- Cuentas del equipo que reporta el agente: usuarios con sesion iniciada (USERS), usuarios locales +-- (LOCAL_USERS) y grupos locales (LOCAL_GROUPS). Se reemplazan completas en cada reporte. Idempotente. +CREATE TABLE IF NOT EXISTS device_accounts ( + id INT AUTO_INCREMENT PRIMARY KEY, + device_id INT NOT NULL, + kind ENUM('session','local_user','local_group') NOT NULL, + name VARCHAR(190) NOT NULL, + domain VARCHAR(190) NULL, + sid VARCHAR(190) NULL, + KEY idx_device_accounts_device (device_id, kind), + CONSTRAINT fk_device_accounts_device FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/package.json b/backend/package.json index c895dee..b27a535 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "project-linkedcorp-backend", - "version": "2.16.5", + "version": "2.17.0", "private": true, "type": "module", "main": "src/server.js", diff --git a/backend/src/modules/devices/normalizer.js b/backend/src/modules/devices/normalizer.js index 4c448d9..024d1bf 100644 --- a/backend/src/modules/devices/normalizer.js +++ b/backend/src/modules/devices/normalizer.js @@ -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', 'videos', 'sounds', 'batteries', 'controllers', 'ports', 'slots', 'antivirus', + 'users', 'local_users', 'local_groups', 'videos', 'sounds', 'batteries', 'controllers', 'ports', 'slots', 'antivirus', ]); const xmlParser = new XMLParser({ ignoreAttributes: true, parseTagValue: false, trimValues: true }); @@ -144,6 +144,14 @@ export function normalizeInventory(parsed) { }) .filter((s) => s.name); + // Cuentas: sesion iniciada (USERS), usuarios locales y grupos locales (solo cuentas LOCALES del equipo). + const account = (kind, name, domain, sid) => ({ kind, name: str(name, 190), domain: str(domain, 190), sid: str(sid, 190) }); + const accounts = [ + ...asArray(c.users).map((x) => account('session', x.login, x.domain, null)), + ...asArray(c.local_users).map((x) => account('local_user', x.name, null, x.id)), + ...asArray(c.local_groups).map((x) => account('local_group', x.name, null, x.id)), + ].filter((a) => a.name); + // Resumenes para listado/KPIs. const hostname = str(hw.name || nd.name || printer.name || os.fqdn, 190); const upIfaces = networks.filter((n) => n.ip && !/^(127\.|169\.254\.|::1|fe80)/i.test(n.ip) && !n.ip.includes(':')); @@ -190,5 +198,5 @@ export function normalizeInventory(parsed) { if (c[key] && (!Array.isArray(c[key]) || c[key].length)) extra[key] = c[key]; } - return { device, cpus, memories, disks, volumes, networks, monitors, software, extra }; + return { device, cpus, memories, disks, volumes, networks, monitors, software, accounts, extra }; } diff --git a/backend/src/modules/devices/repository.js b/backend/src/modules/devices/repository.js index 422dec8..2bb0377 100644 --- a/backend/src/modules/devices/repository.js +++ b/backend/src/modules/devices/repository.js @@ -13,6 +13,7 @@ const CHILD_TABLES = { volumes: { table: 'device_volumes', cols: ['mount', 'label', 'filesystem', 'type', 'total_mb', 'free_mb'] }, networks: { table: 'device_networks', cols: ['description', 'mac', 'ip', 'mask', 'gateway', 'type', 'status'] }, monitors: { table: 'device_monitors', cols: ['manufacturer', 'model', 'serial'] }, + accounts: { table: 'device_accounts', cols: ['kind', 'name', 'domain', 'sid'] }, software: { table: 'device_software', cols: ['name', 'version', 'publisher', 'install_date', 'category', 'is_component'] }, }; diff --git a/frontend/package.json b/frontend/package.json index bdc574f..a6a17f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "project-linkedcorp-frontend", "private": true, - "version": "2.16.5", + "version": "2.17.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js index c986f3c..3ae969c 100644 --- a/frontend/src/i18n/messages.js +++ b/frontend/src/i18n/messages.js @@ -210,6 +210,13 @@ export const messages = { eq_share_remove: 'Quitar acceso', eq_share_remove_title: 'Quitar acceso a Equipos', eq_share_remove_msg: 'dejará de ver el menú Equipos.', + eq_tab_users: 'Usuarios', + eq_users_session: 'Con sesión iniciada', + eq_users_local_count: 'Usuarios locales (total)', + eq_groups_local_count: 'Grupos locales (total)', + eq_users_local: 'Usuarios locales de Windows', + eq_groups_local: 'Grupos locales', + eq_users_hint: 'Solo cuentas locales del equipo: no incluye usuarios de dominio ni de Microsoft 365, e incluye cuentas de sistema o de programas. Los grupos no indican a qué usuario pertenecen.', eq_tab_summary: 'Resumen', eq_show_components: 'Mostrar actualizaciones, drivers y componentes del sistema', eq_tab_devices: 'Equipos', @@ -685,6 +692,13 @@ export const messages = { eq_share_remove: 'Remove access', eq_share_remove_title: 'Remove access to Devices', eq_share_remove_msg: 'will no longer see the Devices menu.', + eq_tab_users: 'Users', + eq_users_session: 'Signed in', + eq_users_local_count: 'Local users (total)', + eq_groups_local_count: 'Local groups (total)', + eq_users_local: 'Windows local users', + eq_groups_local: 'Local groups', + eq_users_hint: 'Local accounts of the machine only: domain and Microsoft 365 users are not included, and system or application accounts are. Groups do not show which user belongs to them.', eq_tab_summary: 'Summary', eq_show_components: 'Show updates, drivers and system components', eq_tab_devices: 'Devices', diff --git a/frontend/src/pages/Equipos/EquiposPage.jsx b/frontend/src/pages/Equipos/EquiposPage.jsx index 52dadca..0070a9f 100644 --- a/frontend/src/pages/Equipos/EquiposPage.jsx +++ b/frontend/src/pages/Equipos/EquiposPage.jsx @@ -103,6 +103,7 @@ function DeviceDetail({ id, onClose, onChanged, canEdit }) { const tabs = [ { key: 'general', label: t('eq_tab_general') }, + { key: 'users', label: t('eq_tab_users'), count: device.accounts.filter((a) => a.kind === 'local_user').length }, { key: 'hardware', label: t('eq_tab_hardware') }, { key: 'network', label: t('eq_tab_network'), count: device.networks.length }, { key: 'software', label: t('eq_tab_software'), count: device.software.filter((r) => showComponents || !r.is_component).length }, @@ -152,6 +153,16 @@ function DeviceDetail({ id, onClose, onChanged, canEdit }) { ); + const sessionNames = device.accounts.filter((a) => a.kind === 'session').map((a) => (a.domain ? `${a.domain}\\${a.name}` : a.name)); + const allLocalUsers = device.accounts.filter((a) => a.kind === 'local_user'); + const allLocalGroups = device.accounts.filter((a) => a.kind === 'local_group'); + const userRows = [ + [t('eq_users_session'), val(sessionNames.join(', ')), sessionNames.join(' ')], + [t('eq_users_local_count'), String(allLocalUsers.length), String(allLocalUsers.length)], + [t('eq_groups_local_count'), String(allLocalGroups.length), String(allLocalGroups.length)], + ]; + const localUsers = only(allLocalUsers, (r) => [r.name, r.sid]); + const localGroups = only(allLocalGroups, (r) => [r.name, r.sid]); const networks = only(device.networks, (r) => [r.description, r.ip, r.mask, r.gateway, r.mac, r.status]); const software = only(device.software.filter((r) => showComponents || !r.is_component), (r) => [r.name, r.version, r.publisher]); const changes = only(device.changes, (r) => [fmtDate(r.created_at), t(`eq_change_${r.change_type}`), r.field, r.old_value, r.new_value]); @@ -208,6 +219,20 @@ function DeviceDetail({ id, onClose, onChanged, canEdit }) { )} + {tab === 'users' && ( + <> + {showRows(userRows)} + {hwSection(t('eq_users_local'), localUsers, [ + { key: 'name', label: t('common_name') }, { key: 'sid', label: 'SID' }, + ])} + {hwSection(t('eq_groups_local'), localGroups, [ + { key: 'name', label: t('common_name') }, { key: 'sid', label: 'SID' }, + ])} + {q && showRows(userRows).length + localUsers.length + localGroups.length === 0 &&
{t('common_search_no_results')}
} +
{t('eq_users_hint')}
+ + )} + {tab === 'hardware' && ( <> {showRows(hwRows)}