From 4a305df73ecff12f28686d34ca08501b4a23b430 Mon Sep 17 00:00:00 2001 From: Milton Gonzalez Date: Tue, 20 Oct 2026 08:52:46 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20version=202.16.0=20--=20contador=20de?= =?UTF-8?q?=20cierre=20en=20Contrase=C3=B1as,=20cierre=20por=20inactividad?= =?UTF-8?q?=20y=20ajustes=20de=20Equipos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contraseñas: el cierre automatico ahora es por inactividad (no saca al usuario a medio escribir), token de desbloqueo renovable y contador visible junto al titulo. Equipos: GUID como componente. Sube la version para que el panel de Actualizaciones ofrezca los cambios 2.15.0 -> 2.16.0 (migraciones 025 y 026, instalador del agente y pestaña Compartir). Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 31 +++++++++---- VERSION | 2 +- backend/package.json | 2 +- backend/src/app.js | 2 +- backend/src/modules/vault/middleware.js | 3 ++ frontend/package.json | 2 +- frontend/src/components/AutoLockCountdown.jsx | 43 +++++++++++++++++++ frontend/src/i18n/messages.js | 2 + .../src/pages/Contrasenas/ContrasenasPage.jsx | 20 ++++----- frontend/src/services/api.js | 4 ++ 10 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/AutoLockCountdown.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9cd98..a835fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,7 @@ # Changelog - project.linkedcorp.net -## [2.15.0] - 2026-10-20 +## [2.16.0] - 2026-10-20 ### Added -- Menú **Equipos**: inventario de TI alimentado por el agente GLPI oficial (sin servidor GLPI). - Ingesta `POST /api/inventory` (JSON/XML, gzip/zlib) con token de instalación, upsert por - `deviceid`, historial de cambios, KPIs, software transversal y pestaña Agentes para generar - tokens. Edición manual de estado, usuario asignado, departamento y notas. -- Migración `023_devices.sql` (devices + componentes, device_changes, inventory_tokens). -- Equipos → Software (general y por equipo): casilla "Mostrar actualizaciones, drivers y componentes - del sistema", desmarcada por defecto (solo aplicaciones). Migración `024` agrega `category` e - `is_component` a `device_software`; se decide por `system_category` del agente y por nombre. - Equipos → Agentes: "Instalador para Windows": descarga un `.bat` (generado en el navegador, con servidor y token ya puestos) que baja el MSI oficial del agente GLPI 1.21 y lo instala en silencio con `SERVER/USER/PASSWORD/TAG`. Si el .msi está junto al .bat, no descarga. @@ -19,6 +11,27 @@ - Equipos → Compartir (solo admin/superadmin): da acceso de **solo lectura** (Resumen, Equipos y Software) a usuarios que por su rol no ven el menú. Migración `026` (`device_viewers`). Sin editar, sin Agentes, sin inventario crudo. El menú Equipos aparece solo a quien tenga acceso. +- Contraseñas: contador visible junto al título con el tiempo que falta para el cierre automático + (se pone amarillo en los últimos 30 s y se reinicia con cada acción). +### Fixed +- Contraseñas: ya no te saca del menú en medio de escribir/editar una credencial (se perdía lo no + guardado). El cierre automático ahora es por **inactividad**: 3 minutos sin teclado, clic, toque + o rueda; cada acción reinicia el contador. El token de desbloqueo del servidor ahora se renueva + en cada request válido (antes vencía a los 30 min aunque estuvieras trabajando). Salir a otro + menú o cerrar la pestaña sigue bloqueando igual. +- Equipos → Software: las entradas con nombre de código `{GUID}` (instaladores sin nombre) ahora + cuentan como componentes y quedan ocultas por defecto. + +## [2.15.0] - 2026-10-19 +### Added +- Menú **Equipos**: inventario de TI alimentado por el agente GLPI oficial (sin servidor GLPI). + Ingesta `POST /api/inventory` (JSON/XML, gzip/zlib) con token de instalación, upsert por + `deviceid`, historial de cambios, KPIs, software transversal y pestaña Agentes para generar + tokens. Edición manual de estado, usuario asignado, departamento y notas. +- Migración `023_devices.sql` (devices + componentes, device_changes, inventory_tokens). +- Equipos → Software (general y por equipo): casilla "Mostrar actualizaciones, drivers y componentes + del sistema", desmarcada por defecto (solo aplicaciones). Migración `024` agrega `category` e + `is_component` a `device_software`; se decide por `system_category` del agente y por nombre. - Dependencia backend: `fast-xml-parser`. Diseño traído de linkedtrack (que sigue aparte) en `docs/ARQUITECTURA-INVENTARIO.md`. diff --git a/VERSION b/VERSION index 68e69e4..7524906 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.15.0 +2.16.0 diff --git a/backend/package.json b/backend/package.json index 18afab1..f419227 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "project-linkedcorp-backend", - "version": "2.15.0", + "version": "2.16.0", "private": true, "type": "module", "main": "src/server.js", diff --git a/backend/src/app.js b/backend/src/app.js index a764eee..2f9819f 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -35,7 +35,7 @@ export const app = express(); app.set('trust proxy', 1); app.use(helmet({ contentSecurityPolicy: false })); -app.use(cors({ origin: env.corsOrigin, credentials: true })); +app.use(cors({ origin: env.corsOrigin, credentials: true, exposedHeaders: ['X-Vault-Token'] })); // Ingesta del agente GLPI: ANTES del parser JSON (usa cuerpo crudo, sin CORS: la llama el agente). app.use(inventoryIngestRoutes); app.use(express.json({ limit: '2mb' })); diff --git a/backend/src/modules/vault/middleware.js b/backend/src/modules/vault/middleware.js index 27e5051..013cd0d 100644 --- a/backend/src/modules/vault/middleware.js +++ b/backend/src/modules/vault/middleware.js @@ -17,5 +17,8 @@ export function requireVaultUnlock(req, res, next) { if (payload.scope !== 'vault' || payload.sub !== req.currentUser.id) { return fail(res, 403, 'Repositorio bloqueado -- ingresa tu PIN'); } + // Token deslizante: mientras el usuario siga trabajando, cada request valido renueva la vigencia + // (el cliente guarda el nuevo). El cierre por inactividad lo decide el frontend (3 min sin actividad). + res.set('X-Vault-Token', jwt.sign({ sub: payload.sub, scope: 'vault' }, env.jwtSecret, { expiresIn: '30m' })); next(); } diff --git a/frontend/package.json b/frontend/package.json index 2627b10..c0910f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "project-linkedcorp-frontend", "private": true, - "version": "2.15.0", + "version": "2.16.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/AutoLockCountdown.jsx b/frontend/src/components/AutoLockCountdown.jsx new file mode 100644 index 0000000..9e72be9 --- /dev/null +++ b/frontend/src/components/AutoLockCountdown.jsx @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState } from 'react'; +import { Timer } from 'lucide-react'; +import { Badge } from './Badge.jsx'; + +// Cuenta regresiva de cierre por INACTIVIDAD: cada tecla/clic/toque/rueda la reinicia y, al llegar +// a 0, llama onExpire (ej. salir del menu). Aislado en su propio componente para que el tic de cada +// segundo no vuelva a renderizar la pantalla que lo contiene. +const ACTIVITY_EVENTS = ['keydown', 'input', 'mousedown', 'touchstart', 'wheel']; + +export function AutoLockCountdown({ ms, onExpire, title }) { + const deadline = useRef(Date.now() + ms); + const expired = useRef(false); + const onExpireRef = useRef(onExpire); + onExpireRef.current = onExpire; + const [left, setLeft] = useState(ms); + + useEffect(() => { + const reset = () => { deadline.current = Date.now() + ms; }; + ACTIVITY_EVENTS.forEach((e) => window.addEventListener(e, reset, true)); + const tick = setInterval(() => { + const remaining = deadline.current - Date.now(); + setLeft(Math.max(0, remaining)); + if (remaining <= 0 && !expired.current) { + expired.current = true; + onExpireRef.current(); + } + }, 1000); + return () => { + clearInterval(tick); + ACTIVITY_EVENTS.forEach((e) => window.removeEventListener(e, reset, true)); + }; + }, [ms]); + + const secs = Math.ceil(left / 1000); + const label = `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`; + return ( + + + {label} + + + ); +} diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js index 4ba065a..4a87c4d 100644 --- a/frontend/src/i18n/messages.js +++ b/frontend/src/i18n/messages.js @@ -331,6 +331,7 @@ export const messages = { vault_copy: 'Copiar', vault_copied: 'Copiado al portapapeles', vault_generate: 'Generar', + vault_countdown_hint: 'Tiempo para el cierre automático por inactividad. Se reinicia con cada tecla o clic.', vault_auto_lock_notice: 'Saliste de Contraseñas por seguridad — ingresa tu PIN de nuevo para volver a entrar.', vault_group_new: 'Nuevo grupo', vault_group_name: 'Nombre del grupo', @@ -806,6 +807,7 @@ export const messages = { vault_copy: 'Copy', vault_copied: 'Copied to clipboard', vault_generate: 'Generate', + vault_countdown_hint: 'Time until automatic close for inactivity. It resets with every key press or click.', vault_auto_lock_notice: 'You left Passwords for security — enter your PIN again to come back in.', vault_group_new: 'New group', vault_group_name: 'Group name', diff --git a/frontend/src/pages/Contrasenas/ContrasenasPage.jsx b/frontend/src/pages/Contrasenas/ContrasenasPage.jsx index 423b9e2..38f6f0e 100644 --- a/frontend/src/pages/Contrasenas/ContrasenasPage.jsx +++ b/frontend/src/pages/Contrasenas/ContrasenasPage.jsx @@ -22,11 +22,12 @@ import { DetailPopup } from '../../components/DetailPopup.jsx'; import { Flyout, FlyoutItem } from '../../components/Flyout.jsx'; import { SearchInput } from '../../components/SearchInput.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; +import { AutoLockCountdown } from '../../components/AutoLockCountdown.jsx'; import { BRAND_ICONS } from '../../data/brandIcons.js'; const EMPTY_FORM = { title: '', url: '', username: '', password: '', notes: '', iconKey: '', groupId: '' }; const EMPTY_GROUP_FORM = { name: '', iconKey: '' }; -const AUTO_LOCK_MS = 3 * 60 * 1000; // 3 minutos: salir solo del menu si queda abierto sin atencion +const AUTO_LOCK_MS = 3 * 60 * 1000; // 3 minutos SIN actividad (teclado/mouse/toque): sale solo del menu // Catalogo fijo de iconos genericos (debe coincidir con backend/src/modules/vault/service.js#ICON_KEYS) -- // a proposito NO se usa ningun servicio externo de favicon (ej. google/s2/favicons): revelaria a @@ -150,15 +151,13 @@ export default function ContrasenasPage() { // PIN de nuevo, sin importar cuanto haya durado la sesion del sistema. useEffect(() => () => setVaultToken(null), []); - // Si queda abierto sin atencion, sale solo al Panel a los 3 minutos de haberse desbloqueado. - useEffect(() => { - if (locked) return; - const timer = setTimeout(() => { - pushToast({ tone: 'warning', message: t('vault_auto_lock_notice') }); - navigate('/'); - }, AUTO_LOCK_MS); - return () => clearTimeout(timer); - }, [locked]); + // Cierre por inactividad (3 min sin teclado/clic/toque/rueda): lo cuenta , que + // muestra el tiempo restante junto al titulo y reinicia con cada accion del usuario, asi que + // nunca lo saca en medio de escribir/editar una credencial. + function autoLockExit() { + pushToast({ tone: 'warning', message: t('vault_auto_lock_notice') }); + navigate('/'); + } async function submitPin() { setPinError(''); @@ -433,6 +432,7 @@ export default function ContrasenasPage() {

{t('nav_passwords')}

+
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 3b66888..3465360 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -21,6 +21,10 @@ async function request(method, path, { body, isForm, extraHeaders } = {}) { body: isForm ? body : body !== undefined ? JSON.stringify(body) : undefined, }); + // El servidor renueva el token de desbloqueo de Contraseñas en cada request valido (ver vault/middleware.js). + const refreshedVaultToken = res.headers.get('x-vault-token'); + if (refreshedVaultToken) sessionStorage.setItem('plc_vault_token', refreshedVaultToken); + if (res.status === 401) { onUnauthorized(); }