feat: version 2.7.2 -- tarjetas con icono en Contraseñas + fix borrado de password al editar

Listado de Contraseñas pasa de tabla a tarjetas: icono elegible (
catalogo fijo, sin servicios externos de favicon por privacidad) o
inicial del nombre como fallback, junto con nombre y enlace.

Fix real: editar una credencial sin revelar la contraseña antes la
dejaba vacia al guardar -- ahora se conserva la que ya estaba si no
se escribe una nueva.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-01 12:41:22 -05:00
parent 28e339f19a
commit 127ca2ba7e
11 changed files with 161 additions and 36 deletions
+17 -3
View File
@@ -11,6 +11,16 @@ import { sendMail } from '../../utils/mailer.js';
const PIN_RE = /^\d{4}$/;
const VAULT_TOKEN_EXPIRES = '30m';
// Catalogo fijo (debe coincidir con el del frontend, ContrasenasPage.jsx#ICONS) -- nunca se
// acepta un valor arbitrario, para no guardar basura ni abrir una via de inyeccion rara.
const ICON_KEYS = new Set([
'mail', 'globe', 'server', 'database', 'cloud', 'shield', 'lock', 'wifi',
'card', 'building', 'printer', 'phone', 'key', 'disk', 'router', 'user',
]);
function sanitizeIconKey(key) {
return ICON_KEYS.has(key) ? key : null;
}
function newToken() {
return crypto.randomBytes(32).toString('hex');
}
@@ -54,20 +64,24 @@ export async function createEntry(payload, currentUser) {
username: payload.username?.trim(),
passwordEnc: payload.password ? encrypt(payload.password) : null,
notesEnc: payload.notes ? encrypt(payload.notes) : null,
iconKey: sanitizeIconKey(payload.iconKey),
});
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'vault_entry', entityId: entry.id });
return stripSensitive(entry);
}
export async function updateEntry(id, payload, currentUser) {
await getOwnEntryOr404(id, currentUser);
const entry = await getOwnEntryOr404(id, currentUser);
if (!payload.title?.trim()) throw new HttpError(400, 'El nombre es obligatorio');
// Si no viene password/notes nuevo (ej. se edito solo el nombre sin pasar por "Revelar"
// primero), se conserva lo que ya estaba guardado -- nunca se borra por venir vacio.
const updated = await repo.update(id, {
title: payload.title.trim(),
url: payload.url?.trim(),
username: payload.username?.trim(),
passwordEnc: payload.password ? encrypt(payload.password) : null,
notesEnc: payload.notes ? encrypt(payload.notes) : null,
passwordEnc: payload.password ? encrypt(payload.password) : entry.password_enc,
notesEnc: payload.notes !== undefined && payload.notes !== '' ? encrypt(payload.notes) : entry.notes_enc,
iconKey: sanitizeIconKey(payload.iconKey),
});
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'vault_entry', entityId: id });
return stripSensitive(updated);