feat: version 2.14.0 -- compartir grupo completo y destruir con cascada
Contraseñas: "Compartir grupo" aplica el mismo share de solo lectura a todas las credenciales de un grupo de una vez (el grupo sigue sin ser una unidad de permiso -- es una accion de conveniencia). El grupo y cada credencial se marcan con el icono de compartido. "Destruir grupo" ahora tiene dos opciones: solo desagrupar (como antes) o destruir tambien las N credenciales, con advertencia explicita del conteo. Fix de paso en ComboBox (afecta a toda la app): el panel se cerraba solo al abrirse si el trigger estaba cerca del borde de un contenedor con scroll, por un micro-scroll automatico del navegador al enfocar el boton. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,10 +63,15 @@ export const putGroup = asyncHandler(async (req, res) => {
|
||||
});
|
||||
|
||||
export const deleteGroup = asyncHandler(async (req, res) => {
|
||||
await service.removeGroup(Number(req.params.id), req.currentUser);
|
||||
const cascade = req.query.cascade === 'true';
|
||||
await service.removeGroup(Number(req.params.id), req.currentUser, cascade);
|
||||
ok(res, { deleted: true });
|
||||
});
|
||||
|
||||
export const postGroupShare = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.shareGroup(Number(req.params.id), Number(req.body.userId), req.currentUser), 201);
|
||||
});
|
||||
|
||||
export const getEntryShares = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.listEntryShares(Number(req.params.id), req.currentUser));
|
||||
});
|
||||
|
||||
@@ -71,14 +71,19 @@ export async function remove(id) {
|
||||
|
||||
// --- Grupos (carpetas de un solo nivel) ---
|
||||
|
||||
// shared_entries_count: cuantas entradas del grupo estan compartidas con AL MENOS un usuario --
|
||||
// alcanza con 1 para que la tarjeta del grupo muestre el icono de compartido (ver service.js).
|
||||
export async function findGroupsByUser(userId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT g.id, g.user_id, g.name, g.icon_key, g.created_at, g.updated_at,
|
||||
(SELECT COUNT(*) FROM vault_entries e WHERE e.group_id = g.id) AS entry_count
|
||||
(SELECT COUNT(*) FROM vault_entries e WHERE e.group_id = g.id) AS entry_count,
|
||||
(SELECT COUNT(DISTINCT e.id) FROM vault_entries e
|
||||
JOIN vault_entry_shares s ON s.entry_id = e.id
|
||||
WHERE e.group_id = g.id) AS shared_entries_count
|
||||
FROM vault_groups g WHERE g.user_id = ? ORDER BY g.name`,
|
||||
[userId]
|
||||
);
|
||||
return rows.map((r) => ({ ...r, entry_count: Number(r.entry_count) }));
|
||||
return rows.map((r) => ({ ...r, entry_count: Number(r.entry_count), shared_entries_count: Number(r.shared_entries_count) }));
|
||||
}
|
||||
|
||||
export async function findGroupById(id) {
|
||||
@@ -104,6 +109,13 @@ export async function removeGroup(id) {
|
||||
await pool.query('DELETE FROM vault_groups WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
// Borrado en cascada: elimina primero las entradas del grupo (irreversible, ver vault/service.js),
|
||||
// despues el grupo mismo. Separado de removeGroup() porque ese sigue siendo el borrado "suave"
|
||||
// (solo desagrupa) que ya existia.
|
||||
export async function removeEntriesInGroup(groupId) {
|
||||
await pool.query('DELETE FROM vault_entries WHERE group_id = ?', [groupId]);
|
||||
}
|
||||
|
||||
// --- Compartir una entrada (solo lectura, ver vault/service.js) ---
|
||||
|
||||
export async function listShares(entryId) {
|
||||
@@ -131,6 +143,18 @@ export async function removeShare(entryId, userId) {
|
||||
return listShares(entryId);
|
||||
}
|
||||
|
||||
// Compartir TODAS las entradas de un grupo de una vez (accion de conveniencia -- sigue siendo
|
||||
// un share por entrada, el grupo nunca es una unidad de permiso: ver vault/service.js#shareGroup).
|
||||
// Devuelve cuantas entradas tiene el grupo (para el mensaje de confirmacion en el frontend).
|
||||
export async function shareGroupEntries(groupId, userId) {
|
||||
await pool.query(
|
||||
'INSERT IGNORE INTO vault_entry_shares (entry_id, user_id) SELECT id, ? FROM vault_entries WHERE group_id = ?',
|
||||
[userId, groupId]
|
||||
);
|
||||
const [rows] = await pool.query('SELECT COUNT(*) AS total FROM vault_entries WHERE group_id = ?', [groupId]);
|
||||
return Number(rows[0].total);
|
||||
}
|
||||
|
||||
// --- PIN de acceso al modulo (en la tabla users, ver migracion 017) ---
|
||||
|
||||
export async function getVaultPinHash(userId) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { requireVaultUnlock } from './middleware.js';
|
||||
import {
|
||||
getStatus, postPin, postUnlock, postChangePin, postForgotPin, postResetPin,
|
||||
getEntries, getEntry, postEntry, putEntry, deleteEntry,
|
||||
getGroups, postGroup, putGroup, deleteGroup,
|
||||
getGroups, postGroup, putGroup, deleteGroup, postGroupShare,
|
||||
getEntryShares, postEntryShare, deleteEntryShare,
|
||||
} from './controller.js';
|
||||
|
||||
@@ -28,6 +28,7 @@ router.get('/groups', requireVaultUnlock, getGroups);
|
||||
router.post('/groups', requireVaultUnlock, postGroup);
|
||||
router.put('/groups/:id', requireVaultUnlock, putGroup);
|
||||
router.delete('/groups/:id', requireVaultUnlock, deleteGroup);
|
||||
router.post('/groups/:id/share', requireVaultUnlock, postGroupShare);
|
||||
|
||||
router.get('/entries', requireVaultUnlock, getEntries);
|
||||
router.get('/entries/:id', requireVaultUnlock, getEntry);
|
||||
|
||||
@@ -176,10 +176,14 @@ export async function updateGroup(id, payload, currentUser) {
|
||||
return group;
|
||||
}
|
||||
|
||||
export async function removeGroup(id, currentUser) {
|
||||
// cascade=false (default): borrado "suave", las entradas quedan sin grupo (como siempre).
|
||||
// cascade=true: ademas destruye TODAS las entradas del grupo -- irreversible, ver confirmacion
|
||||
// fuerte en el frontend (ContrasenasPage.jsx#confirmDeleteGroup).
|
||||
export async function removeGroup(id, currentUser, cascade = false) {
|
||||
await getOwnGroupOr404(id, currentUser);
|
||||
if (cascade) await repo.removeEntriesInGroup(id);
|
||||
await repo.removeGroup(id);
|
||||
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'vault_group', entityId: id });
|
||||
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'vault_group', entityId: id, detail: { cascade } });
|
||||
}
|
||||
|
||||
// --- Compartir una entrada (solo lectura) ---
|
||||
@@ -207,6 +211,21 @@ export async function unshareEntry(id, targetUserId, currentUser) {
|
||||
return shares;
|
||||
}
|
||||
|
||||
// Compartir TODAS las entradas de un grupo de una vez (conveniencia desde el menu del grupo) --
|
||||
// el grupo sigue sin ser una unidad de permiso: esto solo aplica el mismo share de siempre a
|
||||
// cada entrada que tiene hoy ese group_id, una por una. Entradas que se agreguen al grupo
|
||||
// despues NO heredan el share retroactivamente.
|
||||
export async function shareGroup(groupId, targetUserId, currentUser) {
|
||||
await getOwnGroupOr404(groupId, currentUser);
|
||||
if (!targetUserId) throw new HttpError(400, 'Indica un usuario');
|
||||
if (targetUserId === currentUser.id) throw new HttpError(400, 'No puedes compartir contigo mismo');
|
||||
const targetUser = await usersRepo.findById(targetUserId);
|
||||
if (!targetUser) throw new HttpError(404, 'Usuario no encontrado');
|
||||
const sharedCount = await repo.shareGroupEntries(groupId, targetUserId);
|
||||
await logAudit({ userId: currentUser.id, action: 'share', entityType: 'vault_group', entityId: groupId, detail: { userId: targetUserId, entriesCount: sharedCount } });
|
||||
return { sharedCount };
|
||||
}
|
||||
|
||||
// --- PIN ---
|
||||
|
||||
export async function getStatus(currentUser) {
|
||||
|
||||
Reference in New Issue
Block a user