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:
+18
-3
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user