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 ---
|
// --- CONTRATOS ---
|
||||||
app.get('/api/orcamentos', tenantGuard, async (req, res) => {
|
app.get('/api/orcamentos', tenantGuard, requireCapability('orcamentos'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const { paciente_id, status } = req.query;
|
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 }); }
|
} 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 {
|
try {
|
||||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const { items, ...contrato } = req.body;
|
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 }); }
|
} 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 {
|
try {
|
||||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
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]);
|
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;
|
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 {
|
try {
|
||||||
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
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]);
|
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/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;
|
const clinicaId = req.clinicaId || null;
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query(
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const { nome, categoria, tipo_relacao, corpo, vigencia_dias } = req.body || {};
|
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.' });
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const { nome, categoria, tipo_relacao, corpo, vigencia_dias } = req.body || {};
|
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.' });
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
try {
|
||||||
const { rowCount } = await pool.query(
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query(
|
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);
|
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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const b = req.body || {};
|
const b = req.body || {};
|
||||||
if (!b.titulo?.trim()) return res.status(400).json({ error: 'Título obrigatório.' });
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query(
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
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]);
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const b = req.body || {};
|
const b = req.body || {};
|
||||||
try {
|
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
|
// 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
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]);
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
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]);
|
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;
|
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).
|
// 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
try {
|
||||||
const c = await loadContratoScoped(req.params.id, req.clinicaId);
|
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).
|
// 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
const parteId = req.body?.parteId;
|
const parteId = req.body?.parteId;
|
||||||
if (!parteId) return res.status(400).json({ error: 'parteId obrigatório.' });
|
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 }); }
|
} 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.' });
|
if (!req.clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||||
try {
|
try {
|
||||||
const { rowCount } = await pool.query(
|
const { rowCount } = await pool.query(
|
||||||
|
|||||||
Reference in New Issue
Block a user