security(rbac): autorização por cargo em orçamentos e contratos
Gateia 18 rotas (tenantGuard + requireCapability), espelhando o frontend: - orcamentos (GET/POST/PUT/DELETE) → 'orcamentos' (funcionário + dono) - contratos (modelos CRUD+duplicar; instâncias CRUD+enviar+assinar+cancelar+ gerar-cobranca; GETs) → 'contratos' (funcionário + dono) 'assinar' incluído: usa tenantGuard+loadContratoScoped (quem assina é membro da clínica registrando a assinatura via ContratosView — não há fluxo de contraparte externa). Todos os endpoints de contrato são usados só no ContratosView (funcionário+dono). Propostas (marketplace, autorização por posse do profissional) ficam para sub-pass próprio. Validado em DEV: dono acessa (fake→404); dentista 403. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-18
@@ -4033,7 +4033,7 @@ app.delete('/api/protese/os/:id', tenantGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// --- CONTRATOS ---
|
||||
app.get('/api/orcamentos', tenantGuard, async (req, res) => {
|
||||
app.get('/api/orcamentos', tenantGuard, requireCapability('orcamentos'), async (req, res) => {
|
||||
try {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const { paciente_id, status } = req.query;
|
||||
@@ -4072,7 +4072,7 @@ app.get('/api/orcamentos', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/orcamentos', tenantGuard, async (req, res) => {
|
||||
app.post('/api/orcamentos', tenantGuard, requireCapability('orcamentos'), async (req, res) => {
|
||||
try {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const { items, ...contrato } = req.body;
|
||||
@@ -4093,7 +4093,7 @@ app.post('/api/orcamentos', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.put('/api/orcamentos/:id', tenantGuard, async (req, res) => {
|
||||
app.put('/api/orcamentos/:id', tenantGuard, requireCapability('orcamentos'), async (req, res) => {
|
||||
try {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const own = await pool.query('SELECT financeiro_gerado FROM orcamentos WHERE id = $1 AND clinica_id = $2', [req.params.id, req.clinicaId]);
|
||||
@@ -4155,7 +4155,7 @@ async function gerarFinanceiroOrcamento(o, req) {
|
||||
return lanc.filter(l => l.valor > 0).length;
|
||||
}
|
||||
|
||||
app.delete('/api/orcamentos/:id', tenantGuard, async (req, res) => {
|
||||
app.delete('/api/orcamentos/:id', tenantGuard, requireCapability('orcamentos'), async (req, res) => {
|
||||
try {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const own = await pool.query('SELECT 1 FROM orcamentos WHERE id = $1 AND clinica_id = $2', [req.params.id, req.clinicaId]);
|
||||
@@ -7065,7 +7065,7 @@ const MODELO_CONTRATO_ROW = 'id, clinica_id, nome, categoria, tipo_relacao, corp
|
||||
|
||||
app.get('/api/contratos/variaveis', authGuard, async (_req, res) => res.json(VARIAVEIS_CONTRATO));
|
||||
|
||||
app.get('/api/contratos/modelos', tenantGuard, async (req, res) => {
|
||||
app.get('/api/contratos/modelos', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
const clinicaId = req.clinicaId || null;
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
@@ -7076,7 +7076,7 @@ app.get('/api/contratos/modelos', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/contratos/modelos', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/modelos', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const { nome, categoria, tipo_relacao, corpo, vigencia_dias } = req.body || {};
|
||||
if (!nome?.trim() || !corpo?.trim()) return res.status(400).json({ error: 'Nome e corpo são obrigatórios.' });
|
||||
@@ -7090,7 +7090,7 @@ app.post('/api/contratos/modelos', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.put('/api/contratos/modelos/:id', tenantGuard, async (req, res) => {
|
||||
app.put('/api/contratos/modelos/:id', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const { nome, categoria, tipo_relacao, corpo, vigencia_dias } = req.body || {};
|
||||
if (!nome?.trim() || !corpo?.trim()) return res.status(400).json({ error: 'Nome e corpo são obrigatórios.' });
|
||||
@@ -7104,7 +7104,7 @@ app.put('/api/contratos/modelos/:id', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.delete('/api/contratos/modelos/:id', tenantGuard, async (req, res) => {
|
||||
app.delete('/api/contratos/modelos/:id', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
@@ -7115,7 +7115,7 @@ app.delete('/api/contratos/modelos/:id', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/contratos/modelos/:id/duplicar', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/modelos/:id/duplicar', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
@@ -7139,7 +7139,7 @@ function renderContratoCorpo(corpo, valores) {
|
||||
return String(corpo || '').replace(/\{\{(\w+)\}\}/g, (m, k) => (v[k] != null && String(v[k]).trim() !== '') ? String(v[k]) : m);
|
||||
}
|
||||
|
||||
app.post('/api/contratos/instancias', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/instancias', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const b = req.body || {};
|
||||
if (!b.titulo?.trim()) return res.status(400).json({ error: 'Título obrigatório.' });
|
||||
@@ -7168,7 +7168,7 @@ app.post('/api/contratos/instancias', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.get('/api/contratos/instancias', tenantGuard, async (req, res) => {
|
||||
app.get('/api/contratos/instancias', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
@@ -7178,7 +7178,7 @@ app.get('/api/contratos/instancias', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.get('/api/contratos/instancias/:id', tenantGuard, async (req, res) => {
|
||||
app.get('/api/contratos/instancias/:id', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT * FROM contratos WHERE id=$1 AND deleted_at IS NULL AND clinica_origem_id=$2', [req.params.id, req.clinicaId]);
|
||||
@@ -7192,7 +7192,7 @@ app.get('/api/contratos/instancias/:id', tenantGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.put('/api/contratos/instancias/:id', tenantGuard, async (req, res) => {
|
||||
app.put('/api/contratos/instancias/:id', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const b = req.body || {};
|
||||
try {
|
||||
@@ -7213,7 +7213,7 @@ app.put('/api/contratos/instancias/:id', tenantGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// H4.2 — gera manualmente a cobrança recorrente da competência atual de um contrato vigente
|
||||
app.post('/api/contratos/instancias/:id/gerar-cobranca', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/instancias/:id/gerar-cobranca', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT * FROM contratos WHERE id=$1 AND deleted_at IS NULL AND clinica_origem_id=$2', [req.params.id, req.clinicaId]);
|
||||
@@ -7228,7 +7228,7 @@ app.post('/api/contratos/instancias/:id/gerar-cobranca', tenantGuard, async (req
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.delete('/api/contratos/instancias/:id', tenantGuard, async (req, res) => {
|
||||
app.delete('/api/contratos/instancias/:id', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rowCount } = await pool.query('UPDATE contratos SET deleted_at=NOW() WHERE id=$1 AND clinica_origem_id=$2 AND deleted_at IS NULL', [req.params.id, req.clinicaId]);
|
||||
@@ -7245,7 +7245,7 @@ async function loadContratoScoped(id, clinicaId) {
|
||||
const reqIp = (req) => (String(req.headers['x-forwarded-for'] || '').split(',')[0].trim()) || req.socket?.remoteAddress || null;
|
||||
|
||||
// Enviar para assinatura: rascunho → aguardando_assinatura (cria assinaturas pendentes por parte).
|
||||
app.post('/api/contratos/instancias/:id/enviar', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/instancias/:id/enviar', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const c = await loadContratoScoped(req.params.id, req.clinicaId);
|
||||
@@ -7266,7 +7266,7 @@ app.post('/api/contratos/instancias/:id/enviar', tenantGuard, async (req, res) =
|
||||
});
|
||||
|
||||
// Assinar uma parte (aceite eletrônico simples do usuário logado: ip/hash/user-agent).
|
||||
app.post('/api/contratos/instancias/:id/assinar', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/instancias/:id/assinar', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
const parteId = req.body?.parteId;
|
||||
if (!parteId) return res.status(400).json({ error: 'parteId obrigatório.' });
|
||||
@@ -7297,7 +7297,7 @@ app.post('/api/contratos/instancias/:id/assinar', tenantGuard, async (req, res)
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/contratos/instancias/:id/cancelar', tenantGuard, async (req, res) => {
|
||||
app.post('/api/contratos/instancias/:id/cancelar', tenantGuard, requireCapability('contratos'), async (req, res) => {
|
||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
|
||||
Reference in New Issue
Block a user