Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda

This commit is contained in:
Leonardo
2026-02-22 17:56:01 -03:00
parent 6eff67bf22
commit 89b4ecaba1
77 changed files with 9433 additions and 1995 deletions

View File

@@ -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 {}
})
}
}
}