first commit
This commit is contained in:
248
src/router/guards.js
Normal file
248
src/router/guards.js
Normal file
@@ -0,0 +1,248 @@
|
||||
// ⚠️ Guard depende de sessão estável.
|
||||
// Nunca disparar refresh concorrente durante navegação protegida.
|
||||
// Ver comentário em session.js sobre race condition.
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
import { buildUpgradeUrl } from '@/utils/upgradeContext'
|
||||
|
||||
import { sessionUser, sessionReady, sessionRefreshing, initSession } from '@/app/session'
|
||||
|
||||
// cache simples (evita bater no banco em toda navegação)
|
||||
let sessionUidCache = null
|
||||
|
||||
// cache de saas admin por uid (pra não consultar tabela toda vez)
|
||||
let saasAdminCacheUid = null
|
||||
let saasAdminCacheIsAdmin = null
|
||||
|
||||
function roleToPath (role) {
|
||||
if (role === 'tenant_admin') return '/admin'
|
||||
if (role === 'therapist') return '/therapist'
|
||||
if (role === 'patient') return '/patient'
|
||||
if (role === 'saas_admin') return '/saas'
|
||||
return '/'
|
||||
}
|
||||
|
||||
function sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function waitSessionIfRefreshing () {
|
||||
if (!sessionReady.value) {
|
||||
try { await initSession({ initial: true }) } catch (e) {
|
||||
console.warn('[guards] initSession falhou:', e)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (!sessionRefreshing.value) return
|
||||
await sleep(50)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
const ok = !error && !!data
|
||||
saasAdminCacheUid = uid
|
||||
saasAdminCacheIsAdmin = ok
|
||||
return ok
|
||||
}
|
||||
|
||||
// heurística segura: carrega entitlements se ainda não carregou ou mudou tenant
|
||||
function shouldLoadEntitlements (ent, tenantId) {
|
||||
if (!tenantId) return false
|
||||
|
||||
const loaded = typeof ent.loaded === 'boolean' ? ent.loaded : false
|
||||
const entTenantId = ent.activeTenantId ?? ent.tenantId ?? null
|
||||
|
||||
if (!loaded) return true
|
||||
if (entTenantId && entTenantId !== tenantId) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// wrapper: chama loadForTenant sem depender de force:false existir
|
||||
async function loadEntitlementsSafe (ent, tenantId, force) {
|
||||
if (!ent?.loadForTenant) return
|
||||
|
||||
try {
|
||||
await ent.loadForTenant(tenantId, { force: !!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)
|
||||
await ent.loadForTenant(tenantId, { force: true })
|
||||
return
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// util: roles guard (plural)
|
||||
function matchesRoles (roles, activeRole) {
|
||||
if (!Array.isArray(roles) || !roles.length) return true
|
||||
return roles.includes(activeRole)
|
||||
}
|
||||
|
||||
export function applyGuards (router) {
|
||||
if (window.__guardsBound) return
|
||||
window.__guardsBound = true
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const tlabel = `[guard] ${to.fullPath}`
|
||||
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 }
|
||||
|
||||
// se rota não exige auth, libera
|
||||
if (!to.meta?.requiresAuth) { console.timeEnd(tlabel); return true }
|
||||
|
||||
// não decide nada no meio do refresh do session.js
|
||||
console.timeLog(tlabel, '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)
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
// se uid mudou, invalida caches e stores dependentes
|
||||
if (sessionUidCache !== uid) {
|
||||
sessionUidCache = uid
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
|
||||
const ent0 = useEntitlementsStore()
|
||||
if (typeof ent0.invalidate === 'function') ent0.invalidate()
|
||||
}
|
||||
|
||||
// saas admin (com cache)
|
||||
if (to.meta?.saasAdmin) {
|
||||
console.timeLog(tlabel, 'isSaasAdmin')
|
||||
const ok = await isSaasAdmin(uid)
|
||||
if (!ok) { console.timeEnd(tlabel); return { path: '/pages/access' } }
|
||||
}
|
||||
|
||||
// carrega tenant + role
|
||||
const tenant = useTenantStore()
|
||||
console.timeLog(tlabel, 'tenant.loadSessionAndTenant?')
|
||||
if (!tenant.loaded && !tenant.loading) {
|
||||
await tenant.loadSessionAndTenant()
|
||||
}
|
||||
|
||||
// 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)
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
// 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 : []
|
||||
const firstActive = mem.find(m => m && m.status === 'active' && m.tenant_id)
|
||||
|
||||
if (!firstActive) {
|
||||
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
|
||||
console.timeEnd(tlabel)
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
if (typeof tenant.setActiveTenant === 'function') {
|
||||
tenant.setActiveTenant(firstActive.tenant_id)
|
||||
} else {
|
||||
tenant.activeTenantId = firstActive.tenant_id
|
||||
tenant.activeRole = firstActive.role
|
||||
}
|
||||
}
|
||||
|
||||
const tenantId = tenant.activeTenantId
|
||||
if (!tenantId) {
|
||||
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
|
||||
console.timeEnd(tlabel)
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
// entitlements (✅ carrega só quando precisa)
|
||||
const ent = useEntitlementsStore()
|
||||
if (shouldLoadEntitlements(ent, tenantId)) {
|
||||
console.timeLog(tlabel, 'ent.loadForTenant')
|
||||
await loadEntitlementsSafe(ent, tenantId, true)
|
||||
}
|
||||
|
||||
// roles guard (plural)
|
||||
const allowedRoles = to.meta?.roles
|
||||
if (Array.isArray(allowedRoles) && allowedRoles.length) {
|
||||
if (!matchesRoles(allowedRoles, tenant.activeRole)) {
|
||||
const fallback = roleToPath(tenant.activeRole)
|
||||
if (to.path === fallback) { console.timeEnd(tlabel); return { path: '/pages/access' } }
|
||||
console.timeEnd(tlabel)
|
||||
return { path: fallback }
|
||||
}
|
||||
}
|
||||
|
||||
// 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' } }
|
||||
console.timeEnd(tlabel)
|
||||
return { path: fallback }
|
||||
}
|
||||
|
||||
// feature guard
|
||||
const requiredFeature = to.meta?.feature
|
||||
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 }
|
||||
}
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error('[guards] erro no beforeEach:', e)
|
||||
|
||||
// fallback seguro
|
||||
if (to.meta?.public || to.path.startsWith('/auth')) return true
|
||||
if (to.path === '/pages/access') return true
|
||||
|
||||
sessionStorage.setItem('redirect_after_login', to.fullPath)
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
})
|
||||
|
||||
// auth listener (reset caches)
|
||||
if (!window.__supabaseAuthListenerBound) {
|
||||
window.__supabaseAuthListenerBound = true
|
||||
|
||||
supabase.auth.onAuthStateChange(() => {
|
||||
sessionUidCache = null
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,146 +1,111 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
isNavigationFailure,
|
||||
NavigationFailureType
|
||||
} from 'vue-router'
|
||||
|
||||
import publicRoutes from './routes.public'
|
||||
import adminRoutes from './routes.admin'
|
||||
import therapistRoutes from './routes.therapist'
|
||||
import patientRoutes from './routes.patient'
|
||||
import miscRoutes from './routes.misc'
|
||||
import authRoutes from './routes.auth'
|
||||
import configuracoesRoutes from './router.configuracoes'
|
||||
import billingRoutes from './routes.billing'
|
||||
import saasRoutes from './routes.saas'
|
||||
import demoRoutes from './routes.demo'
|
||||
import meRoutes from './router.me'
|
||||
|
||||
import { applyGuards } from './guards'
|
||||
|
||||
const routes = [
|
||||
...(Array.isArray(publicRoutes) ? publicRoutes : [publicRoutes]),
|
||||
...(Array.isArray(authRoutes) ? authRoutes : [authRoutes]),
|
||||
...(Array.isArray(miscRoutes) ? miscRoutes : [miscRoutes]),
|
||||
...(Array.isArray(billingRoutes) ? billingRoutes : [billingRoutes]),
|
||||
...(Array.isArray(saasRoutes) ? saasRoutes : [saasRoutes]),
|
||||
...(Array.isArray(meRoutes) ? meRoutes : [meRoutes]),
|
||||
...(Array.isArray(adminRoutes) ? adminRoutes : [adminRoutes]),
|
||||
...(Array.isArray(therapistRoutes) ? therapistRoutes : [therapistRoutes]),
|
||||
...(Array.isArray(patientRoutes) ? patientRoutes : [patientRoutes]),
|
||||
...(Array.isArray(demoRoutes) ? demoRoutes : [demoRoutes]),
|
||||
...(Array.isArray(configuracoesRoutes) ? configuracoesRoutes : [configuracoesRoutes]),
|
||||
|
||||
{ path: '/:pathMatch(.*)*', name: 'notfound', component: () => import('@/views/pages/NotFound.vue') }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: AppLayout,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'dashboard',
|
||||
component: () => import('@/views/Dashboard.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/formlayout',
|
||||
name: 'formlayout',
|
||||
component: () => import('@/views/uikit/FormLayout.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/input',
|
||||
name: 'input',
|
||||
component: () => import('@/views/uikit/InputDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/button',
|
||||
name: 'button',
|
||||
component: () => import('@/views/uikit/ButtonDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/table',
|
||||
name: 'table',
|
||||
component: () => import('@/views/uikit/TableDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/list',
|
||||
name: 'list',
|
||||
component: () => import('@/views/uikit/ListDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/tree',
|
||||
name: 'tree',
|
||||
component: () => import('@/views/uikit/TreeDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/panel',
|
||||
name: 'panel',
|
||||
component: () => import('@/views/uikit/PanelsDoc.vue')
|
||||
},
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// volta/avançar do navegador mantém posição
|
||||
if (savedPosition) return savedPosition
|
||||
|
||||
{
|
||||
path: '/uikit/overlay',
|
||||
name: 'overlay',
|
||||
component: () => import('@/views/uikit/OverlayDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/media',
|
||||
name: 'media',
|
||||
component: () => import('@/views/uikit/MediaDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/message',
|
||||
name: 'message',
|
||||
component: () => import('@/views/uikit/MessagesDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/file',
|
||||
name: 'file',
|
||||
component: () => import('@/views/uikit/FileDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/menu',
|
||||
name: 'menu',
|
||||
component: () => import('@/views/uikit/MenuDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/charts',
|
||||
name: 'charts',
|
||||
component: () => import('@/views/uikit/ChartDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/misc',
|
||||
name: 'misc',
|
||||
component: () => import('@/views/uikit/MiscDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/timeline',
|
||||
name: 'timeline',
|
||||
component: () => import('@/views/uikit/TimelineDoc.vue')
|
||||
},
|
||||
{
|
||||
path: '/blocks/free',
|
||||
name: 'blocks',
|
||||
meta: {
|
||||
breadcrumb: ['Prime Blocks', 'Free Blocks']
|
||||
},
|
||||
component: () => import('@/views/utilities/Blocks.vue')
|
||||
},
|
||||
{
|
||||
path: '/pages/empty',
|
||||
name: 'empty',
|
||||
component: () => import('@/views/pages/Empty.vue')
|
||||
},
|
||||
{
|
||||
path: '/pages/crud',
|
||||
name: 'crud',
|
||||
component: () => import('@/views/pages/Crud.vue')
|
||||
},
|
||||
{
|
||||
path: '/start/documentation',
|
||||
name: 'documentation',
|
||||
component: () => import('@/views/pages/Documentation.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/landing',
|
||||
name: 'landing',
|
||||
component: () => import('@/views/pages/Landing.vue')
|
||||
},
|
||||
{
|
||||
path: '/pages/notfound',
|
||||
name: 'notfound',
|
||||
component: () => import('@/views/pages/NotFound.vue')
|
||||
},
|
||||
// qualquer navegação normal NÃO altera o scroll
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
path: '/auth/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/pages/auth/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/auth/access',
|
||||
name: 'accessDenied',
|
||||
component: () => import('@/views/pages/auth/Access.vue')
|
||||
},
|
||||
{
|
||||
path: '/auth/error',
|
||||
name: 'error',
|
||||
component: () => import('@/views/pages/auth/Error.vue')
|
||||
}
|
||||
]
|
||||
});
|
||||
/* 🔎 DEBUG: listar todas as rotas registradas */
|
||||
console.log(
|
||||
'[ROUTES]',
|
||||
router.getRoutes().map(r => r.path).sort()
|
||||
)
|
||||
|
||||
export default router;
|
||||
// ===== DEBUG NAV + TRACE (remover depois) =====
|
||||
const _push = router.push.bind(router)
|
||||
router.push = async (loc) => {
|
||||
console.log('[router.push]', loc)
|
||||
console.trace('[push caller]')
|
||||
|
||||
const res = await _push(loc)
|
||||
|
||||
if (isNavigationFailure(res, NavigationFailureType.duplicated)) {
|
||||
console.warn('[NAV FAIL] duplicated', res)
|
||||
} else if (isNavigationFailure(res, NavigationFailureType.cancelled)) {
|
||||
console.warn('[NAV FAIL] cancelled', res)
|
||||
} else if (isNavigationFailure(res, NavigationFailureType.aborted)) {
|
||||
console.warn('[NAV FAIL] aborted', res)
|
||||
} else if (isNavigationFailure(res, NavigationFailureType.redirected)) {
|
||||
console.warn('[NAV FAIL] redirected', res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const _replace = router.replace.bind(router)
|
||||
router.replace = async (loc) => {
|
||||
console.log('[router.replace]', loc)
|
||||
console.trace('[replace caller]')
|
||||
|
||||
const res = await _replace(loc)
|
||||
|
||||
if (isNavigationFailure(res, NavigationFailureType.cancelled)) {
|
||||
console.warn('[NAV FAIL replace] cancelled', res)
|
||||
} else if (isNavigationFailure(res, NavigationFailureType.aborted)) {
|
||||
console.warn('[NAV FAIL replace] aborted', res)
|
||||
} else if (isNavigationFailure(res, NavigationFailureType.redirected)) {
|
||||
console.warn('[NAV FAIL replace] redirected', res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
router.onError((e) => console.error('[router.onError]', e))
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
console.log('[beforeEach]', from.fullPath, '->', to.fullPath)
|
||||
return true
|
||||
})
|
||||
|
||||
router.afterEach((to, from, failure) => {
|
||||
if (failure) console.warn('[afterEach failure]', failure)
|
||||
else console.log('[afterEach ok]', from.fullPath, '->', to.fullPath)
|
||||
})
|
||||
// ===== /DEBUG NAV + TRACE =====
|
||||
|
||||
// ✅ mantém seus guards, mas agora a landing tem meta.public
|
||||
applyGuards(router)
|
||||
|
||||
export default router
|
||||
|
||||
36
src/router/router.configuracoes.js
Normal file
36
src/router/router.configuracoes.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// src/router/router.configuracoes.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
const configuracoesRoutes = {
|
||||
path: '/configuracoes',
|
||||
component: AppLayout,
|
||||
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['admin', 'tenant_admin', 'therapist']
|
||||
},
|
||||
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/layout/ConfiguracoesPage.vue'),
|
||||
redirect: { name: 'ConfiguracoesAgenda' },
|
||||
|
||||
children: [
|
||||
{
|
||||
path: 'agenda',
|
||||
name: 'ConfiguracoesAgenda',
|
||||
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
|
||||
}
|
||||
|
||||
// Futuro:
|
||||
// { path: 'clinica', name: 'ConfiguracoesClinica', component: () => import('@/layout/configuracoes/ConfiguracoesClinicaPage.vue') },
|
||||
// { path: 'intake', name: 'ConfiguracoesIntake', component: () => import('@/layout/configuracoes/ConfiguracoesIntakePage.vue') },
|
||||
// { path: 'conta', name: 'ConfiguracoesConta', component: () => import('@/layout/configuracoes/ConfiguracoesContaPage.vue') },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default configuracoesRoutes
|
||||
|
||||
32
src/router/router.me.js
Normal file
32
src/router/router.me.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// src/router/router.me.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
const meRoutes = {
|
||||
path: '/me',
|
||||
component: AppLayout,
|
||||
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['admin', 'tenant_admin', 'therapist', 'patient']
|
||||
},
|
||||
|
||||
children: [
|
||||
{
|
||||
// ✅ quando entrar em /me, manda pro perfil
|
||||
path: '',
|
||||
redirect: { name: 'MeuPerfil' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'perfil',
|
||||
name: 'MeuPerfil',
|
||||
component: () => import('@/views/pages/me/MeuPerfilPage.vue')
|
||||
}
|
||||
|
||||
// Futuro:
|
||||
// { path: 'preferencias', name: 'MePreferencias', component: () => import('@/pages/me/PreferenciasPage.vue') },
|
||||
// { path: 'notificacoes', name: 'MeNotificacoes', component: () => import('@/pages/me/NotificacoesPage.vue') },
|
||||
]
|
||||
}
|
||||
|
||||
export default meRoutes
|
||||
93
src/router/routes.admin.js
Normal file
93
src/router/routes.admin.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// src/router/routes.admin.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/admin',
|
||||
component: AppLayout,
|
||||
|
||||
meta: {
|
||||
// 🔐 Tudo aqui dentro exige login
|
||||
requiresAuth: true,
|
||||
|
||||
// 👤 Perfil de acesso
|
||||
role: 'tenant_admin'
|
||||
},
|
||||
|
||||
children: [
|
||||
// DASHBOARD
|
||||
{
|
||||
path: '',
|
||||
name: 'admin-dashboard',
|
||||
component: () => import('@/views/pages/admin/AdminDashboard.vue')
|
||||
},
|
||||
|
||||
// PACIENTES - LISTA
|
||||
{
|
||||
path: 'pacientes',
|
||||
name: 'admin-pacientes',
|
||||
component: () => import('@/views/pages/admin/pacientes/PatientsIndexPage.vue')
|
||||
},
|
||||
|
||||
// PACIENTES - CADASTRO (NOVO / EDITAR)
|
||||
{
|
||||
path: 'pacientes/cadastro',
|
||||
name: 'admin-pacientes-cadastro',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsCadastroPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'pacientes/cadastro/:id',
|
||||
name: 'admin-pacientes-cadastro-edit',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsCadastroPage.vue'),
|
||||
props: true
|
||||
},
|
||||
|
||||
// GRUPOS DE PACIENTES ✅
|
||||
{
|
||||
path: 'pacientes/grupos',
|
||||
name: 'admin-pacientes-grupos',
|
||||
component: () => import('@/views/pages/admin/pacientes/grupos/GruposPacientesPage.vue')
|
||||
},
|
||||
|
||||
// TAGS
|
||||
{
|
||||
path: 'pacientes/tags',
|
||||
name: 'admin-pacientes-tags',
|
||||
component: () => import('@/views/pages/admin/pacientes/tags/TagsPage.vue')
|
||||
},
|
||||
|
||||
// LINK EXTERNO
|
||||
{
|
||||
path: 'pacientes/link-externo',
|
||||
name: 'admin.pacientes.linkexterno',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsExternalLinkPage.vue')
|
||||
},
|
||||
|
||||
// CADASTROS RECEBIDOS
|
||||
{
|
||||
path: 'pacientes/cadastro/recebidos',
|
||||
name: 'admin.pacientes.recebidos',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/recebidos/CadastrosRecebidosPage.vue')
|
||||
},
|
||||
|
||||
// SEGURANÇA
|
||||
{
|
||||
path: 'settings/security',
|
||||
name: 'admin-settings-security',
|
||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||
},
|
||||
|
||||
// ================================
|
||||
// 🔒 MÓDULO PRO — Online Scheduling
|
||||
// ================================
|
||||
// Admin também gerencia agendamento online; mesma feature de gestão.
|
||||
// Você pode ter uma página admin para isso, ou reaproveitar a do therapist.
|
||||
{
|
||||
path: 'online-scheduling',
|
||||
name: 'admin-online-scheduling',
|
||||
component: () => import('@/views/pages/admin/OnlineSchedulingAdminPage.vue'),
|
||||
meta: {
|
||||
feature: 'online_scheduling.manage'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
45
src/router/routes.auth.js
Normal file
45
src/router/routes.auth.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export default {
|
||||
path: '/auth',
|
||||
children: [
|
||||
{
|
||||
path: 'login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/pages/auth/Login.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
|
||||
// ✅ Signup público, mas com URL /auth/signup
|
||||
{
|
||||
path: 'signup',
|
||||
name: 'signup',
|
||||
component: () => import('@/views/pages/public/Signup.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
{
|
||||
path: 'welcome',
|
||||
name: 'auth.welcome',
|
||||
component: () => import('@/views/pages/auth/Welcome.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
{
|
||||
path: 'reset-password',
|
||||
name: 'resetPassword',
|
||||
component: () => import('@/views/pages/auth/ResetPasswordPage.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'access',
|
||||
name: 'accessDenied',
|
||||
component: () => import('@/views/pages/auth/Access.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'error',
|
||||
name: 'error',
|
||||
component: () => import('@/views/pages/auth/Error.vue'),
|
||||
meta: { public: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
15
src/router/routes.billing.js
Normal file
15
src/router/routes.billing.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// src/router/routes.billing.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/upgrade',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'upgrade',
|
||||
component: () => import('@/views/pages/billing/UpgradePage.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
29
src/router/routes.demo.js
Normal file
29
src/router/routes.demo.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
// ✅ não use '/' aqui (conflita com HomeCards)
|
||||
path: '/demo',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true, role: 'tenant_admin' },
|
||||
children: [
|
||||
{ path: 'uikit/formlayout', name: 'uikit-formlayout', component: () => import('@/views/uikit/FormLayout.vue') },
|
||||
{ path: 'uikit/input', name: 'uikit-input', component: () => import('@/views/uikit/InputDoc.vue') },
|
||||
{ path: 'uikit/button', name: 'uikit-button', component: () => import('@/views/uikit/ButtonDoc.vue') },
|
||||
{ path: 'uikit/table', name: 'uikit-table', component: () => import('@/views/uikit/TableDoc.vue') },
|
||||
{ path: 'uikit/list', name: 'uikit-list', component: () => import('@/views/uikit/ListDoc.vue') },
|
||||
{ path: 'uikit/tree', name: 'uikit-tree', component: () => import('@/views/uikit/TreeDoc.vue') },
|
||||
{ path: 'uikit/panel', name: 'uikit-panel', component: () => import('@/views/uikit/PanelsDoc.vue') },
|
||||
{ path: 'uikit/overlay', name: 'uikit-overlay', component: () => import('@/views/uikit/OverlayDoc.vue') },
|
||||
{ path: 'uikit/media', name: 'uikit-media', component: () => import('@/views/uikit/MediaDoc.vue') },
|
||||
{ path: 'uikit/menu', name: 'uikit-menu', component: () => import('@/views/uikit/MenuDoc.vue') },
|
||||
{ path: 'uikit/message', name: 'uikit-message', component: () => import('@/views/uikit/MessagesDoc.vue') },
|
||||
{ path: 'uikit/file', name: 'uikit-file', component: () => import('@/views/uikit/FileDoc.vue') },
|
||||
{ path: 'uikit/charts', name: 'uikit-charts', component: () => import('@/views/uikit/ChartDoc.vue') },
|
||||
{ path: 'uikit/timeline', name: 'uikit-timeline', component: () => import('@/views/uikit/TimelineDoc.vue') },
|
||||
{ path: 'uikit/misc', name: 'uikit-misc', component: () => import('@/views/uikit/MiscDoc.vue') },
|
||||
{ path: 'utilities', name: 'blocks', component: () => import('@/views/utilities/Blocks.vue') },
|
||||
{ path: 'pages', name: 'start-documentation', component: () => import('@/views/pages/Documentation.vue') },
|
||||
{ path: 'pages/empty', name: 'pages-empty', component: () => import('@/views/pages/Empty.vue') },
|
||||
{ path: 'pages/crud', name: 'pages-crud', component: () => import('@/views/pages/Crud.vue') }
|
||||
]
|
||||
}
|
||||
15
src/router/routes.misc.js
Normal file
15
src/router/routes.misc.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
path: 'landing',
|
||||
name: 'landing',
|
||||
component: () => import('@/views/pages/Landing.vue')
|
||||
},
|
||||
{
|
||||
path: 'pages/notfound',
|
||||
name: 'notfound',
|
||||
component: () => import('@/views/pages/NotFound.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
19
src/router/routes.patient.js
Normal file
19
src/router/routes.patient.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/patient',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true, role: 'patient' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'patient-dashboard',
|
||||
component: () => import('@/views/pages/patient/PatientDashboard.vue')
|
||||
},
|
||||
{
|
||||
path: 'settings/security',
|
||||
name: 'patient-settings-security',
|
||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
26
src/router/routes.public.js
Normal file
26
src/router/routes.public.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('@/views/pages/HomeCards.vue')
|
||||
},
|
||||
|
||||
// ✅ LP (página separada da landing do template)
|
||||
{
|
||||
path: 'lp',
|
||||
name: 'lp',
|
||||
component: () => import('@/views/pages/public/landingpage-v1.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
|
||||
// ✅ cadastro externo
|
||||
{
|
||||
path: 'cadastro/paciente',
|
||||
name: 'public.patient.intake',
|
||||
component: () => import('@/views/pages/public/CadastroPacienteExterno.vue'),
|
||||
meta: { public: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
60
src/router/routes.saas.js
Normal file
60
src/router/routes.saas.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/saas',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true, saasAdmin: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'saas-dashboard',
|
||||
component: () => import('@/views/pages/saas/SaasDashboard.vue')
|
||||
},
|
||||
{
|
||||
path: 'plans',
|
||||
name: 'saas-plans',
|
||||
component: () => import('@/views/pages/saas/SaasPlansPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'plans-public',
|
||||
name: 'saas-plans-public',
|
||||
component: () => import('@/views/pages/saas/SaasPlansPublicPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'features',
|
||||
name: 'saas-features',
|
||||
component: () => import('@/views/pages/saas/SaasFeaturesPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'plan-features',
|
||||
name: 'saas-plan-features',
|
||||
component: () => import('@/views/pages/saas/SaasPlanFeaturesMatrixPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'subscriptions',
|
||||
name: 'saas-subscriptions',
|
||||
component: () => import('@/views/pages/saas/SaasSubscriptionsPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'subscription-events',
|
||||
name: 'saas-subscription-events',
|
||||
component: () => import('@/views/pages/saas/SaasSubscriptionEventsPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'subscription-health',
|
||||
name: 'saas-subscription-health',
|
||||
component: () => import('@/views/pages/saas/SaasSubscriptionHealthPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'subscription-intents',
|
||||
name: 'saas.subscriptionIntents',
|
||||
component: () => import('@/views/pages/saas/SubscriptionIntentsPage.vue'),
|
||||
meta: { requiresAuth: true, saasAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'tenants',
|
||||
name: 'saas-tenants',
|
||||
component: () => import('@/views/pages/saas/SaasPlaceholder.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
71
src/router/routes.therapist.js
Normal file
71
src/router/routes.therapist.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/therapist',
|
||||
component: AppLayout,
|
||||
|
||||
meta: {
|
||||
// 🔐 Tudo aqui dentro exige login
|
||||
requiresAuth: true,
|
||||
|
||||
// 👤 Perfil de acesso (seu guard atual usa meta.role)
|
||||
role: 'therapist'
|
||||
},
|
||||
|
||||
children: [
|
||||
// ======================
|
||||
// ✅ Dashboard Therapist
|
||||
// ======================
|
||||
{
|
||||
path: '',
|
||||
name: 'therapist-dashboard',
|
||||
component: () => import('@/views/pages/therapist/TherapistDashboard.vue')
|
||||
// herda requiresAuth + role do pai
|
||||
},
|
||||
|
||||
// ======================
|
||||
// ✅ Segurança
|
||||
// ======================
|
||||
{
|
||||
path: 'settings/security',
|
||||
name: 'therapist-settings-security',
|
||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||
// herda requiresAuth + role do pai
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 🔒 PRO — Online Scheduling (gestão interna)
|
||||
// ==========================================
|
||||
// feature gate via meta.feature:
|
||||
// - bloqueia rota (guard)
|
||||
// - menu pode desabilitar/ocultar (entitlementsStore.has)
|
||||
{
|
||||
path: 'online-scheduling',
|
||||
name: 'therapist-online-scheduling',
|
||||
component: () => import('@/views/pages/therapist/OnlineSchedulingPage.vue'),
|
||||
meta: {
|
||||
// ✅ herda requiresAuth + role do pai
|
||||
feature: 'online_scheduling.manage'
|
||||
}
|
||||
},
|
||||
|
||||
// =================================================
|
||||
// 🔒 PRO — Online Scheduling (página pública/config)
|
||||
// =================================================
|
||||
// Se você tiver/for criar a tela para configurar/visualizar a página pública,
|
||||
// use a chave granular:
|
||||
// - online_scheduling.public
|
||||
//
|
||||
// Dica de produto:
|
||||
// - "manage" = operação interna
|
||||
// - "public" = ajustes/preview/links
|
||||
//
|
||||
// Quando criar o arquivo, descomente.
|
||||
// {
|
||||
// path: 'online-scheduling/public',
|
||||
// name: 'therapist-online-scheduling-public',
|
||||
// component: () => import('@/views/pages/therapist/OnlineSchedulingPublicPage.vue'),
|
||||
// meta: { feature: 'online_scheduling.public' }
|
||||
// }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user