fix: include missing files for tenant impersonation feature
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m7s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m7s
- Added backend impersonate endpoint. - Added frontend impersonate button and functions. - Fixed build failure by including missing exported functions in dataService.ts.
This commit is contained in:
@@ -217,6 +217,30 @@ apiRouter.post('/auth/login', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// God Mode (Impersonate Tenant)
|
||||||
|
apiRouter.post('/impersonate/:tenantId', requireRole(['super_admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Buscar o primeiro admin (ou qualquer usuário) do tenant para assumir a identidade
|
||||||
|
const [users] = await pool.query("SELECT * FROM users WHERE tenant_id = ? AND role = 'admin' LIMIT 1", [req.params.tenantId]);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Nenhum administrador encontrado nesta organização para assumir a identidade.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = users[0];
|
||||||
|
|
||||||
|
if (user.status !== 'active') {
|
||||||
|
return res.status(403).json({ error: 'A conta do admin desta organização está inativa.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gerar um token JWT como se fôssemos o admin do tenant
|
||||||
|
const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, JWT_SECRET, { expiresIn: '2h' });
|
||||||
|
res.json({ token, user: { id: user.id, name: user.name, email: user.email, role: user.role, tenant_id: user.tenant_id, team_id: user.team_id, slug: user.slug } });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Forgot Password
|
// Forgot Password
|
||||||
apiRouter.post('/auth/forgot-password', async (req, res) => {
|
apiRouter.post('/auth/forgot-password', async (req, res) => {
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Building2, Users, MessageSquare, Plus, Search,
|
Building2, Users, MessageSquare, Plus, Search,
|
||||||
Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X, CheckCircle2, Loader2
|
Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X, CheckCircle2, Loader2, LogIn
|
||||||
} from 'lucide-react';import { getTenants, createTenant, deleteTenant, updateTenant } from '../services/dataService';
|
} from 'lucide-react';
|
||||||
|
import { getTenants, createTenant, deleteTenant, updateTenant, impersonateTenant } from '../services/dataService';
|
||||||
import { Tenant } from '../types';
|
import { Tenant } from '../types';
|
||||||
import { DateRangePicker } from '../components/DateRangePicker';
|
import { DateRangePicker } from '../components/DateRangePicker';
|
||||||
import { KPICard } from '../components/KPICard';
|
import { KPICard } from '../components/KPICard';
|
||||||
|
|
||||||
export const SuperAdmin: React.FC = () => {
|
export const SuperAdmin: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [dateRange, setDateRange] = useState({
|
const [dateRange, setDateRange] = useState({
|
||||||
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||||
end: new Date()
|
end: new Date()
|
||||||
@@ -90,6 +93,20 @@ export const SuperAdmin: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImpersonate = async (tenantId: string) => {
|
||||||
|
try {
|
||||||
|
if (tenantId === 'system') {
|
||||||
|
alert('Você já está na organização do sistema.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await impersonateTenant(tenantId);
|
||||||
|
// Force a full reload to clear any cached context/state in the React app
|
||||||
|
window.location.href = '/';
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.message || 'Erro ao tentar entrar na organização.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [successMessage, setSuccessMessage] = useState('');
|
const [successMessage, setSuccessMessage] = useState('');
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -232,8 +249,13 @@ export const SuperAdmin: React.FC = () => {
|
|||||||
<td className="px-6 py-4 text-center font-medium text-zinc-700 dark:text-zinc-300">{tenant.attendance_count?.toLocaleString()}</td>
|
<td className="px-6 py-4 text-center font-medium text-zinc-700 dark:text-zinc-300">{tenant.attendance_count?.toLocaleString()}</td>
|
||||||
<td className="px-6 py-4 text-right">
|
<td className="px-6 py-4 text-right">
|
||||||
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button onClick={() => handleEdit(tenant)} className="p-2 text-zinc-400 hover:text-brand-yellow hover:bg-zinc-50 dark:hover:bg-dark-bg rounded-lg transition-colors"><Edit size={16} /></button>
|
{tenant.id !== 'system' && (
|
||||||
<button onClick={() => handleDelete(tenant.id)} className="p-2 text-zinc-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"><Trash2 size={16} /></button>
|
<button onClick={() => handleImpersonate(tenant.id)} title="Entrar na Organização" className="p-2 text-zinc-400 hover:text-green-600 hover:bg-green-50 dark:hover:bg-green-900/30 rounded-lg transition-colors">
|
||||||
|
<LogIn size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={() => handleEdit(tenant)} title="Editar" className="p-2 text-zinc-400 hover:text-brand-yellow hover:bg-zinc-50 dark:hover:bg-dark-bg rounded-lg transition-colors"><Edit size={16} /></button>
|
||||||
|
<button onClick={() => handleDelete(tenant.id)} title="Excluir" className="p-2 text-zinc-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"><Trash2 size={16} /></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -387,6 +387,50 @@ export const login = async (credentials: any): Promise<any> => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const impersonateTenant = async (tenantId: string): Promise<any> => {
|
||||||
|
const response = await fetch(`${API_URL}/impersonate/${tenantId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
const isJson = contentType && contentType.indexOf("application/json") !== -1;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = isJson ? await response.json() : { error: 'Erro no servidor' };
|
||||||
|
throw new Error(errorData.error || 'Erro ao assumir identidade');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const oldToken = localStorage.getItem('ctms_token');
|
||||||
|
if (oldToken) {
|
||||||
|
localStorage.setItem('ctms_super_admin_token', oldToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('ctms_token', data.token);
|
||||||
|
localStorage.setItem('ctms_user_id', data.user.id);
|
||||||
|
localStorage.setItem('ctms_tenant_id', data.user.tenant_id || '');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const returnToSuperAdmin = (): boolean => {
|
||||||
|
const superAdminToken = localStorage.getItem('ctms_super_admin_token');
|
||||||
|
if (superAdminToken) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(superAdminToken.split('.')[1]));
|
||||||
|
localStorage.setItem('ctms_token', superAdminToken);
|
||||||
|
localStorage.setItem('ctms_user_id', payload.id);
|
||||||
|
localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system');
|
||||||
|
localStorage.removeItem('ctms_super_admin_token');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to restore super admin token", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const register = async (userData: any): Promise<boolean> => {
|
export const register = async (userData: any): Promise<boolean> => {
|
||||||
const response = await fetch(`${API_URL}/auth/register`, {
|
const response = await fetch(`${API_URL}/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Reference in New Issue
Block a user