feat: version 2.7.0 -- menu Contraseñas (repositorio de credenciales privado)

Nuevo modulo backend/src/modules/vault/: contraseña y notas cifradas
en el servidor (AES-256-GCM, mismo mecanismo ya usado para la clave
SMTP). Acceso al menu protegido con un PIN de 4 digitos independiente
del login (token corto propio, header X-Vault-Token, nunca el JWT de
sesion). El PIN no participa del cifrado -- cambiarlo o restablecerlo
por correo nunca afecta las contraseñas ya guardadas. Privado por
usuario, sin excepcion. Cada accion sensible (crear/editar/eliminar/
revelar/desbloquear) queda en audit_log. Compartir con otro usuario
queda para una proxima version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-01 12:12:54 -05:00
parent f607455668
commit 0be0712ff0
20 changed files with 879 additions and 10 deletions
+31
View File
@@ -0,0 +1,31 @@
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { requireAuth } from '../../middleware/auth.js';
import { requireVaultUnlock } from './middleware.js';
import {
getStatus, postPin, postUnlock, postChangePin, postForgotPin, postResetPin,
getEntries, getEntry, postEntry, putEntry, deleteEntry,
} from './controller.js';
const router = Router();
// PIN de 4 digitos: 10,000 combinaciones -- sin este limiter seria trivial de forzar.
const unlockLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 8, standardHeaders: true, legacyHeaders: false });
// Publico, como auth/reset-password: el link del correo puede abrirse sin sesion activa.
router.post('/reset-pin', unlockLimiter, postResetPin);
router.use(requireAuth);
router.get('/status', getStatus);
router.post('/pin', postPin);
router.post('/unlock', unlockLimiter, postUnlock);
router.post('/change-pin', unlockLimiter, postChangePin);
router.post('/forgot-pin', unlockLimiter, postForgotPin);
router.get('/entries', requireVaultUnlock, getEntries);
router.get('/entries/:id', requireVaultUnlock, getEntry);
router.post('/entries', requireVaultUnlock, postEntry);
router.put('/entries/:id', requireVaultUnlock, putEntry);
router.delete('/entries/:id', requireVaultUnlock, deleteEntry);
export default router;