From d5b57835a7f01496cb3835a1bbcbb26b09cf4613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 4 Mar 2026 11:36:47 -0300 Subject: [PATCH] fix: resolve super_admin privileges and tenant management issues - Fixed real backend deletion for tenants - Allowed super_admins to manage other super_admins in Global Users - Filtered teams based on selected tenant in user creation - Protected system tenant from deletion --- backend/index.js | 100 +++++++++++++++++++++++++++++++++++++-- components/Layout.tsx | 1 + pages/SuperAdmin.tsx | 15 ++++-- pages/TeamManagement.tsx | 61 ++++++++++++++++++++---- services/dataService.ts | 27 +++++++++++ 5 files changed, 187 insertions(+), 17 deletions(-) diff --git a/backend/index.js b/backend/index.js index 37a400c..1e5932f 100644 --- a/backend/index.js +++ b/backend/index.js @@ -457,7 +457,14 @@ apiRouter.get('/teams', async (req, res) => { try { const { tenantId } = req.query; const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - const [rows] = await pool.query('SELECT * FROM teams WHERE tenant_id = ?', [effectiveTenantId]); + + let q = 'SELECT * FROM teams'; + const params = []; + if (effectiveTenantId && effectiveTenantId !== 'all') { + q += ' WHERE tenant_id = ?'; + params.push(effectiveTenantId); + } + const [rows] = await pool.query(q, params); res.json(rows); } catch (error) { res.status(500).json({ error: error.message }); @@ -509,13 +516,49 @@ apiRouter.post('/tenants', requireRole(['super_admin']), async (req, res) => { await connection.beginTransaction(); const tid = `tenant_${crypto.randomUUID().split('-')[0]}`; await connection.query('INSERT INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', [tid, name, slug, admin_email, status || 'active']); - const uid = `u_${crypto.randomUUID().split('-')[0]}`; - await connection.query('INSERT INTO users (id, tenant_id, name, email, role) VALUES (?, ?, ?, ?, ?)', [uid, tid, 'Admin', admin_email, 'admin']); + + // Check if user already exists + const [existingUser] = await connection.query('SELECT id FROM users WHERE email = ?', [admin_email]); + if (existingUser.length === 0) { + const uid = `u_${crypto.randomUUID().split('-')[0]}`; + const placeholderHash = 'pending_setup'; + await connection.query('INSERT INTO users (id, tenant_id, name, email, password_hash, role) VALUES (?, ?, ?, ?, ?, ?)', [uid, tid, 'Admin', admin_email, placeholderHash, 'admin']); + + const token = crypto.randomBytes(32).toString('hex'); + await connection.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 24 HOUR))', [admin_email, token]); + + const setupLink = `${process.env.APP_URL || 'http://localhost:3001'}/#/reset-password?token=${token}`; + await transporter.sendMail({ + from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`, + to: admin_email, + subject: 'Bem-vindo ao Fasto - Crie sua senha', + html: `

VocĆŖ foi convidado para ser Admin. Crie sua senha aqui.

` + }).catch(err => console.error("Email failed:", err)); + } + await connection.commit(); res.status(201).json({ id: tid }); } catch (error) { await connection.rollback(); res.status(500).json({ error: error.message }); } finally { connection.release(); } }); +apiRouter.put('/tenants/:id', requireRole(['super_admin']), async (req, res) => { + const { name, slug, admin_email, status } = req.body; + try { + await pool.query( + 'UPDATE tenants SET name = ?, slug = ?, admin_email = ?, status = ? WHERE id = ?', + [name, slug || null, admin_email, status, req.params.id] + ); + res.json({ message: 'Tenant updated successfully.' }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +apiRouter.delete('/tenants/:id', requireRole(['super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM tenants WHERE id = ?', [req.params.id]); + res.json({ message: 'Tenant deleted successfully.' }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + // Mount the API Router app.use('/api', apiRouter); @@ -529,6 +572,55 @@ if (process.env.NODE_ENV === 'production') { }); } -app.listen(PORT, () => { +// Auto-provision Super Admin +const provisionSuperAdmin = async () => { + const email = 'suporte@blyzer.com.br'; + try { + // Ensure system tenant exists + await pool.query('INSERT IGNORE INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', ['system', 'System Admin', 'system', email, 'active']); + + const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]); + if (existing.length === 0) { + console.log('Provisioning default super_admin...'); + const uid = `u_${crypto.randomUUID().split('-')[0]}`; + const placeholderHash = 'pending_setup'; + + await pool.query( + 'INSERT INTO users (id, tenant_id, name, email, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?)', + [uid, 'system', 'Blyzer Suporte', email, placeholderHash, 'super_admin', 'active'] + ); + + const token = crypto.randomBytes(32).toString('hex'); + await pool.query( + 'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 24 HOUR))', + [email, token] + ); + + const setupLink = `${process.env.APP_URL || 'http://localhost:3001'}/#/reset-password?token=${token}`; + console.log(`\n\n=== SUPER ADMIN SETUP LINK ===\n${setupLink}\n==============================\n\n`); + + await transporter.sendMail({ + from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`, + to: email, + subject: 'Conta Super Admin Criada - Fasto', + html: ` +
+

Conta Super Admin Gerada

+

Sua conta de suporte (super_admin) foi criada no Fasto.

+
+ Definir Senha do Super Admin +
+

Este link expira em 24 horas.

+
+ ` + }).catch(err => console.error("Failed to send super_admin email:", err)); + } + } catch (error) { + console.error('Failed to provision super_admin:', error); + } +}; + +app.listen(PORT, async () => { + await provisionSuperAdmin(); console.log(`šŸš€ Servidor Backend rodando em http://localhost:${PORT}`); }); diff --git a/components/Layout.tsx b/components/Layout.tsx index 74f91bf..2a92708 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -124,6 +124,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => Super Admin + )} diff --git a/pages/SuperAdmin.tsx b/pages/SuperAdmin.tsx index 450fdec..0cd5c34 100644 --- a/pages/SuperAdmin.tsx +++ b/pages/SuperAdmin.tsx @@ -3,7 +3,7 @@ import { Building2, Users, MessageSquare, Plus, Search, Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X } from 'lucide-react'; -import { getTenants, createTenant } from '../services/dataService'; +import { getTenants, createTenant, deleteTenant } from '../services/dataService'; import { Tenant } from '../types'; import { DateRangePicker } from '../components/DateRangePicker'; import { KPICard } from '../components/KPICard'; @@ -76,9 +76,18 @@ export const SuperAdmin: React.FC = () => { setIsModalOpen(true); }; - const handleDelete = (id: string) => { + const handleDelete = async (id: string) => { + if (id === 'system') { + alert('A organização do sistema nĆ£o pode ser excluĆ­da.'); + return; + } if (confirm('Tem certeza que deseja excluir esta organização? Esta ação nĆ£o pode ser desfeita.')) { - setTenants(prev => prev.filter(t => t.id !== id)); + const success = await deleteTenant(id); + if (success) { + setTenants(prev => prev.filter(t => t.id !== id)); + } else { + alert('Erro ao excluir a organização do servidor.'); + } } }; diff --git a/pages/TeamManagement.tsx b/pages/TeamManagement.tsx index 00d5bdb..1689302 100644 --- a/pages/TeamManagement.tsx +++ b/pages/TeamManagement.tsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from 'react'; import { Users, Plus, Mail, Search, X, Edit, Trash2, Loader2, AlertTriangle } from 'lucide-react'; -import { getUsers, getTeams, createMember, deleteUser, updateUser, getUserById } from '../services/dataService'; -import { User } from '../types'; +import { getUsers, getTeams, createMember, deleteUser, updateUser, getUserById, getTenants } from '../services/dataService'; +import { User, Tenant } from '../types'; export const TeamManagement: React.FC = () => { const [users, setUsers] = useState([]); const [teams, setTeams] = useState([]); + const [tenants, setTenants] = useState([]); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); @@ -20,7 +21,8 @@ export const TeamManagement: React.FC = () => { email: '', role: 'agent' as any, team_id: '', - status: 'active' as any + status: 'active' as any, + tenant_id: '' }); const loadData = async () => { @@ -29,9 +31,28 @@ export const TeamManagement: React.FC = () => { if (!tid) return; setLoading(true); try { - const [fu, ft, me] = await Promise.all([getUsers(tid), getTeams(tid), uid ? getUserById(uid) : null]); - setUsers(fu.filter(u => u.role !== 'super_admin')); - setTeams(ft); + const me = uid ? await getUserById(uid) : null; + + const isSuperAdmin = me?.role === 'super_admin'; + const effectiveTid = isSuperAdmin ? 'all' : tid; + + const promises: Promise[] = [ + getUsers(effectiveTid), + getTeams(effectiveTid) + ]; + + if (isSuperAdmin) { + promises.push(getTenants()); + } + + const results = await Promise.all(promises); + + setUsers(isSuperAdmin ? results[0] : results[0].filter((u: User) => u.role !== 'super_admin')); + setTeams(results[1]); + if (isSuperAdmin && results[2]) { + setTenants(results[2]); + } + if (me) setCurrentUser(me); } catch (err) { console.error(err); } finally { setLoading(false); } }; @@ -43,11 +64,13 @@ export const TeamManagement: React.FC = () => { setIsSaving(true); try { const tid = localStorage.getItem('ctms_tenant_id') || ''; + const finalTenantId = currentUser?.role === 'super_admin' ? formData.tenant_id : tid; + if (editingUser) { - const success = await updateUser(editingUser.id, formData); + const success = await updateUser(editingUser.id, { ...formData, tenant_id: finalTenantId }); if (success) { setIsModalOpen(false); loadData(); } } else { - await createMember({ ...formData, tenant_id: tid }); + await createMember({ ...formData, tenant_id: finalTenantId }); setIsModalOpen(false); loadData(); } @@ -72,6 +95,7 @@ export const TeamManagement: React.FC = () => { const filtered = users.filter(u => u.name.toLowerCase().includes(searchTerm.toLowerCase()) || u.email.toLowerCase().includes(searchTerm.toLowerCase())); const getRoleLabel = (role: string) => { + if (role === 'super_admin') return 'Super Admin'; if (role === 'admin') return 'Admin'; if (role === 'manager') return 'Gerente'; return 'Agente'; @@ -111,6 +135,7 @@ export const TeamManagement: React.FC = () => { UsuĆ”rio + {currentUser?.role === 'super_admin' && Organização} Função Time Status @@ -138,6 +163,13 @@ export const TeamManagement: React.FC = () => { + {currentUser?.role === 'super_admin' && ( + + + {tenants.find(t => t.id === user.tenant_id)?.name || user.tenant_id} + + + )} {getRoleLabel(user.role)} @@ -148,7 +180,7 @@ export const TeamManagement: React.FC = () => { {canManage && (
- +
@@ -173,6 +205,15 @@ export const TeamManagement: React.FC = () => { setFormData({...formData, email:e.target.value})} className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-dark-text disabled:bg-zinc-50 dark:disabled:bg-dark-bg/50 dark:disabled:text-dark-muted" disabled={!!editingUser} required /> + {currentUser?.role === 'super_admin' && ( +
+ + +
+ )}
@@ -186,7 +227,7 @@ export const TeamManagement: React.FC = () => {
diff --git a/services/dataService.ts b/services/dataService.ts index 8407d22..0c1a9cf 100644 --- a/services/dataService.ts +++ b/services/dataService.ts @@ -248,6 +248,33 @@ export const createTenant = async (tenantData: any): Promise => { } }; +export const updateTenant = async (id: string, tenantData: any): Promise => { + try { + const response = await fetch(`${API_URL}/tenants/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(tenantData) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateTenant):", error); + return false; + } +}; + +export const deleteTenant = async (id: string): Promise => { + try { + const response = await fetch(`${API_URL}/tenants/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteTenant):", error); + return false; + } +}; + // --- Auth Functions --- export const logout = () => {