feat: version 2.8.0 -- buscador en Contraseñas (nombre/usuario/enlace/notas)

Filtra las tarjetas mientras se escribe. Nombre/usuario/enlace se
comparan en texto plano; las notas (cifradas) se descifran solo en
el servidor para la comparacion -- el contenido nunca viaja de vuelta
al cliente salvo al revelar esa entrada puntual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-01 12:57:10 -05:00
parent 127ca2ba7e
commit 551ed9b790
9 changed files with 61 additions and 11 deletions
@@ -7,6 +7,7 @@ import {
} from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAction } from '../../hooks/useAsync.js';
import { useDebouncedValue } from '../../hooks/useDebouncedValue.js';
import {
getVaultStatus, createVaultPin, unlockVault, changeVaultPin, forgotVaultPin, setVaultToken,
listVaultEntries, getVaultEntry, createVaultEntry, updateVaultEntry, deleteVaultEntry,
@@ -16,6 +17,7 @@ import { Field } from '../../components/Field.jsx';
import { PasswordInput } from '../../components/PasswordInput.jsx';
import { DetailPopup } from '../../components/DetailPopup.jsx';
import { Flyout, FlyoutItem } from '../../components/Flyout.jsx';
import { SearchInput } from '../../components/SearchInput.jsx';
const EMPTY_FORM = { title: '', url: '', username: '', password: '', notes: '', iconKey: '' };
const AUTO_LOCK_MS = 3 * 60 * 1000; // 3 minutos: salir solo del menu si queda abierto sin atencion
@@ -54,6 +56,8 @@ export default function ContrasenasPage() {
const [checking, setChecking] = useState(true);
const [hasPinSet, setHasPinSet] = useState(null);
const [entries, setEntries] = useState(null);
const [search, setSearch] = useState('');
const debouncedSearch = useDebouncedValue(search, 300);
const [pin, setPin] = useState('');
const [pinConfirm, setPinConfirm] = useState('');
@@ -71,8 +75,8 @@ export default function ContrasenasPage() {
const [pinChangeForm, setPinChangeForm] = useState({ current: '', next: '', next2: '' });
const [pinChangeError, setPinChangeError] = useState('');
async function loadEntries() {
const data = await listVaultEntries();
async function loadEntries(query) {
const data = await listVaultEntries(query);
setEntries(data);
setLocked(false);
}
@@ -94,6 +98,15 @@ export default function ContrasenasPage() {
useEffect(() => { checkAccess(); }, []);
// Mientras esta desbloqueado, cada cambio de busqueda (con debounce) vuelve a pedir el listado
// ya filtrado -- nombre/usuario/enlace se comparan tal cual, las notas se descifran solo en el
// servidor para comparar (nunca viaja el contenido al cliente, ver vault/service.js#listEntries).
useEffect(() => {
if (locked || checking) return;
loadEntries(debouncedSearch).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearch]);
// Por seguridad: salir de este menu (a otro, o cerrando la pestaña) siempre bloquea de nuevo --
// el token de desbloqueo se borra al desmontar, asi que la proxima vez que se entra aca pide el
// PIN de nuevo, sin importar cuanto haya durado la sesion del sistema.
@@ -275,12 +288,15 @@ export default function ContrasenasPage() {
<div>
<div className="toolbar">
<h2 className="page-title mt-0" style={{ marginBottom: 0 }}>{t('nav_passwords')}</h2>
<SearchInput value={search} onChange={setSearch} />
<div style={{ flex: 1 }} />
<Button variant="standard" icon={RefreshCw} onClick={() => setChangingPin(true)}>{t('vault_change_pin')}</Button>
<Button variant="accent" icon={Plus} onClick={openCreate}>{t('vault_new_entry')}</Button>
</div>
{entries.length === 0 && <div className="text-muted">{t('common_empty')}</div>}
{entries.length === 0 && (
<div className="text-muted">{search.trim() ? t('common_search_no_results') : t('common_empty')}</div>
)}
<div className="vault-grid">
{entries.map((entry) => (
+1 -1
View File
@@ -26,7 +26,7 @@ export const changeVaultPin = (currentPin, newPin) => api.post('/vault/change-pi
export const forgotVaultPin = () => api.post('/vault/forgot-pin');
export const resetVaultPin = (token, newPin) => api.post('/vault/reset-pin', { token, newPin });
export const listVaultEntries = () => api.get('/vault/entries', vaultHeaders());
export const listVaultEntries = (query) => api.get(`/vault/entries${query ? `?q=${encodeURIComponent(query)}` : ''}`, vaultHeaders());
export const getVaultEntry = (id) => api.get(`/vault/entries/${id}`, vaultHeaders());
export const createVaultEntry = (data) => api.post('/vault/entries', data, vaultHeaders());
export const updateVaultEntry = (id, data) => api.put(`/vault/entries/${id}`, data, vaultHeaders());