Agenda, Agendador, Configurações
This commit is contained in:
@@ -8,6 +8,7 @@ import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
|
||||
import { buildUpgradeUrl } from '@/utils/upgradeContext'
|
||||
import { logGuard, logError, logPerf } from '@/support/supportLogger'
|
||||
|
||||
import { useMenuStore } from '@/stores/menuStore'
|
||||
import { getMenuByRole } from '@/navigation'
|
||||
@@ -24,6 +25,10 @@ let sessionUidCache = null
|
||||
let saasAdminCacheUid = null
|
||||
let saasAdminCacheIsAdmin = null
|
||||
|
||||
// cache de globalRole por uid (evita query ao banco em cada navegação)
|
||||
let globalRoleCacheUid = null
|
||||
let globalRoleCache = null
|
||||
|
||||
// -----------------------------------------
|
||||
// Pending invite (Modelo B) — retomada pós-login
|
||||
// -----------------------------------------
|
||||
@@ -94,7 +99,7 @@ function sleep (ms) {
|
||||
async function waitSessionIfRefreshing () {
|
||||
if (!sessionReady.value) {
|
||||
try { await initSession({ initial: true }) } catch (e) {
|
||||
console.warn('[guards] initSession falhou:', e)
|
||||
logGuard('[guards] initSession falhou', { error: e?.message })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +151,7 @@ async function loadEntitlementsSafe (ent, tenantId, force) {
|
||||
} catch (e) {
|
||||
// se quebrou tentando force false (store não suporta), tenta força true uma vez
|
||||
if (!force) {
|
||||
console.warn('[guards] ent.loadForTenant(force:false) falhou, tentando force:true', e)
|
||||
logGuard('[guards] ent.loadForTenant force fallback', { error: e?.message })
|
||||
await ent.loadForTenant(tenantId, { force: true })
|
||||
return
|
||||
}
|
||||
@@ -163,7 +168,7 @@ async function fetchTenantFeaturesSafe (tf, tenantId) {
|
||||
try {
|
||||
await tf.fetchForTenant(tenantId, { force: false })
|
||||
} catch (e) {
|
||||
console.warn('[guards] tf.fetchForTenant(force:false) falhou, tentando force:true', e)
|
||||
logGuard('[guards] tf.fetchForTenant force fallback', { error: e?.message })
|
||||
await tf.fetchForTenant(tenantId, { force: true })
|
||||
}
|
||||
}
|
||||
@@ -234,9 +239,7 @@ async function ensureMenuBuilt ({ uid, tenantId, tenantRole, globalRole }) {
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
logGuard('[ensureMenuBuilt] menu incompatível com role, forçando rebuild', { key, safeRole, firstLabel })
|
||||
menuStore.reset()
|
||||
}
|
||||
|
||||
@@ -267,7 +270,7 @@ async function ensureMenuBuilt ({ uid, tenantId, tenantRole, globalRole }) {
|
||||
const model = getMenuByRole(roleForMenu, ctx) || []
|
||||
menuStore.setMenu(key, model)
|
||||
} catch (e) {
|
||||
console.warn('[guards] ensureMenuBuilt failed:', e)
|
||||
logGuard('[guards] ensureMenuBuilt failed', { error: e?.message })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +280,7 @@ export function applyGuards (router) {
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const tlabel = `[guard] ${to.fullPath}`
|
||||
console.time(tlabel)
|
||||
const _perfEnd = logPerf('router.guard', tlabel)
|
||||
|
||||
try {
|
||||
// ==========================================
|
||||
@@ -285,7 +288,7 @@ export function applyGuards (router) {
|
||||
// (ordem importa: /auth antes de meta.public)
|
||||
// ==========================================
|
||||
if (to.path.startsWith('/auth')) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -293,25 +296,25 @@ export function applyGuards (router) {
|
||||
// ✅ Rotas públicas
|
||||
// ==========================================
|
||||
if (to.meta?.public) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
// se rota não exige auth, libera
|
||||
if (!to.meta?.requiresAuth) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
// não decide nada no meio do refresh do session.js
|
||||
console.timeLog(tlabel, 'waitSessionIfRefreshing')
|
||||
logGuard('waitSessionIfRefreshing')
|
||||
await waitSessionIfRefreshing()
|
||||
|
||||
// precisa estar logado (fonte estável do session.js)
|
||||
const uid = sessionUser.value?.id || null
|
||||
if (!uid) {
|
||||
sessionStorage.setItem('redirect_after_login', to.fullPath)
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
@@ -321,21 +324,32 @@ export function applyGuards (router) {
|
||||
to.path.startsWith('/supervisor')
|
||||
|
||||
// ======================================
|
||||
// ✅ IDENTIDADE GLOBAL (1x por navegação)
|
||||
// ✅ IDENTIDADE GLOBAL (cached por uid — sem query a cada 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()
|
||||
let globalRole = null
|
||||
|
||||
const globalRole = !profErr ? prof?.role : null
|
||||
console.timeLog(tlabel, 'profiles.role =', globalRole)
|
||||
if (globalRoleCacheUid === uid && globalRoleCache) {
|
||||
globalRole = globalRoleCache
|
||||
logGuard('profiles.role (cache) =', globalRole)
|
||||
} else {
|
||||
const { data: prof, error: profErr } = await supabase
|
||||
.from('profiles')
|
||||
.select('role')
|
||||
.eq('id', uid)
|
||||
.single()
|
||||
|
||||
globalRole = !profErr ? prof?.role : null
|
||||
if (globalRole) {
|
||||
globalRoleCacheUid = uid
|
||||
globalRoleCache = globalRole
|
||||
}
|
||||
logGuard('profiles.role (db) =', globalRole)
|
||||
}
|
||||
|
||||
if (!globalRole) {
|
||||
sessionStorage.setItem('redirect_after_login', to.fullPath)
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
@@ -350,7 +364,7 @@ export function applyGuards (router) {
|
||||
localStorage.removeItem('currentTenantId')
|
||||
} catch (_) {}
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/portal' }
|
||||
}
|
||||
|
||||
@@ -359,7 +373,7 @@ export function applyGuards (router) {
|
||||
// ======================================
|
||||
if (to.meta?.profileRole) {
|
||||
if (globalRole !== to.meta.profileRole) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
@@ -371,7 +385,7 @@ export function applyGuards (router) {
|
||||
globalRole
|
||||
})
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -382,7 +396,7 @@ export function applyGuards (router) {
|
||||
// ======================================
|
||||
const isAccountArea = (to.path === '/account' || to.path.startsWith('/account/'))
|
||||
if (isAccountArea) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -410,7 +424,7 @@ export function applyGuards (router) {
|
||||
isUuid(pendingInviteToken) &&
|
||||
!to.path.startsWith('/accept-invite')
|
||||
) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/accept-invite', query: { token: pendingInviteToken } }
|
||||
}
|
||||
|
||||
@@ -419,6 +433,8 @@ export function applyGuards (router) {
|
||||
sessionUidCache = uid
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
globalRoleCacheUid = null
|
||||
globalRoleCache = null
|
||||
|
||||
const ent0 = useEntitlementsStore()
|
||||
if (typeof ent0.invalidate === 'function') ent0.invalidate()
|
||||
@@ -436,10 +452,10 @@ export function applyGuards (router) {
|
||||
// ✅ SAAS MASTER: não depende de tenant
|
||||
// ================================
|
||||
if (to.meta?.saasAdmin) {
|
||||
console.timeLog(tlabel, 'isSaasAdmin')
|
||||
logGuard('isSaasAdmin')
|
||||
// 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' } }
|
||||
if (!ok) { _perfEnd(); return { path: '/pages/access' } }
|
||||
|
||||
// ✅ monta menu SaaS 1x (AppMenu lê do menuStore)
|
||||
await ensureMenuBuilt({
|
||||
@@ -449,7 +465,7 @@ export function applyGuards (router) {
|
||||
globalRole
|
||||
})
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -472,7 +488,7 @@ export function applyGuards (router) {
|
||||
}
|
||||
|
||||
if (!platformRoles.includes('editor')) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
@@ -483,7 +499,7 @@ export function applyGuards (router) {
|
||||
globalRole
|
||||
})
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -491,7 +507,7 @@ export function applyGuards (router) {
|
||||
// 🚫 SaaS master: bloqueia tenant-app por padrão
|
||||
// ✅ Mas libera rotas de DEMO em DEV (Sakai)
|
||||
// ================================
|
||||
console.timeLog(tlabel, 'saas.lockdown?')
|
||||
logGuard('saas.lockdown?')
|
||||
|
||||
// 🔥 PATCH: aqui NÃO consulta isSaasAdmin — usa só identidade global
|
||||
const isSaas = (globalRole === 'saas_admin')
|
||||
@@ -515,13 +531,13 @@ export function applyGuards (router) {
|
||||
globalRole
|
||||
})
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
// Fora de /saas (e não-demo), não pode
|
||||
if (!isSaasArea) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/saas' }
|
||||
}
|
||||
|
||||
@@ -540,7 +556,7 @@ export function applyGuards (router) {
|
||||
|
||||
// carrega tenant + role
|
||||
const tenant = useTenantStore()
|
||||
console.timeLog(tlabel, 'tenant.loadSessionAndTenant?')
|
||||
logGuard('tenant.loadSessionAndTenant?')
|
||||
if (!tenant.loaded && !tenant.loading) {
|
||||
await tenant.loadSessionAndTenant()
|
||||
}
|
||||
@@ -548,7 +564,7 @@ export function applyGuards (router) {
|
||||
// se não tem user no store, trata como não logado
|
||||
if (!tenant.user) {
|
||||
sessionStorage.setItem('redirect_after_login', to.fullPath)
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
@@ -576,12 +592,12 @@ export function applyGuards (router) {
|
||||
// 🔥 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)
|
||||
_perfEnd()
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
|
||||
console.timeEnd(tlabel)
|
||||
if (to.path === '/pages/access') { _perfEnd(); return true }
|
||||
_perfEnd()
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
@@ -596,14 +612,14 @@ export function applyGuards (router) {
|
||||
// 🔥 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)
|
||||
_perfEnd()
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
let tenantId = tenant.activeTenantId
|
||||
if (!tenantId) {
|
||||
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
|
||||
console.timeEnd(tlabel)
|
||||
if (to.path === '/pages/access') { _perfEnd(); return true }
|
||||
_perfEnd()
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
@@ -630,7 +646,7 @@ export function applyGuards (router) {
|
||||
const desiredTenantId = desired?.tenant_id || null
|
||||
|
||||
if (desiredTenantId && tenant.activeTenantId !== desiredTenantId) {
|
||||
console.timeLog(tlabel, `tenantScope.switch(${scope})`)
|
||||
logGuard(`tenantScope.switch(${scope})`)
|
||||
|
||||
// ✅ guarda o tenant antigo para invalidar APENAS ele
|
||||
const oldTenantId = tenant.activeTenantId
|
||||
@@ -661,13 +677,9 @@ export function applyGuards (router) {
|
||||
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
|
||||
}))
|
||||
logGuard('[guards] tenantScope sem match', {
|
||||
scope,
|
||||
memberships: mem.map(x => ({ tenant_id: x?.tenant_id, role: x?.role, kind: x?.kind, status: x?.status }))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -682,7 +694,7 @@ export function applyGuards (router) {
|
||||
// entitlements (✅ carrega só quando precisa)
|
||||
const ent = useEntitlementsStore()
|
||||
if (shouldLoadEntitlements(ent, tenantId)) {
|
||||
console.timeLog(tlabel, 'ent.loadForTenant')
|
||||
logGuard('ent.loadForTenant')
|
||||
await loadEntitlementsSafe(ent, tenantId, true)
|
||||
}
|
||||
|
||||
@@ -691,9 +703,9 @@ export function applyGuards (router) {
|
||||
// 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')
|
||||
logGuard('ent.loadForUser')
|
||||
try { await ent.loadForUser(uid) } catch (e) {
|
||||
console.warn('[guards] ent.loadForUser failed:', e)
|
||||
logGuard('[guards] ent.loadForUser failed', { error: e?.message })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,7 +716,7 @@ export function applyGuards (router) {
|
||||
const requiredTenantFeature = to.meta?.tenantFeature
|
||||
if (requiredTenantFeature) {
|
||||
const tf = useTenantFeaturesStore()
|
||||
console.timeLog(tlabel, 'tenantFeatures.fetchForTenant')
|
||||
logGuard('tenantFeatures.fetchForTenant')
|
||||
await fetchTenantFeaturesSafe(tf, tenantId)
|
||||
|
||||
// ✅ IMPORTANTÍSSIMO: passa tenantId
|
||||
@@ -713,9 +725,9 @@ export function applyGuards (router) {
|
||||
: false
|
||||
|
||||
if (!enabled) {
|
||||
if (to.path === '/admin/clinic/features') { console.timeEnd(tlabel); return true }
|
||||
if (to.path === '/admin/clinic/features') { _perfEnd(); return true }
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return {
|
||||
path: '/admin/clinic/features',
|
||||
query: { missing: requiredTenantFeature, redirectTo: to.fullPath }
|
||||
@@ -746,7 +758,7 @@ export function applyGuards (router) {
|
||||
if (compatible) {
|
||||
tenant.activeRole = normalizeRole(compatible.role, compatible.kind)
|
||||
} else {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return denyByRole({ to, currentRole: tenant.activeRole })
|
||||
}
|
||||
}
|
||||
@@ -756,7 +768,7 @@ export function applyGuards (router) {
|
||||
const requiredRole = requiredRoleRaw ? normalizeRole(requiredRoleRaw) : null
|
||||
|
||||
if (requiredRole && normalizeRole(tenant.activeRole) !== requiredRole) {
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return denyByRole({ to, currentRole: tenant.activeRole })
|
||||
}
|
||||
|
||||
@@ -765,7 +777,7 @@ export function applyGuards (router) {
|
||||
// ------------------------------------------------
|
||||
const requiredFeature = to.meta?.feature
|
||||
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
|
||||
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
|
||||
if (to.name === 'upgrade') { _perfEnd(); return true }
|
||||
|
||||
const url = buildUpgradeUrl({
|
||||
missingKeys: [requiredFeature],
|
||||
@@ -773,7 +785,7 @@ export function applyGuards (router) {
|
||||
role: normalizeRole(tenant.activeRole) // ✅ passa o role para a UpgradePage detectar
|
||||
})
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -787,10 +799,10 @@ export function applyGuards (router) {
|
||||
globalRole
|
||||
})
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
_perfEnd()
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error('[guards] erro no beforeEach:', e)
|
||||
logError('router.guard', 'erro no beforeEach', e)
|
||||
|
||||
if (to.path.startsWith('/auth')) return true
|
||||
if (to.meta?.public) return true
|
||||
@@ -814,6 +826,8 @@ export function applyGuards (router) {
|
||||
sessionUidCache = null
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
globalRoleCacheUid = null
|
||||
globalRoleCache = null
|
||||
|
||||
// ✅ FIX: limpa o localStorage de tenant na saída
|
||||
// Sem isso, o próximo login restaura o tenant do usuário anterior.
|
||||
@@ -861,6 +875,8 @@ export function applyGuards (router) {
|
||||
sessionUidCache = uid || null
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
globalRoleCacheUid = null
|
||||
globalRoleCache = null
|
||||
|
||||
try {
|
||||
const tf = useTenantFeaturesStore()
|
||||
|
||||
Reference in New Issue
Block a user