Compare commits

..

3 Commits

Author SHA1 Message Date
Cauê Faleiros
1d49161a05 fix: block race condition causing logout during impersonation handoff
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m31s
- Introduced isReloadingForImpersonation flag to temporarily disable the logout function while tokens are being swapped before the hard reload.
2026-03-11 15:49:03 -03:00
Cauê Faleiros
bf157687d4 fix: resolve race conditions during impersonation handoff by reloading directly from dataService 2026-03-11 15:33:38 -03:00
Cauê Faleiros
7cb78f13c0 fix: pad base64 string when parsing jwt during impersonation exit
- Prevented the browser's atob() function from throwing a 'String contains an invalid character' exception by adding proper Base64 padding to the JWT payload before decoding.
2026-03-11 15:13:19 -03:00
3 changed files with 30 additions and 8 deletions

View File

@@ -216,10 +216,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
{localStorage.getItem('ctms_super_admin_token') && ( {localStorage.getItem('ctms_super_admin_token') && (
<button <button
onClick={() => { onClick={() => {
if (returnToSuperAdmin()) { returnToSuperAdmin();
window.location.hash = '#/super-admin';
window.location.reload();
}
}} }}
className="w-full flex items-center justify-center gap-2 py-2 px-3 bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 rounded-lg text-xs font-bold hover:opacity-90 transition-colors" className="w-full flex items-center justify-center gap-2 py-2 px-3 bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 rounded-lg text-xs font-bold hover:opacity-90 transition-colors"
> >

View File

@@ -100,9 +100,6 @@ export const SuperAdmin: React.FC = () => {
return; return;
} }
await impersonateTenant(tenantId); await impersonateTenant(tenantId);
// Force a full reload to clear any cached context/state in the React app
window.location.hash = '#/';
window.location.reload();
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Erro ao tentar entrar na organização.'); alert(err.message || 'Erro ao tentar entrar na organização.');
} }

View File

@@ -357,7 +357,11 @@ export const deleteTenant = async (id: string): Promise<boolean> => {
// --- Auth Functions --- // --- Auth Functions ---
// Flag to prevent background fetches from throwing 401 and logging out during impersonation handoffs
export let isReloadingForImpersonation = false;
export const logout = () => { export const logout = () => {
if (isReloadingForImpersonation) return; // Prevent logout if we are just switching tokens
localStorage.removeItem('ctms_token'); localStorage.removeItem('ctms_token');
localStorage.removeItem('ctms_user_id'); localStorage.removeItem('ctms_user_id');
localStorage.removeItem('ctms_tenant_id'); localStorage.removeItem('ctms_tenant_id');
@@ -401,6 +405,8 @@ export const impersonateTenant = async (tenantId: string): Promise<any> => {
throw new Error(errorData.error || 'Erro ao assumir identidade'); throw new Error(errorData.error || 'Erro ao assumir identidade');
} }
isReloadingForImpersonation = true; // Block logouts
const data = await response.json(); const data = await response.json();
const oldToken = localStorage.getItem('ctms_token'); const oldToken = localStorage.getItem('ctms_token');
if (oldToken) { if (oldToken) {
@@ -410,6 +416,10 @@ export const impersonateTenant = async (tenantId: string): Promise<any> => {
localStorage.setItem('ctms_token', data.token); localStorage.setItem('ctms_token', data.token);
localStorage.setItem('ctms_user_id', data.user.id); localStorage.setItem('ctms_user_id', data.user.id);
localStorage.setItem('ctms_tenant_id', data.user.tenant_id || ''); localStorage.setItem('ctms_tenant_id', data.user.tenant_id || '');
window.location.hash = '#/';
window.location.reload();
return data; return data;
}; };
@@ -417,13 +427,31 @@ export const returnToSuperAdmin = (): boolean => {
const superAdminToken = localStorage.getItem('ctms_super_admin_token'); const superAdminToken = localStorage.getItem('ctms_super_admin_token');
if (superAdminToken) { if (superAdminToken) {
try { try {
const payload = JSON.parse(atob(superAdminToken.split('.')[1])); isReloadingForImpersonation = true; // Block logouts
// Correctly decode Base64Url JWT payload with proper padding
const base64Url = superAdminToken.split('.')[1];
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64.length % 4;
if (pad) {
base64 += '='.repeat(4 - pad);
}
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const payload = JSON.parse(jsonPayload);
localStorage.setItem('ctms_token', superAdminToken); localStorage.setItem('ctms_token', superAdminToken);
localStorage.setItem('ctms_user_id', payload.id); localStorage.setItem('ctms_user_id', payload.id);
localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system'); localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system');
localStorage.removeItem('ctms_super_admin_token'); localStorage.removeItem('ctms_super_admin_token');
window.location.hash = '#/super-admin';
window.location.reload();
return true; return true;
} catch (e) { } catch (e) {
isReloadingForImpersonation = false;
console.error("Failed to restore super admin token", e); console.error("Failed to restore super admin token", e);
return false; return false;
} }