Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// src/router/guard.js
|
||||
// ⚠️ Guard depende de sessão estável.
|
||||
// Nunca disparar refresh concorrente durante navegação protegida.
|
||||
// Ver comentário em session.js sobre race condition.
|
||||
@@ -5,6 +6,7 @@
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
|
||||
import { buildUpgradeUrl } from '@/utils/upgradeContext'
|
||||
|
||||
import { sessionUser, sessionReady, sessionRefreshing, initSession } from '@/app/session'
|
||||
@@ -16,11 +18,33 @@ let sessionUidCache = null
|
||||
let saasAdminCacheUid = null
|
||||
let saasAdminCacheIsAdmin = null
|
||||
|
||||
// -----------------------------------------
|
||||
// Pending invite (Modelo B) — retomada pós-login
|
||||
// -----------------------------------------
|
||||
const PENDING_INVITE_TOKEN_KEY = 'pending_invite_token_v1'
|
||||
|
||||
function readPendingInviteToken () {
|
||||
try { return sessionStorage.getItem(PENDING_INVITE_TOKEN_KEY) } catch (_) { return null }
|
||||
}
|
||||
|
||||
function clearPendingInviteToken () {
|
||||
try { sessionStorage.removeItem(PENDING_INVITE_TOKEN_KEY) } catch (_) {}
|
||||
}
|
||||
|
||||
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 || ''))
|
||||
}
|
||||
|
||||
function roleToPath (role) {
|
||||
if (role === 'tenant_admin') return '/admin'
|
||||
// ✅ clínica: aceita nomes canônicos e legado
|
||||
if (role === 'clinic_admin' || role === 'tenant_admin' || role === 'admin') return '/admin'
|
||||
|
||||
if (role === 'therapist') return '/therapist'
|
||||
if (role === 'patient') return '/patient'
|
||||
if (role === 'patient') return '/portal'
|
||||
|
||||
// ✅ saas master
|
||||
if (role === 'saas_admin') return '/saas'
|
||||
|
||||
return '/'
|
||||
}
|
||||
|
||||
@@ -103,12 +127,28 @@ export function applyGuards (router) {
|
||||
console.time(tlabel)
|
||||
|
||||
try {
|
||||
// públicos
|
||||
if (to.meta?.public) { console.timeEnd(tlabel); return true }
|
||||
if (to.path.startsWith('/auth')) { console.timeEnd(tlabel); return true }
|
||||
// ==========================================
|
||||
// ✅ AUTH SEMPRE LIBERADO (blindagem total)
|
||||
// (ordem importa: /auth antes de meta.public)
|
||||
// ==========================================
|
||||
if (to.path.startsWith('/auth')) {
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ✅ Rotas públicas
|
||||
// ==========================================
|
||||
if (to.meta?.public) {
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// se rota não exige auth, libera
|
||||
if (!to.meta?.requiresAuth) { console.timeEnd(tlabel); return true }
|
||||
if (!to.meta?.requiresAuth) {
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// não decide nada no meio do refresh do session.js
|
||||
console.timeLog(tlabel, 'waitSessionIfRefreshing')
|
||||
@@ -122,6 +162,29 @@ export function applyGuards (router) {
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ✅ Pending invite (Modelo B): se o usuário logou e tem token pendente,
|
||||
// redireciona para /accept-invite antes de qualquer load pesado.
|
||||
// ==========================================
|
||||
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) &&
|
||||
!to.path.startsWith('/accept-invite')
|
||||
) {
|
||||
console.timeEnd(tlabel)
|
||||
return { path: '/accept-invite', query: { token: pendingInviteToken } }
|
||||
}
|
||||
|
||||
// se uid mudou, invalida caches e stores dependentes
|
||||
if (sessionUidCache !== uid) {
|
||||
sessionUidCache = uid
|
||||
@@ -130,15 +193,27 @@ export function applyGuards (router) {
|
||||
|
||||
const ent0 = useEntitlementsStore()
|
||||
if (typeof ent0.invalidate === 'function') ent0.invalidate()
|
||||
|
||||
const tf0 = useTenantFeaturesStore()
|
||||
if (typeof tf0.invalidate === 'function') tf0.invalidate()
|
||||
}
|
||||
|
||||
// saas admin (com cache)
|
||||
// ================================
|
||||
// ✅ SAAS MASTER: não depende de tenant
|
||||
// ================================
|
||||
if (to.meta?.saasAdmin) {
|
||||
console.timeLog(tlabel, 'isSaasAdmin')
|
||||
const ok = await isSaasAdmin(uid)
|
||||
if (!ok) { console.timeEnd(tlabel); return { path: '/pages/access' } }
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// ================================
|
||||
// ✅ Abaixo daqui é tudo tenant-app
|
||||
// ================================
|
||||
|
||||
// carrega tenant + role
|
||||
const tenant = useTenantStore()
|
||||
console.timeLog(tlabel, 'tenant.loadSessionAndTenant?')
|
||||
@@ -181,6 +256,12 @@ export function applyGuards (router) {
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// entitlements (✅ carrega só quando precisa)
|
||||
const ent = useEntitlementsStore()
|
||||
if (shouldLoadEntitlements(ent, tenantId)) {
|
||||
@@ -188,6 +269,28 @@ export function applyGuards (router) {
|
||||
await loadEntitlementsSafe(ent, tenantId, true)
|
||||
}
|
||||
|
||||
// ================================
|
||||
// ✅ tenant_features (módulos ativáveis por clínica)
|
||||
// meta.tenantFeature = 'patients' | ...
|
||||
// ================================
|
||||
const requiredTenantFeature = to.meta?.tenantFeature
|
||||
if (requiredTenantFeature) {
|
||||
const tf = useTenantFeaturesStore()
|
||||
console.timeLog(tlabel, 'tenantFeatures.fetchForTenant')
|
||||
await tf.fetchForTenant(tenantId, { force: false })
|
||||
|
||||
if (!tf.isEnabled(requiredTenantFeature)) {
|
||||
// evita loop
|
||||
if (to.path === '/admin/clinic/features') { console.timeEnd(tlabel); return true }
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return {
|
||||
path: '/admin/clinic/features',
|
||||
query: { missing: requiredTenantFeature, redirectTo: to.fullPath }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// roles guard (plural)
|
||||
const allowedRoles = to.meta?.roles
|
||||
if (Array.isArray(allowedRoles) && allowedRoles.length) {
|
||||
@@ -208,18 +311,18 @@ export function applyGuards (router) {
|
||||
return { path: fallback }
|
||||
}
|
||||
|
||||
// feature guard
|
||||
// feature guard (entitlements/plano → upgrade)
|
||||
const requiredFeature = to.meta?.feature
|
||||
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
|
||||
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
|
||||
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
|
||||
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
|
||||
|
||||
const url = buildUpgradeUrl({
|
||||
missingKeys: [requiredFeature],
|
||||
redirectTo: to.fullPath
|
||||
})
|
||||
console.timeEnd(tlabel)
|
||||
return { path: url }
|
||||
}
|
||||
const url = buildUpgradeUrl({
|
||||
missingKeys: [requiredFeature],
|
||||
redirectTo: to.fullPath
|
||||
})
|
||||
console.timeEnd(tlabel)
|
||||
return url
|
||||
}
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
@@ -227,7 +330,8 @@ export function applyGuards (router) {
|
||||
console.error('[guards] erro no beforeEach:', e)
|
||||
|
||||
// fallback seguro
|
||||
if (to.meta?.public || to.path.startsWith('/auth')) return true
|
||||
if (to.path.startsWith('/auth')) return true
|
||||
if (to.meta?.public) return true
|
||||
if (to.path === '/pages/access') return true
|
||||
|
||||
sessionStorage.setItem('redirect_after_login', to.fullPath)
|
||||
@@ -243,6 +347,11 @@ export function applyGuards (router) {
|
||||
sessionUidCache = null
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
|
||||
try {
|
||||
const tf = useTenantFeaturesStore()
|
||||
if (typeof tf.invalidate === 'function') tf.invalidate()
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user