feat: version 2.17.0 -- Equipos: pestaña Usuarios (sesion, usuarios y grupos locales)
Migracion 027 (device_accounts). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
| `vault_entries` | repositorio de credenciales privado por usuario (menú "Contraseñas"), contraseña/notas cifradas |
|
| `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` |
|
| `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_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_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`) |
|
| `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) |
|
| `inventory_tokens` | tokens de ingesta del agente (solo hash sha256) |
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# 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
|
## [2.16.5] - 2026-10-20
|
||||||
### Changed
|
### Changed
|
||||||
- Equipos → listado: se quita el botón "Información" del final de cada fila; el detalle del equipo
|
- Equipos → listado: se quita el botón "Información" del final de cada fila; el detalle del equipo
|
||||||
|
|||||||
@@ -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
|
- 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
|
hardware y software alimentado por el **agente GLPI oficial** instalado en cada equipo (sin
|
||||||
servidor GLPI). Diseño en `docs/ARQUITECTURA-INVENTARIO.md`.
|
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,
|
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`).
|
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
|
- Edición manual solo de: estado (activo/de baja), usuario asignado, departamento y notas; el
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "2.16.5",
|
"version": "2.17.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { XMLParser } from 'fast-xml-parser';
|
|||||||
|
|
||||||
const XML_ARRAY_SECTIONS = new Set([
|
const XML_ARRAY_SECTIONS = new Set([
|
||||||
'cpus', 'memories', 'storages', 'drives', 'networks', 'monitors', 'softwares', 'printers',
|
'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 });
|
const xmlParser = new XMLParser({ ignoreAttributes: true, parseTagValue: false, trimValues: true });
|
||||||
@@ -144,6 +144,14 @@ export function normalizeInventory(parsed) {
|
|||||||
})
|
})
|
||||||
.filter((s) => s.name);
|
.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.
|
// Resumenes para listado/KPIs.
|
||||||
const hostname = str(hw.name || nd.name || printer.name || os.fqdn, 190);
|
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(':'));
|
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];
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const CHILD_TABLES = {
|
|||||||
volumes: { table: 'device_volumes', cols: ['mount', 'label', 'filesystem', 'type', 'total_mb', 'free_mb'] },
|
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'] },
|
networks: { table: 'device_networks', cols: ['description', 'mac', 'ip', 'mask', 'gateway', 'type', 'status'] },
|
||||||
monitors: { table: 'device_monitors', cols: ['manufacturer', 'model', 'serial'] },
|
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'] },
|
software: { table: 'device_software', cols: ['name', 'version', 'publisher', 'install_date', 'category', 'is_component'] },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.16.5",
|
"version": "2.17.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -210,6 +210,13 @@ export const messages = {
|
|||||||
eq_share_remove: 'Quitar acceso',
|
eq_share_remove: 'Quitar acceso',
|
||||||
eq_share_remove_title: 'Quitar acceso a Equipos',
|
eq_share_remove_title: 'Quitar acceso a Equipos',
|
||||||
eq_share_remove_msg: 'dejará de ver el menú 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_tab_summary: 'Resumen',
|
||||||
eq_show_components: 'Mostrar actualizaciones, drivers y componentes del sistema',
|
eq_show_components: 'Mostrar actualizaciones, drivers y componentes del sistema',
|
||||||
eq_tab_devices: 'Equipos',
|
eq_tab_devices: 'Equipos',
|
||||||
@@ -685,6 +692,13 @@ export const messages = {
|
|||||||
eq_share_remove: 'Remove access',
|
eq_share_remove: 'Remove access',
|
||||||
eq_share_remove_title: 'Remove access to Devices',
|
eq_share_remove_title: 'Remove access to Devices',
|
||||||
eq_share_remove_msg: 'will no longer see the Devices menu.',
|
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_tab_summary: 'Summary',
|
||||||
eq_show_components: 'Show updates, drivers and system components',
|
eq_show_components: 'Show updates, drivers and system components',
|
||||||
eq_tab_devices: 'Devices',
|
eq_tab_devices: 'Devices',
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ function DeviceDetail({ id, onClose, onChanged, canEdit }) {
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'general', label: t('eq_tab_general') },
|
{ 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: 'hardware', label: t('eq_tab_hardware') },
|
||||||
{ key: 'network', label: t('eq_tab_network'), count: device.networks.length },
|
{ 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 },
|
{ 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 }) {
|
|||||||
<SimpleTable empty={emptyText} rows={rows} columns={columns} />
|
<SimpleTable empty={emptyText} rows={rows} columns={columns} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
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 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 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]);
|
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 && <div className="text-muted">{t('common_search_no_results')}</div>}
|
||||||
|
<div className="text-muted" style={{ fontSize: '.72rem', marginTop: 12 }}>{t('eq_users_hint')}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{tab === 'hardware' && (
|
{tab === 'hardware' && (
|
||||||
<>
|
<>
|
||||||
{showRows(hwRows)}
|
{showRows(hwRows)}
|
||||||
|
|||||||
Reference in New Issue
Block a user