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:
@@ -0,0 +1,67 @@
|
||||
import { pool } from '../../config/db.js';
|
||||
|
||||
const LIST_SELECT = 'id, user_id, title, url, username, created_at, updated_at';
|
||||
const FULL_SELECT = `${LIST_SELECT}, password_enc, notes_enc`;
|
||||
|
||||
export async function findByUser(userId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT ${LIST_SELECT} FROM vault_entries WHERE user_id = ? ORDER BY title`,
|
||||
[userId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function findById(id) {
|
||||
const [rows] = await pool.query(`SELECT ${FULL_SELECT} FROM vault_entries WHERE id = ?`, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function create(userId, { title, url, username, passwordEnc, notesEnc }) {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO vault_entries (user_id, title, url, username, password_enc, notes_enc)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[userId, title, url || null, username || null, passwordEnc || null, notesEnc || null]
|
||||
);
|
||||
return findById(result.insertId);
|
||||
}
|
||||
|
||||
export async function update(id, { title, url, username, passwordEnc, notesEnc }) {
|
||||
await pool.query(
|
||||
`UPDATE vault_entries SET title = ?, url = ?, username = ?, password_enc = ?, notes_enc = ? WHERE id = ?`,
|
||||
[title, url || null, username || null, passwordEnc || null, notesEnc || null, id]
|
||||
);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
export async function remove(id) {
|
||||
await pool.query('DELETE FROM vault_entries WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
// --- PIN de acceso al modulo (en la tabla users, ver migracion 017) ---
|
||||
|
||||
export async function getVaultPinHash(userId) {
|
||||
const [rows] = await pool.query('SELECT vault_pin_hash FROM users WHERE id = ?', [userId]);
|
||||
return rows[0]?.vault_pin_hash || null;
|
||||
}
|
||||
|
||||
export async function setVaultPinHash(userId, hash) {
|
||||
await pool.query(
|
||||
'UPDATE users SET vault_pin_hash = ?, vault_pin_reset_token = NULL, vault_pin_reset_token_expires = NULL WHERE id = ?',
|
||||
[hash, userId]
|
||||
);
|
||||
}
|
||||
|
||||
export async function setPinResetToken(userId, token) {
|
||||
await pool.query(
|
||||
'UPDATE users SET vault_pin_reset_token = ?, vault_pin_reset_token_expires = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE id = ?',
|
||||
[token, userId]
|
||||
);
|
||||
}
|
||||
|
||||
export async function findByPinResetToken(token) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id FROM users WHERE vault_pin_reset_token = ? AND vault_pin_reset_token_expires > NOW()',
|
||||
[token]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
Reference in New Issue
Block a user