feat: version 2.16.0 -- contador de cierre en Contraseñas, cierre por inactividad y ajustes de Equipos
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<span title={title} style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<Badge color={secs <= 30 ? 'warning' : 'muted'}>
|
||||
<Timer size={12} style={{ verticalAlign: '-2px', marginRight: 4 }} />{label}
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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 <AutoLockCountdown>, 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() {
|
||||
<div>
|
||||
<div className="toolbar">
|
||||
<h2 className="page-title mt-0" style={{ marginBottom: 0 }}>{t('nav_passwords')}</h2>
|
||||
<AutoLockCountdown ms={AUTO_LOCK_MS} onExpire={autoLockExit} title={t('vault_countdown_hint')} />
|
||||
<SearchInput value={search} onChange={setSearch} />
|
||||
<div style={{ flex: 1 }} />
|
||||
<Button variant="standard" icon={RefreshCw} onClick={() => setChangingPin(true)}>{t('vault_change_pin')}</Button>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user