From 06674563d5a1d83eafefd75e1141388366a42a85 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Fri, 19 Jun 2026 18:21:03 +0200 Subject: [PATCH] security(rbac): gateia rotas /reorder + auditoria do marketplace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Novo helper requireCapabilityFromOrders(cap, tabela): deriva o clinicaId do 1º item de req.body.orders, fechando o RBAC dos catálogos. - Aplica em PUT /{especialidades,procedimentos,dentistas}/reorder (cap do catálogo). Antes eram authGuard puro (qualquer autenticado reordenava catálogo de qualquer clínica). - Validado em DEV: dono 200; dentista 403; id inexistente 404; lista vazia 400. Marketplace (salas/sala-reservas/propostas): AUDITADO — já protegido por checagem de POSSE nos handlers (owner_usuario_id / profissional_id destinatário / vínculo p/ cancelar). Não há gap; capability-gating seria incorreto (modelo é posse cross-clínica). Sem mudança. Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/server.js b/backend/server.js index 61ad1b2..267a62a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -327,6 +327,21 @@ function requireCapabilityRow(cap, table) { }; } +// Capability para reordenação: clinicaId vem do 1º item de req.body.orders ([{id,ordem}]). +function requireCapabilityFromOrders(cap, table) { + return async function (req, res, next) { + try { + const orders = Array.isArray(req.body?.orders) ? req.body.orders : []; + const firstId = orders.length ? orders[0].id : null; + if (!firstId) return res.status(400).json({ error: 'Lista de ordenação vazia.' }); + const { rows } = await pool.query(`SELECT clinica_id AS cid FROM ${table} WHERE id = $1`, [firstId]); + if (!rows.length) return res.status(404).json({ error: 'Item não encontrado.' }); + req.clinicaId = rows[0].cid || null; + return await capabilityCore(cap, req, res, next); + } catch (e) { return res.status(500).json({ error: 'Erro interno de autorização.' }); } + }; +} + // ============================================================================= // AUDITORIA GENÉRICA + PENDÊNCIAS DE AGENDA (Fase 1) @@ -1992,7 +2007,7 @@ app.post('/api/dentistas', tenantGuard, requireCapability('dentistas'), async (r } catch (err) { res.status(500).json({ error: err.message }); } }); -app.put('/api/dentistas/reorder', authGuard, async (req, res) => { +app.put('/api/dentistas/reorder', authGuard, requireCapabilityFromOrders('dentistas', 'dentistas'), async (req, res) => { const { orders } = req.body; // Array of {id, ordem} try { const ids = (orders || []).map(o => o.id); @@ -3380,7 +3395,7 @@ app.post('/api/especialidades', authGuard, requireCapability('especialidades'), } catch (err) { res.status(500).json({ error: err.message }); } }); -app.put('/api/especialidades/reorder', authGuard, async (req, res) => { +app.put('/api/especialidades/reorder', authGuard, requireCapabilityFromOrders('especialidades', 'especialidades'), async (req, res) => { const { orders } = req.body; // Array of {id, ordem} try { await withTransaction(async (client) => { @@ -3531,7 +3546,7 @@ app.post('/api/procedimentos', authGuard, requireCapability('procedimentos'), as } }); -app.put('/api/procedimentos/reorder', authGuard, async (req, res) => { +app.put('/api/procedimentos/reorder', authGuard, requireCapabilityFromOrders('procedimentos', 'procedimentos'), async (req, res) => { const { orders } = req.body; // Array of {id, ordem} try { await withTransaction(async (client) => {