security(rbac): sub-pass clínico — fecha o RBAC intra-clínica
Gateia 16 rotas clínicas remanescentes a 'agenda' (staff clínico; bloqueia paciente):
- coleções (requireCapability): agenda/presenca (GET+POST), agenda/pendencias,
agenda/atividade, agenda/importar-google, interesses (GET+POST), dentistas/horarios (POST),
dentistas/:dentistaId/horarios (GET)
- /:id (requireCapabilityRow, clinicaId da linha): agenda/pendencias/:id/resolver,
dentistas/horarios/:id (DELETE), agendamentos/:id/{avaliacao,historico},
pacientes/:id/{score,faltas,familia}
As /:id eram authGuard SEM validação de tenant — agora o requireCapabilityRow valida
vínculo+cargo pela clínica da linha (fecha também o gap cross-tenant).
Confirmado que telas de paciente (MeusTratamentos/Notifications) não usam nenhuma delas;
detalhes de paciente são só do PatientsView (funcionário+dono).
Validado em DEV: dono+dentista passam em tudo (200/404); row real→200, fake→404.
Residual cosmético (fora do sub-pass): rotas /reorder (sem clinicaId/:id, baixo impacto).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-16
@@ -2042,7 +2042,7 @@ app.delete('/api/dentistas/:id', authGuard, requireCapabilityRow('dentistas', 'd
|
||||
});
|
||||
|
||||
// --- GESTÃO DE HORÁRIOS ---
|
||||
app.get('/api/dentistas/:dentistaId/horarios', authGuard, async (req, res) => {
|
||||
app.get('/api/dentistas/:dentistaId/horarios', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
if (!await assertRowScoped(req, res, 'SELECT clinica_id AS cid FROM dentistas WHERE id = $1', [req.params.dentistaId])) return;
|
||||
const { dentistaId } = req.params;
|
||||
@@ -2059,7 +2059,7 @@ app.get('/api/dentistas/:dentistaId/horarios', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/dentistas/horarios', authGuard, async (req, res) => {
|
||||
app.post('/api/dentistas/horarios', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
if (!await assertCidScoped(req, res, req.body?.clinica_id)) return;
|
||||
const { id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo } = req.body;
|
||||
@@ -2075,7 +2075,7 @@ app.post('/api/dentistas/horarios', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.delete('/api/dentistas/horarios/:id', authGuard, async (req, res) => {
|
||||
app.delete('/api/dentistas/horarios/:id', authGuard, requireCapabilityRow('agenda', 'dentistas_horarios'), async (req, res) => {
|
||||
try {
|
||||
if (!await assertRowScoped(req, res, 'SELECT clinica_id AS cid FROM dentistas_horarios WHERE id = $1', [req.params.id])) return;
|
||||
await pool.query('DELETE FROM dentistas_horarios WHERE id = $1', [req.params.id]);
|
||||
@@ -3010,7 +3010,7 @@ app.get('/api/avaliacoes', authGuard, tenantGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Avaliação ligada a um agendamento (o dentista abre pelo slot de avaliação).
|
||||
app.get('/api/agendamentos/:id/avaliacao', authGuard, async (req, res) => {
|
||||
app.get('/api/agendamentos/:id/avaliacao', authGuard, requireCapabilityRow('agenda', 'agendamentos'), async (req, res) => {
|
||||
try {
|
||||
if (!await loadAgendamentoScoped(req, res)) return;
|
||||
const { rows } = await pool.query('SELECT * FROM avaliacoes WHERE agendamento_id = $1 ORDER BY created_at DESC LIMIT 1', [req.params.id]);
|
||||
@@ -3100,7 +3100,7 @@ app.delete('/api/avaliacoes/fotos/:id', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Histórico (auditoria) de um agendamento.
|
||||
app.get('/api/agendamentos/:id/historico', authGuard, async (req, res) => {
|
||||
app.get('/api/agendamentos/:id/historico', authGuard, requireCapabilityRow('agenda', 'agendamentos'), async (req, res) => {
|
||||
try {
|
||||
if (!await loadAgendamentoScoped(req, res)) return;
|
||||
const { rows } = await pool.query(
|
||||
@@ -3111,7 +3111,7 @@ app.get('/api/agendamentos/:id/historico', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Pendências "a reagendar" da clínica (default: abertas).
|
||||
app.get('/api/agenda/pendencias', authGuard, async (req, res) => {
|
||||
app.get('/api/agenda/pendencias', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const { clinicaId, status } = req.query;
|
||||
if (!clinicaId) return res.json({ success: true, pendencias: [] });
|
||||
@@ -3123,7 +3123,7 @@ app.get('/api/agenda/pendencias', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Baixa manual de pendência ("não vai reagendar" / dispensar).
|
||||
app.post('/api/agenda/pendencias/:id/resolver', authGuard, async (req, res) => {
|
||||
app.post('/api/agenda/pendencias/:id/resolver', authGuard, requireCapabilityRow('agenda', 'agenda_pendencias'), async (req, res) => {
|
||||
try {
|
||||
const actor = req.authUser?.userId || null;
|
||||
const actorNm = await actorNome(actor);
|
||||
@@ -3135,7 +3135,7 @@ app.post('/api/agenda/pendencias/:id/resolver', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Contagem de faltas de um paciente.
|
||||
app.get('/api/pacientes/:id/faltas', authGuard, async (req, res) => {
|
||||
app.get('/api/pacientes/:id/faltas', authGuard, requireCapabilityRow('agenda', 'pacientes'), async (req, res) => {
|
||||
try {
|
||||
if (!await assertRowScoped(req, res, 'SELECT clinica_id AS cid FROM pacientes WHERE id = $1', [req.params.id])) return;
|
||||
const { rows } = await pool.query(`SELECT count(*)::int AS faltas FROM agendamentos WHERE pacienteid=$1 AND lower(status)='falta'`, [req.params.id]);
|
||||
@@ -3145,7 +3145,7 @@ app.get('/api/pacientes/:id/faltas', authGuard, async (req, res) => {
|
||||
|
||||
// Score do paciente: faltas (no-show sem aviso = ponto negativo) + remarcações (não pontua,
|
||||
// mas em grande número = atenção). Remarcações vêm do HISTÓRICO (audit_log 'remarcou'), durável.
|
||||
app.get('/api/pacientes/:id/score', authGuard, async (req, res) => {
|
||||
app.get('/api/pacientes/:id/score', authGuard, requireCapabilityRow('agenda', 'pacientes'), async (req, res) => {
|
||||
try {
|
||||
if (!await assertRowScoped(req, res, 'SELECT clinica_id AS cid FROM pacientes WHERE id = $1', [req.params.id])) return;
|
||||
const pid = req.params.id;
|
||||
@@ -3164,7 +3164,7 @@ app.get('/api/pacientes/:id/score', authGuard, async (req, res) => {
|
||||
|
||||
// --- PRESENÇA NA AGENDA (Fase 2) ---
|
||||
// Heartbeat: registra a última visita (DB, persistente) + presença AO VIVO (Dragonfly, TTL).
|
||||
app.post('/api/agenda/presenca', authGuard, async (req, res) => {
|
||||
app.post('/api/agenda/presenca', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const actor = req.authUser?.userId || null;
|
||||
const { clinicaId } = req.body || {};
|
||||
@@ -3185,7 +3185,7 @@ app.post('/api/agenda/presenca', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Quem está ONLINE agora (Dragonfly, janela de 45s) + últimas visitas (DB).
|
||||
app.get('/api/agenda/presenca', authGuard, async (req, res) => {
|
||||
app.get('/api/agenda/presenca', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const { clinicaId } = req.query;
|
||||
if (!clinicaId) return res.json({ success: true, online: [], ultimas: [] });
|
||||
@@ -3210,7 +3210,7 @@ app.get('/api/agenda/presenca', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Feed de ATIVIDADE da equipe (auditoria da clínica) — default: hoje.
|
||||
app.get('/api/agenda/atividade', authGuard, async (req, res) => {
|
||||
app.get('/api/agenda/atividade', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const { clinicaId, desde } = req.query;
|
||||
if (!clinicaId) return res.json({ success: true, atividade: [] });
|
||||
@@ -3228,7 +3228,7 @@ app.get('/api/agenda/atividade', authGuard, async (req, res) => {
|
||||
|
||||
// Importa agendamentos do Google Calendar p/ DENTRO do sistema (viram agendamentos editáveis).
|
||||
// Janela: últimos 7 dias + próximos N dias (default 60). Idempotente (dedup por google_event_id).
|
||||
app.post('/api/agenda/importar-google', authGuard, async (req, res) => {
|
||||
app.post('/api/agenda/importar-google', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const { clinicaId, dias } = req.body || {};
|
||||
if (!clinicaId) return res.status(400).json({ success: false, message: 'clinicaId obrigatório.' });
|
||||
@@ -3277,7 +3277,7 @@ app.post('/api/agenda/importar-google', authGuard, async (req, res) => {
|
||||
});
|
||||
|
||||
// Grupo familiar do paciente (para chips compactos no modal de agenda).
|
||||
app.get('/api/pacientes/:id/familia', authGuard, async (req, res) => {
|
||||
app.get('/api/pacientes/:id/familia', authGuard, requireCapabilityRow('agenda', 'pacientes'), async (req, res) => {
|
||||
try {
|
||||
if (!await assertRowScoped(req, res, 'SELECT clinica_id AS cid FROM pacientes WHERE id = $1', [req.params.id])) return;
|
||||
const { rows: pr } = await pool.query('SELECT grupofamiliarid FROM pacientes WHERE id = $1', [req.params.id]);
|
||||
@@ -3292,7 +3292,7 @@ app.get('/api/pacientes/:id/familia', authGuard, async (req, res) => {
|
||||
|
||||
// --- INTERESSES DE AGENDA (Fase 4) ---
|
||||
// Fila de interesse quando o horário do profissional está ocupado (não trava o slot).
|
||||
app.post('/api/interesses', authGuard, async (req, res) => {
|
||||
app.post('/api/interesses', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const { dentistaId, profissionalUsuarioId, clinicaInteressadaId, solicitanteUsuarioId, start, end, tempoEsperaMin, observacoes } = req.body;
|
||||
if (!clinicaInteressadaId || !start) return res.status(400).json({ success: false, message: 'clinicaInteressadaId e start são obrigatórios.' });
|
||||
@@ -3329,7 +3329,7 @@ app.post('/api/interesses', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
});
|
||||
|
||||
app.get('/api/interesses', authGuard, async (req, res) => {
|
||||
app.get('/api/interesses', authGuard, requireCapability('agenda'), async (req, res) => {
|
||||
try {
|
||||
const { profissionalId, clinicaId } = req.query;
|
||||
const uid = req.authUser?.userId;
|
||||
|
||||
Reference in New Issue
Block a user