feat: version 2.15.0 -- pestaña Compartir en Equipos (acceso de solo lectura)
Admin/superadmin dan acceso de solo lectura (Resumen, Equipos, Software) a otros usuarios; el backend valida el acceso en cada endpoint y el menu aparece solo a quien lo tenga. Migracion 026 (device_viewers). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,3 +18,8 @@ export const postToken = asyncHandler(async (req, res) => ok(res, await service.
|
||||
export const getTokenSecret = asyncHandler(async (req, res) => ok(res, await service.revealToken(Number(req.params.id), req.currentUser)));
|
||||
export const patchToken = asyncHandler(async (req, res) => ok(res, await service.setTokenActive(Number(req.params.id), !!req.body.isActive, req.currentUser)));
|
||||
export const deleteToken = asyncHandler(async (req, res) => ok(res, await service.removeToken(Number(req.params.id), req.currentUser)));
|
||||
|
||||
export const getAccess = asyncHandler(async (req, res) => ok(res, await service.getAccess(req.currentUser)));
|
||||
export const getViewers = asyncHandler(async (req, res) => ok(res, await service.listViewers()));
|
||||
export const postViewer = asyncHandler(async (req, res) => ok(res, await service.addViewer(Number(req.body.userId), req.currentUser), 201));
|
||||
export const deleteViewer = asyncHandler(async (req, res) => ok(res, await service.removeViewer(Number(req.params.userId), req.currentUser)));
|
||||
|
||||
@@ -46,6 +46,30 @@ export async function findTokenSecret(id) {
|
||||
export const setTokenActive = (id, active) => pool.query('UPDATE inventory_tokens SET is_active = ? WHERE id = ?', [active ? 1 : 0, id]);
|
||||
export const deleteToken = (id) => pool.query('DELETE FROM inventory_tokens WHERE id = ?', [id]);
|
||||
|
||||
// ---------- Acceso compartido (solo lectura) ----------
|
||||
|
||||
export async function isViewer(userId) {
|
||||
const [rows] = await pool.query('SELECT 1 FROM device_viewers WHERE user_id = ?', [userId]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export async function listViewers() {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT v.user_id, u.name, u.email, r.name AS role_name, v.created_at, g.name AS granted_by_name
|
||||
FROM device_viewers v JOIN users u ON u.id = v.user_id JOIN roles r ON r.id = u.role_id
|
||||
LEFT JOIN users g ON g.id = v.granted_by ORDER BY u.name`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export const addViewer = (userId, grantedBy) => pool.query('INSERT IGNORE INTO device_viewers (user_id, granted_by) VALUES (?, ?)', [userId, grantedBy]);
|
||||
export const removeViewer = (userId) => pool.query('DELETE FROM device_viewers WHERE user_id = ?', [userId]);
|
||||
|
||||
export async function findUserBrief(userId) {
|
||||
const [[row]] = await pool.query('SELECT u.id, u.name, u.is_active, r.code AS role_code FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ?', [userId]);
|
||||
return row || null;
|
||||
}
|
||||
|
||||
// ---------- Ingesta ----------
|
||||
|
||||
// Foto de lo que hay hoy en BD (para calcular el diff contra el reporte nuevo).
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requireRole, ROLES } from '../../middleware/auth.js';
|
||||
import { fail } from '../../utils/http.js';
|
||||
import { getAccess as getAccessFor } from './service.js';
|
||||
import {
|
||||
getDevices, getDevice, putDevice, getDashboard, getSoftware, getSoftwareDevices, getRaw,
|
||||
getTokens, postToken, patchToken, deleteToken, getTokenSecret,
|
||||
getTokens, postToken, patchToken, deleteToken, getTokenSecret, getAccess, getViewers, postViewer, deleteViewer,
|
||||
} from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Inventario: lo ve y lo edita quien gestiona TI. Los tokens del agente, solo admin/superadmin.
|
||||
const CAN_VIEW = [ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER];
|
||||
const CAN_MANAGE_TOKENS = [ROLES.SUPERADMIN, ROLES.ADMIN];
|
||||
// Inventario: lo ve y lo edita quien gestiona TI. Los tokens del agente y el compartir, solo
|
||||
// admin/superadmin. Otros usuarios pueden tener acceso de SOLO LECTURA compartido (device_viewers).
|
||||
const CAN_EDIT = [ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER];
|
||||
const CAN_MANAGE = [ROLES.SUPERADMIN, ROLES.ADMIN];
|
||||
|
||||
router.use(requireAuth, requireRole(...CAN_VIEW));
|
||||
router.get('/dashboard', getDashboard);
|
||||
router.get('/software', getSoftware);
|
||||
router.get('/software/devices', getSoftwareDevices);
|
||||
router.get('/tokens', requireRole(...CAN_MANAGE_TOKENS), getTokens);
|
||||
router.post('/tokens', requireRole(...CAN_MANAGE_TOKENS), postToken);
|
||||
router.get('/tokens/:id/secret', requireRole(...CAN_MANAGE_TOKENS), getTokenSecret);
|
||||
router.patch('/tokens/:id', requireRole(...CAN_MANAGE_TOKENS), patchToken);
|
||||
router.delete('/tokens/:id', requireRole(...CAN_MANAGE_TOKENS), deleteToken);
|
||||
router.get('/', getDevices);
|
||||
router.get('/:id', getDevice);
|
||||
router.get('/:id/raw', getRaw);
|
||||
router.put('/:id', putDevice);
|
||||
async function canView(req, res, next) {
|
||||
try {
|
||||
const access = await getAccessFor(req.currentUser);
|
||||
if (!access.canView) return fail(res, 403, 'No tienes permisos para esta accion');
|
||||
next();
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
router.get('/access', getAccess); // cualquier autenticado: el menu lo usa para decidir si se muestra
|
||||
router.get('/dashboard', canView, getDashboard);
|
||||
router.get('/software', canView, getSoftware);
|
||||
router.get('/software/devices', canView, getSoftwareDevices);
|
||||
router.get('/viewers', requireRole(...CAN_MANAGE), getViewers);
|
||||
router.post('/viewers', requireRole(...CAN_MANAGE), postViewer);
|
||||
router.delete('/viewers/:userId', requireRole(...CAN_MANAGE), deleteViewer);
|
||||
router.get('/tokens', requireRole(...CAN_MANAGE), getTokens);
|
||||
router.post('/tokens', requireRole(...CAN_MANAGE), postToken);
|
||||
router.get('/tokens/:id/secret', requireRole(...CAN_MANAGE), getTokenSecret);
|
||||
router.patch('/tokens/:id', requireRole(...CAN_MANAGE), patchToken);
|
||||
router.delete('/tokens/:id', requireRole(...CAN_MANAGE), deleteToken);
|
||||
router.get('/', canView, getDevices);
|
||||
router.get('/:id', canView, getDevice);
|
||||
router.get('/:id/raw', requireRole(...CAN_EDIT), getRaw);
|
||||
router.put('/:id', requireRole(...CAN_EDIT), putDevice);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -51,6 +51,33 @@ export const listSoftware = (q, all) => repo.listSoftware({ search: q?.trim() ||
|
||||
export const devicesWithSoftware = (name) => repo.listDevicesWithSoftware(name);
|
||||
export const getDashboard = () => repo.dashboard(STALE_DAYS);
|
||||
|
||||
const FULL_ACCESS_ROLES = ['superadmin', 'admin', 'it_manager'];
|
||||
const MANAGE_ROLES = ['superadmin', 'admin'];
|
||||
|
||||
// Que puede hacer este usuario en Equipos: ver (rol de TI o compartido), editar, gestionar (agentes/compartir).
|
||||
export async function getAccess(user) {
|
||||
const full = FULL_ACCESS_ROLES.includes(user.roleCode);
|
||||
const canView = full || (await repo.isViewer(user.id));
|
||||
return { canView, canEdit: full, canManage: MANAGE_ROLES.includes(user.roleCode) };
|
||||
}
|
||||
|
||||
export const listViewers = () => repo.listViewers();
|
||||
|
||||
export async function addViewer(userId, actor) {
|
||||
const target = await repo.findUserBrief(userId);
|
||||
if (!target || !target.is_active) throw new HttpError(404, 'Usuario no encontrado');
|
||||
if (FULL_ACCESS_ROLES.includes(target.role_code)) throw new HttpError(400, 'Ese usuario ya tiene acceso completo por su rol');
|
||||
await repo.addViewer(userId, actor.id);
|
||||
await logAudit({ userId: actor.id, action: 'share', entityType: 'devices', entityId: userId, detail: { name: target.name } });
|
||||
return repo.listViewers();
|
||||
}
|
||||
|
||||
export async function removeViewer(userId, actor) {
|
||||
await repo.removeViewer(userId);
|
||||
await logAudit({ userId: actor.id, action: 'unshare', entityType: 'devices', entityId: userId });
|
||||
return repo.listViewers();
|
||||
}
|
||||
|
||||
export const listTokens = () => repo.listTokens();
|
||||
|
||||
// El token en claro se devuelve UNA sola vez (aqui); en BD queda solo el hash.
|
||||
|
||||
Reference in New Issue
Block a user