This commit is contained in:
Leonardo
2026-03-06 06:37:13 -03:00
parent d58dc21297
commit f733db8436
146 changed files with 43436 additions and 12779 deletions

View File

@@ -9,10 +9,13 @@ import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
import { buildUpgradeUrl } from '@/utils/upgradeContext'
import { useMenuStore } from '@/stores/menuStore'
import { getMenuByRole } from '@/navigation'
import { sessionUser, sessionReady, sessionRefreshing, initSession } from '@/app/session'
// ✅ separa RBAC (papel) vs Plano (upgrade)
import { denyByRole, denyByPlan } from '@/router/accessRedirects'
import { denyByRole, denyByPlan } from '@/router/accessRedirects' // (denyByPlan pode ficar, mesmo que não use aqui)
// cache simples (evita bater no banco em toda navegação)
let sessionUidCache = null
@@ -38,12 +41,45 @@ function isUuid (v) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(v || ''))
}
/**
* ✅ Normaliza roles (aliases) para RBAC.
*
* tenant_admin / admin + kind = 'therapist' → 'therapist'
* tenant_admin / admin + kind = clinic_* → 'clinic_admin'
* tenant_admin / admin + kind desconhecido → 'clinic_admin' (legado)
* qualquer outro role → pass-through
*/
function normalizeRole (role, kind) {
const r = String(role || '').trim()
if (!r) return ''
const isAdmin = (r === 'tenant_admin' || r === 'admin')
if (isAdmin) {
const k = String(kind || '').trim()
if (k === 'therapist' || k === 'saas') return 'therapist'
if (k === 'supervisor') return 'supervisor'
return 'clinic_admin'
}
if (r === 'clinic_admin') return 'clinic_admin'
// demais
return r
}
function roleToPath (role) {
// ✅ clínica: aceita nomes canônicos e legado
if (role === 'clinic_admin' || role === 'tenant_admin' || role === 'admin') return '/admin'
if (role === 'therapist') return '/therapist'
// ✅ supervisor (papel de tenant)
if (role === 'supervisor') return '/supervisor'
// ⚠️ legado (se ainda existir em algum lugar)
if (role === 'patient') return '/portal'
if (role === 'portal_user') return '/portal'
// ✅ saas master
if (role === 'saas_admin') return '/saas'
@@ -70,19 +106,22 @@ async function waitSessionIfRefreshing () {
async function isSaasAdmin (uid) {
if (!uid) return false
if (saasAdminCacheUid === uid && typeof saasAdminCacheIsAdmin === 'boolean') {
return saasAdminCacheIsAdmin
}
const { data, error } = await supabase
.from('saas_admins')
.select('user_id')
.eq('user_id', uid)
.maybeSingle()
.from('profiles')
.select('role')
.eq('id', uid)
.single()
const ok = !error && data?.role === 'saas_admin'
const ok = !error && !!data
saasAdminCacheUid = uid
saasAdminCacheIsAdmin = ok
return ok
}
@@ -115,10 +154,121 @@ async function loadEntitlementsSafe (ent, tenantId, force) {
}
}
// util: roles guard (plural)
/**
* wrapper: tenant features store pode não aceitar force:false (ou pode falhar silenciosamente)
* -> tenta sem forçar e, se der ruim, tenta force:true.
*/
async function fetchTenantFeaturesSafe (tf, tenantId) {
if (!tf?.fetchForTenant) return
try {
await tf.fetchForTenant(tenantId, { force: false })
} catch (e) {
console.warn('[guards] tf.fetchForTenant(force:false) falhou, tentando force:true', e)
await tf.fetchForTenant(tenantId, { force: true })
}
}
// util: roles guard (plural) com aliases
function matchesRoles (roles, activeRole) {
if (!Array.isArray(roles) || !roles.length) return true
return roles.includes(activeRole)
const ar = normalizeRole(activeRole)
const wanted = roles.map(normalizeRole)
return wanted.includes(ar)
}
// ======================================================
// ✅ MENU: monta 1x por contexto (sem flicker)
// - O AppMenu lê menuStore.model e não recalcula.
// ======================================================
async function ensureMenuBuilt ({ uid, tenantId, tenantRole, globalRole }) {
try {
const menuStore = useMenuStore()
const isSaas = (globalRole === 'saas_admin')
const roleForMenu = isSaas ? 'saas_admin' : normalizeRole(tenantRole)
// ✅ FIX: inclui o role normalizado E o tenantId no key de forma explícita
// O bug era: em alguns fluxos tenantRole chegava vazio/antigo antes de
// setActiveTenant() ser chamado, fazendo o key bater com o menu errado.
const safeRole = roleForMenu || 'unknown'
const safeTenant = tenantId || 'no-tenant'
const safeGlobal = globalRole || 'no-global'
const key = `${uid}:${safeTenant}:${safeRole}:${safeGlobal}`
// ✅ FIX PRINCIPAL: só considera cache válido se role E tenant baterem.
// Antes, o check era feito antes de garantir que tenant.activeRole
// já tinha sido resolvido corretamente nessa navegação.
if (menuStore.ready && menuStore.key === key && Array.isArray(menuStore.model) && menuStore.model.length > 0) {
// sanity check extra: verifica se o modelo tem itens do role correto
// (evita falso positivo quando key colide por acidente)
const firstLabel = menuStore.model?.[0]?.label || ''
const isClinicMenu = firstLabel === 'Clínica'
const isTherapistMenu = firstLabel === 'Terapeuta'
const isSupervisorMenu = firstLabel === 'Supervisão'
const isEditorMenu = firstLabel === 'Editor'
const isPortalMenu = firstLabel === 'Paciente'
const isSaasMenuCached = firstLabel === 'SaaS'
const expectClinic = safeRole === 'clinic_admin'
const expectTherapist = safeRole === 'therapist'
const expectSupervisor = safeRole === 'supervisor'
const expectEditor = safeRole === 'editor'
const expectPortal = safeRole === 'patient'
const expectSaas = safeRole === 'saas_admin'
const menuMatchesRole =
(expectClinic && isClinicMenu) ||
(expectTherapist && isTherapistMenu) ||
(expectSupervisor && isSupervisorMenu) ||
(expectEditor && isEditorMenu) ||
(expectPortal && isPortalMenu) ||
(expectSaas && isSaasMenuCached) ||
// roles desconhecidos: aceita o cache (coreMenu)
(!expectClinic && !expectTherapist && !expectSupervisor && !expectEditor && !expectPortal && !expectSaas)
if (menuMatchesRole) {
return // cache válido e menu correto
}
// cache com key igual mas menu errado: força rebuild
console.warn('[ensureMenuBuilt] key match mas menu incompatível com role, forçando rebuild:', {
key, safeRole, firstLabel
})
menuStore.reset()
}
// garante tenant_features pronto ANTES de construir
if (!isSaas && tenantId) {
const tfm = useTenantFeaturesStore()
const hasAny = tfm?.features && typeof tfm.features === 'object' && Object.keys(tfm.features).length > 0
const loadedFor = tfm?.loadedForTenantId || null
if (!hasAny || (loadedFor && loadedFor !== tenantId)) {
await fetchTenantFeaturesSafe(tfm, tenantId)
} else if (!loadedFor) {
await fetchTenantFeaturesSafe(tfm, tenantId)
}
}
const tfm2 = useTenantFeaturesStore()
const ctx = {
isSaasAdmin: isSaas,
tenantLoading: () => false,
tenantFeaturesLoading: () => false,
tenantFeatureEnabled: (featureKey) => {
if (!tenantId) return false
try { return !!tfm2.isEnabled(featureKey, tenantId) } catch { return false }
},
role: () => normalizeRole(tenantRole)
}
const model = getMenuByRole(roleForMenu, ctx) || []
menuStore.setMenu(key, model)
} catch (e) {
console.warn('[guards] ensureMenuBuilt failed:', e)
}
}
export function applyGuards (router) {
@@ -165,20 +315,96 @@ export function applyGuards (router) {
return { path: '/auth/login' }
}
const isTenantArea =
to.path.startsWith('/admin') ||
to.path.startsWith('/therapist') ||
to.path.startsWith('/supervisor')
// ======================================
// ✅ IDENTIDADE GLOBAL (1x por navegação)
// - se falhar, NÃO nega por engano: volta pro login (seguro)
// ======================================
const { data: prof, error: profErr } = await supabase
.from('profiles')
.select('role')
.eq('id', uid)
.single()
const globalRole = !profErr ? prof?.role : null
console.timeLog(tlabel, 'profiles.role =', globalRole)
if (!globalRole) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
return { path: '/auth/login' }
}
// ======================================
// ✅ TRAVA GLOBAL: portal_user não entra em tenant-app
// ======================================
if (isTenantArea && globalRole === 'portal_user') {
// limpa lixo de tenant herdado
try {
localStorage.removeItem('tenant_id')
localStorage.removeItem('tenant')
localStorage.removeItem('currentTenantId')
} catch (_) {}
console.timeEnd(tlabel)
return { path: '/portal' }
}
// ======================================
// ✅ Portal (identidade global) via meta.profileRole
// ======================================
if (to.meta?.profileRole) {
if (globalRole !== to.meta.profileRole) {
console.timeEnd(tlabel)
return { path: '/pages/access' }
}
// monta menu do portal (patient) antes de liberar
await ensureMenuBuilt({
uid,
tenantId: null,
tenantRole: globalRole, // ex.: 'portal_user'
globalRole
})
console.timeEnd(tlabel)
return true
}
// ======================================
// ✅ ÁREA GLOBAL (não-tenant)
// - /account/* é perfil/config do usuário
// - NÃO pode carregar tenantStore nem trocar contexto de tenant
// ======================================
const isAccountArea = (to.path === '/account' || to.path.startsWith('/account/'))
if (isAccountArea) {
console.timeEnd(tlabel)
return true
}
// (opcional, mas recomendado)
// se não é tenant_member, evita carregar tenant/entitlements sem necessidade
if (globalRole && globalRole !== 'tenant_member') {
try {
localStorage.removeItem('tenant_id')
localStorage.removeItem('tenant')
localStorage.removeItem('currentTenantId')
} catch (_) {}
}
// ==========================================
// ✅ Pending invite (Modelo B): se o usuário logou e tem token pendente,
// redireciona para /accept-invite antes de qualquer load pesado.
// ✅ Pending invite (Modelo B)
// ==========================================
const pendingInviteToken = readPendingInviteToken()
// Se tiver lixo no storage, limpa para não “travar” o app.
if (pendingInviteToken && !isUuid(pendingInviteToken)) {
clearPendingInviteToken()
}
// Evita loop/efeito colateral:
// - não interfere se já está em /accept-invite
// (não precisamos checar /auth aqui porque /auth já retornou lá em cima)
if (
pendingInviteToken &&
isUuid(pendingInviteToken) &&
@@ -199,6 +425,11 @@ export function applyGuards (router) {
const tf0 = useTenantFeaturesStore()
if (typeof tf0.invalidate === 'function') tf0.invalidate()
try {
const menuStore = useMenuStore()
if (typeof menuStore.reset === 'function') menuStore.reset()
} catch {}
}
// ================================
@@ -206,13 +437,103 @@ export function applyGuards (router) {
// ================================
if (to.meta?.saasAdmin) {
console.timeLog(tlabel, 'isSaasAdmin')
const ok = await isSaasAdmin(uid)
// usa identidade global primeiro (evita cache fantasma)
const ok = (globalRole === 'saas_admin') ? true : await isSaasAdmin(uid)
if (!ok) { console.timeEnd(tlabel); return { path: '/pages/access' } }
// ✅ monta menu SaaS 1x (AppMenu lê do menuStore)
await ensureMenuBuilt({
uid,
tenantId: null,
tenantRole: 'saas_admin',
globalRole
})
console.timeEnd(tlabel)
return true
}
// ================================
// ✅ ÁREA DO EDITOR (papel de plataforma)
// Verificado por platform_roles[] em profiles, não por tenant.
// ⚠️ Requer migration: ALTER TABLE profiles ADD COLUMN platform_roles text[] DEFAULT '{}'
// ================================
if (to.meta?.editorArea) {
let platformRoles = []
try {
const { data: pRoles } = await supabase
.from('profiles')
.select('platform_roles')
.eq('id', uid)
.single()
platformRoles = Array.isArray(pRoles?.platform_roles) ? pRoles.platform_roles : []
} catch {
// coluna ainda não existe: acesso negado por padrão
}
if (!platformRoles.includes('editor')) {
console.timeEnd(tlabel)
return { path: '/pages/access' }
}
await ensureMenuBuilt({
uid,
tenantId: null,
tenantRole: 'editor',
globalRole
})
console.timeEnd(tlabel)
return true
}
// ================================
// 🚫 SaaS master: bloqueia tenant-app por padrão
// ✅ Mas libera rotas de DEMO em DEV (Sakai)
// ================================
console.timeLog(tlabel, 'saas.lockdown?')
// 🔥 PATCH: aqui NÃO consulta isSaasAdmin — usa só identidade global
const isSaas = (globalRole === 'saas_admin')
if (isSaas) {
const isSaasArea = to.path === '/saas' || to.path.startsWith('/saas/')
// Rotas do Sakai Demo (no seu caso ficam em /demo/*)
const isDemoArea = import.meta.env.DEV && (
to.path === '/demo' ||
to.path.startsWith('/demo/')
)
// Se for demo em DEV, libera
if (isDemoArea) {
// ✅ ainda assim monta menu SaaS (pra layout não piscar)
await ensureMenuBuilt({
uid,
tenantId: null,
tenantRole: 'saas_admin',
globalRole
})
console.timeEnd(tlabel)
return true
}
// Fora de /saas (e não-demo), não pode
if (!isSaasArea) {
console.timeEnd(tlabel)
return { path: '/saas' }
}
// ✅ estamos no /saas: monta menu SaaS
await ensureMenuBuilt({
uid,
tenantId: null,
tenantRole: 'saas_admin',
globalRole
})
}
// ================================
// ✅ Abaixo daqui é tudo tenant-app
// ================================
@@ -232,21 +553,33 @@ export function applyGuards (router) {
}
// se não tem tenant ativo:
// - se não tem memberships active -> manda pro access (sem clínica)
// - se tem memberships active mas activeTenantId está null -> seta e segue
if (!tenant.activeTenantId) {
const mem = Array.isArray(tenant.memberships) ? tenant.memberships : []
// 1) tenta casar role da rota (ex.: therapist) com membership
const wantedRoles = Array.isArray(to.meta?.roles) ? to.meta.roles : []
const preferred = wantedRoles.length
? mem.find(m => m && m.status === 'active' && m.tenant_id && wantedRoles.includes(m.role))
const wantedNorm = wantedRoles.map(normalizeRole)
const preferred = wantedNorm.length
? mem.find(m =>
m &&
m.status === 'active' &&
m.tenant_id &&
wantedNorm.includes(normalizeRole(m.role, m.kind))
)
: null
// 2) fallback: primeiro active
const firstActive = preferred || mem.find(m => m && m.status === 'active' && m.tenant_id)
if (!firstActive) {
// 🔥 race/sem vínculo: em área tenant, trate como login necessário (não AccessDenied)
if (isTenantArea) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
return { path: '/auth/login' }
}
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
return { path: '/pages/access' }
@@ -260,17 +593,90 @@ export function applyGuards (router) {
}
}
const tenantId = tenant.activeTenantId
// 🔥 FIX: se ainda assim não resolveu tenant/role e estamos em tenant area, não negue “por engano”
if (isTenantArea && (!tenant.activeTenantId || !tenant.activeRole)) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
return { path: '/auth/login' }
}
let tenantId = tenant.activeTenantId
if (!tenantId) {
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
return { path: '/pages/access' }
}
// =====================================================
// ✅ tenantScope baseado em tenants.kind (fonte da verdade)
// =====================================================
const scope = to.meta?.tenantScope // 'personal' | 'clinic'
if (scope) {
const mem = Array.isArray(tenant.memberships) ? tenant.memberships : []
// seleciona membership ativa cujo kind corresponde ao escopo
const desired = mem.find(m =>
m &&
m.status === 'active' &&
m.tenant_id &&
(
(scope === 'personal' && m.kind === 'saas') ||
(scope === 'clinic' && m.kind === 'clinic') ||
(scope === 'supervisor' && m.kind === 'supervisor')
)
)
const desiredTenantId = desired?.tenant_id || null
if (desiredTenantId && tenant.activeTenantId !== desiredTenantId) {
console.timeLog(tlabel, `tenantScope.switch(${scope})`)
// ✅ guarda o tenant antigo para invalidar APENAS ele
const oldTenantId = tenant.activeTenantId
if (typeof tenant.setActiveTenant === 'function') {
tenant.setActiveTenant(desiredTenantId)
} else {
tenant.activeTenantId = desiredTenantId
}
localStorage.setItem('tenant_id', desiredTenantId)
tenantId = desiredTenantId
try {
const entX = useEntitlementsStore()
if (typeof entX.invalidate === 'function') entX.invalidate()
} catch {}
// ✅ NÃO invalidar o cache inteiro — invalida somente o tenant anterior
try {
const tfX = useTenantFeaturesStore()
if (typeof tfX.invalidate === 'function' && oldTenantId) tfX.invalidate(oldTenantId)
} catch {}
// ✅ troca tenant => menu precisa recompôr (contexto mudou)
try {
const menuStore = useMenuStore()
if (typeof menuStore.reset === 'function') menuStore.reset()
} catch {}
} else if (!desiredTenantId) {
console.warn('[guards] tenantScope sem match:', scope, {
memberships: mem.map(x => ({
tenant_id: x?.tenant_id,
role: x?.role,
kind: x?.kind,
status: x?.status
}))
})
}
}
// se trocar tenant, invalida cache de tenant_features (evita ler feature de tenant errado)
const tfSwitch = useTenantFeaturesStore()
if (tfSwitch.loadedForTenantId && tfSwitch.loadedForTenantId !== tenantId) {
tfSwitch.invalidate()
// ✅ invalida só o tenant que estava carregado antes
tfSwitch.invalidate(tfSwitch.loadedForTenantId)
}
// entitlements (✅ carrega só quando precisa)
@@ -280,6 +686,17 @@ export function applyGuards (router) {
await loadEntitlementsSafe(ent, tenantId, true)
}
// ✅ user entitlements: terapeuta pode ter assinatura pessoal (therapist_pro)
// que gera features em v_user_entitlements, não em v_tenant_entitlements.
// user entitlements: therapist e supervisor têm assinatura pessoal (v_user_entitlements)
const activeRoleNormForEnt = normalizeRole(tenant.activeRole)
if (['therapist', 'supervisor'].includes(activeRoleNormForEnt) && uid && ent.loadedForUser !== uid) {
console.timeLog(tlabel, 'ent.loadForUser')
try { await ent.loadForUser(uid) } catch (e) {
console.warn('[guards] ent.loadForUser failed:', e)
}
}
// ================================
// ✅ tenant_features (módulos ativáveis por clínica)
// meta.tenantFeature = 'patients' | ...
@@ -288,10 +705,14 @@ export function applyGuards (router) {
if (requiredTenantFeature) {
const tf = useTenantFeaturesStore()
console.timeLog(tlabel, 'tenantFeatures.fetchForTenant')
await tf.fetchForTenant(tenantId, { force: false })
await fetchTenantFeaturesSafe(tf, tenantId)
if (!tf.isEnabled(requiredTenantFeature)) {
// evita loop
// ✅ IMPORTANTÍSSIMO: passa tenantId
const enabled = typeof tf.isEnabled === 'function'
? tf.isEnabled(requiredTenantFeature, tenantId)
: false
if (!enabled) {
if (to.path === '/admin/clinic/features') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
@@ -304,65 +725,73 @@ export function applyGuards (router) {
// ------------------------------------------------
// ✅ RBAC (roles) — BLOQUEIA se não for compatível
//
// Importante:
// - Isso é "papel": se falhar, NÃO é caso de upgrade.
// - Só depois disso checamos feature/plano.
// ------------------------------------------------
const allowedRoles = Array.isArray(to.meta?.roles) ? to.meta.roles : null
if (allowedRoles && allowedRoles.length && !allowedRoles.includes(tenant.activeRole)) {
const allowedRolesRaw = Array.isArray(to.meta?.roles) ? to.meta.roles : null
const allowedRoles = allowedRolesRaw && allowedRolesRaw.length
? allowedRolesRaw.map(normalizeRole)
: null
const activeRoleNorm = normalizeRole(tenant.activeRole)
if (allowedRoles && allowedRoles.length && !allowedRoles.includes(activeRoleNorm)) {
const mem = Array.isArray(tenant.memberships) ? tenant.memberships : []
const compatible = mem.find(m =>
m && m.status === 'active' && m.tenant_id === tenantId && allowedRoles.includes(m.role)
m &&
m.status === 'active' &&
m.tenant_id === tenantId &&
allowedRoles.includes(normalizeRole(m.role, m.kind))
)
if (compatible) {
// muda role ativo para o compatível (mesmo tenant)
tenant.activeRole = compatible.role
tenant.activeRole = normalizeRole(compatible.role, compatible.kind)
} else {
// 🔥 aqui era o "furo": antes ajustava se achasse, mas se não achasse, deixava passar.
console.timeEnd(tlabel)
return denyByRole({ to, currentRole: tenant.activeRole })
}
}
// role guard (singular) - mantém compatibilidade
const requiredRole = to.meta?.role
if (requiredRole && tenant.activeRole !== requiredRole) {
// RBAC singular também é "papel" → cai fora (não é upgrade)
// role guard (singular)
const requiredRoleRaw = to.meta?.role
const requiredRole = requiredRoleRaw ? normalizeRole(requiredRoleRaw) : null
if (requiredRole && normalizeRole(tenant.activeRole) !== requiredRole) {
console.timeEnd(tlabel)
return denyByRole({ to, currentRole: tenant.activeRole })
}
// ------------------------------------------------
// ✅ feature guard (entitlements/plano → upgrade)
//
// Aqui sim é caso de upgrade:
// - o usuário "poderia" usar, mas o plano do tenant não liberou.
// ------------------------------------------------
const requiredFeature = to.meta?.feature
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
// evita loop
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
// Mantém compatibilidade com seu fluxo existente (buildUpgradeUrl)
const url = buildUpgradeUrl({
missingKeys: [requiredFeature],
redirectTo: to.fullPath
})
missingKeys: [requiredFeature],
redirectTo: to.fullPath,
role: normalizeRole(tenant.activeRole) // ✅ passa o role para a UpgradePage detectar
})
// Se quiser padronizar no futuro, você pode trocar por:
// return denyByPlan({ to, missingFeature: requiredFeature, redirectTo: to.fullPath })
console.timeEnd(tlabel)
return url
}
// ======================================================
// ✅ MENU: monta 1x por contexto APÓS estabilizar tenant+role
// ======================================================
await ensureMenuBuilt({
uid,
tenantId,
tenantRole: tenant.activeRole,
globalRole
})
console.timeEnd(tlabel)
return true
} catch (e) {
console.error('[guards] erro no beforeEach:', e)
// fallback seguro
if (to.path.startsWith('/auth')) return true
if (to.meta?.public) return true
if (to.path === '/pages/access') return true
@@ -372,19 +801,87 @@ export function applyGuards (router) {
}
})
// auth listener (reset caches)
// auth listener (reset caches) — ✅ agora com filtro de evento
if (!window.__supabaseAuthListenerBound) {
window.__supabaseAuthListenerBound = true
supabase.auth.onAuthStateChange(() => {
sessionUidCache = null
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
supabase.auth.onAuthStateChange((event, sess) => {
// ⚠️ NÃO derrubar caches em token refresh / eventos redundantes.
const uid = sess?.user?.id || null
try {
const tf = useTenantFeaturesStore()
if (typeof tf.invalidate === 'function') tf.invalidate()
} catch {}
// ✅ SIGNED_OUT: aqui sim zera tudo
if (event === 'SIGNED_OUT') {
sessionUidCache = null
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
// ✅ FIX: limpa o localStorage de tenant na saída
// Sem isso, o próximo login restaura o tenant do usuário anterior.
try {
localStorage.removeItem('tenant_id')
localStorage.removeItem('tenant')
localStorage.removeItem('currentTenantId')
} catch (_) {}
try {
const tf = useTenantFeaturesStore()
if (typeof tf.invalidate === 'function') tf.invalidate()
} catch {}
try {
const ent = useEntitlementsStore()
if (typeof ent.invalidate === 'function') ent.invalidate()
} catch {}
try {
const tenant = useTenantStore()
if (typeof tenant.reset === 'function') tenant.reset()
} catch {}
try {
const menuStore = useMenuStore()
if (typeof menuStore.reset === 'function') menuStore.reset()
} catch {}
return
}
// ✅ TOKEN_REFRESHED: NÃO invalida nada (é o caso clássico de trocar de aba)
if (event === 'TOKEN_REFRESHED') return
// ✅ SIGNED_IN / USER_UPDATED:
// só invalida se o usuário mudou de verdade
if (event === 'SIGNED_IN' || event === 'USER_UPDATED') {
if (uid && sessionUidCache && sessionUidCache === uid) {
// mesmo usuário -> não derruba caches
return
}
// user mudou (ou cache vazio) -> invalida dependências
sessionUidCache = uid || null
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
try {
const tf = useTenantFeaturesStore()
if (typeof tf.invalidate === 'function') tf.invalidate()
} catch {}
try {
const ent = useEntitlementsStore()
if (typeof ent.invalidate === 'function') ent.invalidate()
} catch {}
try {
const menuStore = useMenuStore()
if (typeof menuStore.reset === 'function') menuStore.reset()
} catch {}
// tenantStore carrega de novo no fluxo do guard quando precisar
return
}
// default: não faz nada
})
}
}