Ajuste rotas, Menus, Layout, Permissãoes UserRoleGuard
This commit is contained in:
71
src/router/accessRedirects.js
Normal file
71
src/router/accessRedirects.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// src/router/accessRedirects.js
|
||||
|
||||
/**
|
||||
* Redirecionamentos padronizados para:
|
||||
* - RBAC (papel): usuário NÃO deveria acessar essa área nem pagando → manda pra 403 (ou home do papel)
|
||||
* - Entitlements (plano): usuário poderia acessar se tivesse feature → manda pro /upgrade
|
||||
*
|
||||
* Por que isso existe?
|
||||
* - Evitar o bug clássico: “pessoa sem permissão caiu no /upgrade”
|
||||
* - Padronizar o comportamento do app em um único lugar
|
||||
* - Deixar claro: RBAC ≠ Plano
|
||||
*
|
||||
* Convenção recomendada:
|
||||
* - RBAC (role): sempre é bloqueio (403) OU home do papel (UX mais “suave”)
|
||||
* - Plano (feature): sempre é upgrade (porque o usuário *poderia* ter acesso pagando)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Home por role (tenant_members.role).
|
||||
* Obs: 'admin' é role GLOBAL (profiles.role). Aqui é guard de tenant,
|
||||
* mas mantemos um fallback seguro para legado.
|
||||
*/
|
||||
export function roleHomePath (role) {
|
||||
// ✅ clínica: aceita nomes canônicos e legado
|
||||
if (role === 'clinic_admin' || role === 'tenant_admin') return '/admin'
|
||||
if (role === 'therapist') return '/therapist'
|
||||
if (role === 'patient') return '/portal'
|
||||
|
||||
// ✅ fallback (não deveria acontecer em tenant)
|
||||
if (role === 'admin') return '/admin'
|
||||
|
||||
return '/'
|
||||
}
|
||||
|
||||
/**
|
||||
* RBAC (papel) → padrão: acesso negado (403).
|
||||
*
|
||||
* Se você preferir UX “suave”, pode mandar para a home do papel.
|
||||
* Eu deixei as duas opções:
|
||||
* - use403 = true → sempre /pages/access (recomendado para clareza)
|
||||
* - use403 = false → home do papel (útil quando você quer “auto-corrigir” navegação)
|
||||
*/
|
||||
export function denyByRole ({ to, currentRole, use403 = true } = {}) {
|
||||
// ✅ padrão forte: 403 (não é caso de upgrade)
|
||||
if (use403) return { path: '/pages/access' }
|
||||
|
||||
// modo “suave”: manda pra home do papel
|
||||
const fallback = roleHomePath(currentRole)
|
||||
|
||||
// evita loop: se já está no fallback, manda pra página de acesso negado
|
||||
if (to?.path && to.path === fallback) {
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
return { path: fallback }
|
||||
}
|
||||
|
||||
/**
|
||||
* Entitlements (plano) → upgrade.
|
||||
* missingFeature: feature key (ex: 'online_scheduling.manage')
|
||||
* redirectTo: para onde voltar após upgrade
|
||||
*/
|
||||
export function denyByPlan ({ to, missingFeature, redirectTo } = {}) {
|
||||
return {
|
||||
path: '/upgrade',
|
||||
query: {
|
||||
feature: missingFeature || '',
|
||||
redirectTo: redirectTo || to?.fullPath || '/'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { buildUpgradeUrl } from '@/utils/upgradeContext'
|
||||
|
||||
import { sessionUser, sessionReady, sessionRefreshing, initSession } from '@/app/session'
|
||||
|
||||
// ✅ separa RBAC (papel) vs Plano (upgrade)
|
||||
import { denyByRole, denyByPlan } from '@/router/accessRedirects'
|
||||
|
||||
// cache simples (evita bater no banco em toda navegação)
|
||||
let sessionUidCache = null
|
||||
|
||||
@@ -234,14 +237,14 @@ export function applyGuards (router) {
|
||||
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))
|
||||
: null
|
||||
// 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))
|
||||
: null
|
||||
|
||||
// 2) fallback: primeiro active
|
||||
const firstActive = preferred || mem.find(m => m && m.status === 'active' && m.tenant_id)
|
||||
// 2) fallback: primeiro active
|
||||
const firstActive = preferred || mem.find(m => m && m.status === 'active' && m.tenant_id)
|
||||
|
||||
if (!firstActive) {
|
||||
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
|
||||
@@ -299,42 +302,60 @@ export function applyGuards (router) {
|
||||
}
|
||||
}
|
||||
|
||||
// roles guard (plural)
|
||||
// Se a rota pede roles específicas e o role ativo não bate,
|
||||
// tenta ajustar o activeRole dentro do mesmo tenant (se houver membership compatível).
|
||||
const allowedRoles = Array.isArray(to.meta?.roles) ? to.meta.roles : null
|
||||
if (allowedRoles && allowedRoles.length && !allowedRoles.includes(tenant.activeRole)) {
|
||||
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)
|
||||
)
|
||||
if (compatible) {
|
||||
// muda role ativo para o compatível
|
||||
tenant.activeRole = compatible.role
|
||||
}
|
||||
}
|
||||
// ------------------------------------------------
|
||||
// ✅ 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 mem = Array.isArray(tenant.memberships) ? tenant.memberships : []
|
||||
const compatible = mem.find(m =>
|
||||
m && m.status === 'active' && m.tenant_id === tenantId && allowedRoles.includes(m.role)
|
||||
)
|
||||
|
||||
if (compatible) {
|
||||
// muda role ativo para o compatível (mesmo tenant)
|
||||
tenant.activeRole = compatible.role
|
||||
} 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) {
|
||||
const fallback = roleToPath(tenant.activeRole)
|
||||
if (to.path === fallback) { console.timeEnd(tlabel); return { path: '/pages/access' } }
|
||||
// RBAC singular também é "papel" → cai fora (não é upgrade)
|
||||
console.timeEnd(tlabel)
|
||||
return { path: fallback }
|
||||
return denyByRole({ to, currentRole: tenant.activeRole })
|
||||
}
|
||||
|
||||
// feature guard (entitlements/plano → upgrade)
|
||||
// ------------------------------------------------
|
||||
// ✅ 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)) {
|
||||
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
|
||||
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
|
||||
// evita loop
|
||||
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
|
||||
|
||||
const url = buildUpgradeUrl({
|
||||
missingKeys: [requiredFeature],
|
||||
redirectTo: to.fullPath
|
||||
})
|
||||
console.timeEnd(tlabel)
|
||||
return url
|
||||
}
|
||||
// Mantém compatibilidade com seu fluxo existente (buildUpgradeUrl)
|
||||
const url = buildUpgradeUrl({
|
||||
missingKeys: [requiredFeature],
|
||||
redirectTo: to.fullPath
|
||||
})
|
||||
|
||||
// Se quiser padronizar no futuro, você pode trocar por:
|
||||
// return denyByPlan({ to, missingFeature: requiredFeature, redirectTo: to.fullPath })
|
||||
console.timeEnd(tlabel)
|
||||
return url
|
||||
}
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
|
||||
@@ -38,7 +38,7 @@ const routes = [
|
||||
})
|
||||
},
|
||||
|
||||
{ path: '/:pathMatch(.*)*', name: 'notfound', component: () => import('@/views/pages/NotFound.vue') }
|
||||
// inserido no routes.misc { path: '/:pathMatch(.*)*', name: 'notfound', component: () => import('@/views/pages/NotFound.vue') }
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -6,10 +6,28 @@ export default {
|
||||
name: 'landing',
|
||||
component: () => import('@/views/pages/Landing.vue')
|
||||
},
|
||||
|
||||
// 404
|
||||
{
|
||||
path: 'pages/notfound',
|
||||
name: 'notfound',
|
||||
component: () => import('@/views/pages/NotFound.vue')
|
||||
},
|
||||
|
||||
// 403 (Acesso negado - RBAC)
|
||||
{
|
||||
path: 'pages/access', // ❗ SEM barra inicial aqui
|
||||
name: 'AccessDenied',
|
||||
component: () => import('@/views/pages/misc/AccessDeniedPage.vue'),
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
|
||||
// Catch-all (SEMPRE o último)
|
||||
{
|
||||
path: ':pathMatch(.*)*',
|
||||
redirect: { name: 'notfound' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user