diff --git a/backend/server.js b/backend/server.js index a00cbfd..affbfee 100644 --- a/backend/server.js +++ b/backend/server.js @@ -644,7 +644,7 @@ app.post('/api/clinicas', async (req, res) => { } }); -app.get('/api/clinicas/:clinicaId/membros', tenantGuard, async (req, res) => { +app.get('/api/clinicas/:clinicaId/membros', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { try { const { rows } = await pool.query(` SELECT u.id, u.nome, u.email, v.role, v.setor_id, v.funcao_id, @@ -659,7 +659,7 @@ app.get('/api/clinicas/:clinicaId/membros', tenantGuard, async (req, res) => { } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.post('/api/clinicas/:clinicaId/membros', tenantGuard, async (req, res) => { +app.post('/api/clinicas/:clinicaId/membros', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const { email, role, setor_id, funcao_id, nome } = req.body; if (!email || !role) return res.status(400).json({ success: false, message: 'E-mail e função são obrigatórios.' }); try { @@ -679,7 +679,7 @@ app.post('/api/clinicas/:clinicaId/membros', tenantGuard, async (req, res) => { } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.put('/api/clinicas/:clinicaId/membros/:userId', tenantGuard, async (req, res) => { +app.put('/api/clinicas/:clinicaId/membros/:userId', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const { role, setor_id, funcao_id } = req.body; try { const reqNivel = await nivelNaClinica(req.authUser.userId, req.params.clinicaId); @@ -695,7 +695,7 @@ app.put('/api/clinicas/:clinicaId/membros/:userId', tenantGuard, async (req, res // Reset de senha pelo dono/admin da clínica → gera nova senha temporária (resolve // contas criadas sem senha ou esquecidas). Alvo precisa ser membro desta unidade. -app.post('/api/clinicas/:clinicaId/membros/:userId/reset-senha', authGuard, async (req, res) => { +app.post('/api/clinicas/:clinicaId/membros/:userId/reset-senha', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const clinicaId = req.params.clinicaId, alvo = req.params.userId, requester = req.authUser.userId; try { const { rows: tv } = await pool.query('SELECT 1 FROM vinculos WHERE usuario_id = $1 AND clinica_id = $2', [alvo, clinicaId]); @@ -712,7 +712,7 @@ app.post('/api/clinicas/:clinicaId/membros/:userId/reset-senha', authGuard, asyn } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); -app.delete('/api/clinicas/:clinicaId/membros/:userId', tenantGuard, async (req, res) => { +app.delete('/api/clinicas/:clinicaId/membros/:userId', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { try { if (await nivelNaClinica(req.authUser.userId, req.params.clinicaId) <= await nivelNaClinica(req.params.userId, req.params.clinicaId)) { return res.status(403).json({ success: false, message: 'Você só pode remover membros de nível inferior ao seu.' }); @@ -723,14 +723,14 @@ app.delete('/api/clinicas/:clinicaId/membros/:userId', tenantGuard, async (req, }); // --- SETORES (por clínica) --- -app.get('/api/clinicas/:clinicaId/setores', tenantGuard, async (req, res) => { +app.get('/api/clinicas/:clinicaId/setores', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { try { const { rows } = await pool.query('SELECT id, nome, descricao, cor FROM setores WHERE clinica_id = $1 ORDER BY nome', [req.params.clinicaId]); res.json({ success: true, setores: rows }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.post('/api/clinicas/:clinicaId/setores', tenantGuard, async (req, res) => { +app.post('/api/clinicas/:clinicaId/setores', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const { nome, descricao, cor } = req.body; if (!nome) return res.status(400).json({ success: false, message: 'Nome é obrigatório.' }); try { @@ -740,7 +740,7 @@ app.post('/api/clinicas/:clinicaId/setores', tenantGuard, async (req, res) => { } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.put('/api/clinicas/:clinicaId/setores/:id', tenantGuard, async (req, res) => { +app.put('/api/clinicas/:clinicaId/setores/:id', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const { nome, descricao, cor } = req.body; try { await pool.query('UPDATE setores SET nome = COALESCE($1, nome), descricao = $2, cor = COALESCE($3, cor) WHERE id = $4 AND clinica_id = $5', @@ -749,7 +749,7 @@ app.put('/api/clinicas/:clinicaId/setores/:id', tenantGuard, async (req, res) => } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.delete('/api/clinicas/:clinicaId/setores/:id', tenantGuard, async (req, res) => { +app.delete('/api/clinicas/:clinicaId/setores/:id', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { try { await pool.query('UPDATE vinculos SET setor_id = NULL WHERE setor_id = $1 AND clinica_id = $2', [req.params.id, req.params.clinicaId]); await pool.query('DELETE FROM setores WHERE id = $1 AND clinica_id = $2', [req.params.id, req.params.clinicaId]); @@ -758,14 +758,14 @@ app.delete('/api/clinicas/:clinicaId/setores/:id', tenantGuard, async (req, res) }); // --- FUNÇÕES (por clínica) --- -app.get('/api/clinicas/:clinicaId/funcoes', tenantGuard, async (req, res) => { +app.get('/api/clinicas/:clinicaId/funcoes', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { try { const { rows } = await pool.query('SELECT id, nome, descricao, permissoes, nivel FROM funcoes WHERE clinica_id = $1 ORDER BY nivel DESC, nome', [req.params.clinicaId]); res.json({ success: true, funcoes: rows }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.post('/api/clinicas/:clinicaId/funcoes', tenantGuard, async (req, res) => { +app.post('/api/clinicas/:clinicaId/funcoes', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const { nome, descricao, permissoes, nivel } = req.body; if (!nome) return res.status(400).json({ success: false, message: 'Nome é obrigatório.' }); try { @@ -776,7 +776,7 @@ app.post('/api/clinicas/:clinicaId/funcoes', tenantGuard, async (req, res) => { } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.put('/api/clinicas/:clinicaId/funcoes/:id', tenantGuard, async (req, res) => { +app.put('/api/clinicas/:clinicaId/funcoes/:id', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { const { nome, descricao, permissoes, nivel } = req.body; try { await pool.query( @@ -788,7 +788,7 @@ app.put('/api/clinicas/:clinicaId/funcoes/:id', tenantGuard, async (req, res) => } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); -app.delete('/api/clinicas/:clinicaId/funcoes/:id', tenantGuard, async (req, res) => { +app.delete('/api/clinicas/:clinicaId/funcoes/:id', tenantGuard, requireCapability('gestao-equipe'), async (req, res) => { try { await pool.query('UPDATE vinculos SET funcao_id = NULL WHERE funcao_id = $1 AND clinica_id = $2', [req.params.id, req.params.clinicaId]); await pool.query('DELETE FROM funcoes WHERE id = $1 AND clinica_id = $2', [req.params.id, req.params.clinicaId]); @@ -800,7 +800,12 @@ app.delete('/api/clinicas/:clinicaId/funcoes/:id', tenantGuard, async (req, res) app.put('/api/clinicas/:id/color', authGuard, async (req, res) => { const { cor } = req.body; if (!cor || !/^#[0-9a-fA-F]{6}$/.test(cor)) return res.status(400).json({ error: 'Cor inválida (use #RRGGBB).' }); - if (!await userHasVinculo(req.authUser?.userId, req.params.id)) return res.status(403).json({ error: 'Acesso negado: unidade de outra conta.' }); + // Só dono/admin da unidade altera a identidade visual. + const { rows: _own } = await pool.query( + `SELECT 1 FROM clinicas WHERE id=$1 AND owner_id=$2 + UNION SELECT 1 FROM vinculos WHERE clinica_id=$1 AND usuario_id=$2 AND role = ANY(ARRAY['donoclinica','donoconsultorio','admin']) LIMIT 1`, + [req.params.id, req.authUser?.userId]); + if (!_own.length) return res.status(403).json({ error: 'Apenas o dono/admin pode alterar a cor da unidade.' }); try { await pool.query('UPDATE clinicas SET cor = $1 WHERE id = $2', [cor, req.params.id]); res.json({ success: true });