security(rbac): gateia rotas /reorder + auditoria do marketplace

- 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 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-19 18:21:03 +02:00
parent 0fc69bbcd3
commit 06674563d5
+18 -3
View File
@@ -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) => {