This commit is contained in:
Leonardo
2026-03-06 06:37:13 -03:00
parent d58dc21297
commit f733db8436
146 changed files with 43436 additions and 12779 deletions
+81
View File
@@ -0,0 +1,81 @@
// src/composables/usePlatformPermissions.js
//
// Permissões de PLATAFORMA (globais, não vinculadas a tenant).
// Distinto do RBAC de tenant (useRoleGuard).
//
// O campo `platform_roles text[]` na tabela `profiles` do Supabase
// armazena papéis globais da plataforma. Ex.: ['editor'].
//
// Quem pode atribuir: somente o saas_admin.
// Quem pode ter: qualquer usuário autenticado (exceto paciente).
//
// PAPÉIS DE PLATAFORMA DISPONÍVEIS:
// 'editor' — pode criar e gerenciar cursos/módulos da plataforma de microlearning.
//
import { ref, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { sessionUser } from '@/app/session'
// cache em módulo (evita queries repetidas por navegação)
let _cachedUid = null
let _cachedRoles = null
export function usePlatformPermissions () {
const platformRoles = ref(_cachedRoles ?? [])
const loading = ref(false)
const error = ref(null)
async function load (force = false) {
const uid = sessionUser.value?.id
if (!uid) {
platformRoles.value = []
return
}
// cache por uid (invalida se usuário mudou)
if (!force && _cachedUid === uid && _cachedRoles !== null) {
platformRoles.value = _cachedRoles
return
}
loading.value = true
error.value = null
try {
const { data, err } = await supabase
.from('profiles')
.select('platform_roles')
.eq('id', uid)
.single()
const roles = !err && Array.isArray(data?.platform_roles) ? data.platform_roles : []
_cachedUid = uid
_cachedRoles = roles
platformRoles.value = roles
} catch (e) {
console.warn('[usePlatformPermissions] load falhou:', e)
error.value = e
platformRoles.value = []
} finally {
loading.value = false
}
}
function invalidate () {
_cachedUid = null
_cachedRoles = null
platformRoles.value = []
}
const isEditor = computed(() => platformRoles.value.includes('editor'))
return {
platformRoles,
loading,
error,
isEditor,
load,
invalidate
}
}
+107 -51
View File
@@ -4,6 +4,7 @@ import { useTenantStore } from '@/stores/tenantStore'
/**
* ---------------------------------------------------------
* useRoleGuard() — RBAC puro (somente PAPEL do tenant)
* + testMODE (modo operacional de teste)
* ---------------------------------------------------------
*
* Objetivo:
@@ -11,82 +12,133 @@ import { useTenantStore } from '@/stores/tenantStore'
* Aqui NÃO entra plano, módulos ou features pagas.
*
* Fonte da verdade do papel (tenant role):
* - public.tenant_members.role → 'tenant_admin' | 'therapist' | 'patient'
* - no frontend: tenantStore.membership.role (ou fallback tenantStore.activeRole)
* - public.tenant_members.role
* → 'saas' | 'tenant_admin' | 'therapist' | 'patient'
*
* O que este composable resolve:
* - "Esse papel pode ver/usar este elemento?"
* Ex:
* - paciente não vê botão Configurações
* - therapist e tenant_admin veem
* ---------------------------------------------------------
* testMODE — INTERRUPTOR OPERACIONAL
* ---------------------------------------------------------
*
* O que ele NÃO resolve (de propósito):
* - liberar feature por plano (Free/Pro)
* - limitar módulos / recursos contratados
* testMODE NÃO é regra de negócio.
* Ele serve para:
* - testar módulos antes de liberar no plano
* - visualizar telas ainda não contratadas
* - validar UI sem alterar regras de plano
*
* Para controle por plano, use o entStore:
* - entStore.can('feature_key')
* COMPORTAMENTO:
*
* Padrão recomendado (RBAC + Plano):
* Quando algo depende do PLANO e do PAPEL, combine no template:
* - TEST_MODE_ROLES = []
* → testMODE OFF (ninguém vê)
*
* v-if="entStore.can('online_scheduling.manage') && canSee('settings.view')"
* - TEST_MODE_ROLES = [ROLES.ADMIN]
* → Apenas tenant_admin vê elementos marcados como testMODE
*
* - TEST_MODE_ROLES = [ROLES.ADMIN, ROLES.THERAPIST]
* → Admin e Therapist veem
*
* - TEST_MODE_ROLES = [ROLES.SAAS, ROLES.ADMIN, ROLES.THERAPIST, ROLES.PATIENT]
* → testMODE ON geral
*
* ---------------------------------------------------------
* COMO IMPORTAR NO COMPONENTE
* ---------------------------------------------------------
*
* 1) Importação padrão:
*
* import { useRoleGuard } from '@/composables/useRoleGuard'
*
* const { canSee, canSeeOrTest } = useRoleGuard()
*
*
* 2) Se for usar apenas testMODE:
*
* import { useRoleGuard } from '@/composables/useRoleGuard'
*
* const { canSee } = useRoleGuard()
*
*
* ---------------------------------------------------------
* EXEMPLOS DE USO NO TEMPLATE
* ---------------------------------------------------------
*
* 1) Elemento puramente experimental:
*
* <Button
* v-if="canSee('testMODE')"
* label="Botão em teste"
* />
*
*
* 2) Liberar visualização mesmo sem plano:
*
* <Button
* v-if="entStore.can('online_scheduling.manage') || canSee('testMODE')"
* label="Agendamento Online"
* @click="..."
* />
*
* Interpretação:
* - Gate A (Plano): o tenant tem a feature liberada?
* - Gate B (Papel): o usuário, pelo papel, pode ver/usar isso?
* - Gate A → plano liberado
* - Gate B → testMODE ativo
*
* Nota de segurança:
* Isso controla UI/rotas (experiência). Segurança real deve existir no backend (RLS).
*
* 3) Usando helper:
*
* <Button
* v-if="canSeeOrTest('settings.view')"
* ...
* />
*
* Isso significa:
* - Usuário pode ver normalmente pela regra RBAC
* OU
* - testMODE está ativo
*
*
* ---------------------------------------------------------
* IMPORTANTE:
* testMODE controla apenas UI.
* Segurança real deve existir no backend (RLS).
* ---------------------------------------------------------
*/
export function useRoleGuard () {
const tenantStore = useTenantStore()
// Roles confirmados no seu banco (tenant_members.role)
const ROLES = Object.freeze({
ADMIN: 'tenant_admin',
SAAS: 'saas',
ADMIN: 'clinic_admin',
SUPERVISOR: 'supervisor',
THERAPIST: 'therapist',
PATIENT: 'patient'
})
// Papel atual no tenant ativo
const role = computed(() => tenantStore.membership?.role ?? tenantStore.activeRole ?? null)
// Opcional: útil se você quiser segurar render até carregar
const isReady = computed(() => !!role.value)
// Helpers semânticos
const isSaas = computed(() => role.value === ROLES.SAAS)
const isTenantAdmin = computed(() => role.value === ROLES.ADMIN)
const isSupervisor = computed(() => role.value === ROLES.SUPERVISOR)
const isTherapist = computed(() => role.value === ROLES.THERAPIST)
const isPatient = computed(() => role.value === ROLES.PATIENT)
const isStaff = computed(() => [ROLES.ADMIN, ROLES.THERAPIST].includes(role.value))
const isStaff = computed(() => [ROLES.ADMIN, ROLES.SUPERVISOR, ROLES.THERAPIST].includes(role.value))
const TEST_MODE_ROLES = Object.freeze([
ROLES.ADMIN,
ROLES.SUPERVISOR,
ROLES.THERAPIST,
ROLES.SAAS,
ROLES.PATIENT
])
// Matriz RBAC (somente por papel)
// Dica: mantenha chaves no padrão "modulo.acao"
const rbac = Object.freeze({
// Botões/telas de configuração do tenant
'settings.view': [ROLES.ADMIN, ROLES.THERAPIST],
'testMODE': TEST_MODE_ROLES,
// Perfil/conta (normalmente todos)
'profile.view': [ROLES.ADMIN, ROLES.THERAPIST, ROLES.PATIENT],
// Segurança (normalmente todos; ajuste se quiser restringir)
'security.view': [ROLES.ADMIN, ROLES.THERAPIST, ROLES.PATIENT]
// Exemplos futuros:
// 'agenda.view': [ROLES.ADMIN, ROLES.THERAPIST, ROLES.PATIENT],
// 'agenda.manage': [ROLES.ADMIN, ROLES.THERAPIST],
'settings.view': [ROLES.ADMIN, ROLES.SUPERVISOR, ROLES.THERAPIST],
'profile.view': [ROLES.ADMIN, ROLES.SUPERVISOR, ROLES.THERAPIST, ROLES.PATIENT],
'security.view': [ROLES.ADMIN, ROLES.SUPERVISOR, ROLES.THERAPIST, ROLES.PATIENT]
})
/**
* canSee(key)
* Retorna true se o PAPEL atual estiver autorizado para a chave RBAC.
*
* Política segura:
* - se não carregou role → false
* - se não existe mapeamento pra key → false
*/
function canSee (key) {
const r = role.value
if (!r) return false
@@ -97,19 +149,23 @@ export function useRoleGuard () {
return allowed.includes(r)
}
function canSeeOrTest (key) {
return canSee(key) || canSee('testMODE')
}
return {
// estado
role,
isReady,
// constantes & helpers
ROLES,
isSaas,
isTenantAdmin,
isSupervisor,
isTherapist,
isPatient,
isStaff,
// API
canSee
canSee,
canSeeOrTest
}
}
@@ -44,6 +44,7 @@ export function useUserSettingsPersistence() {
primary_color: patch.primary_color ?? layoutConfig.primary ?? 'noir',
surface_color: patch.surface_color ?? layoutConfig.surface ?? 'slate',
menu_mode: patch.menu_mode ?? layoutConfig.menuMode ?? 'static',
layout_variant: patch.layout_variant ?? layoutConfig.variant ?? 'classic',
updated_at: new Date().toISOString()
}
@@ -83,6 +84,7 @@ export function useUserSettingsPersistence() {
primary_color: pendingPatch.value.primary_color ?? layoutConfig.primary,
surface_color: pendingPatch.value.surface_color ?? layoutConfig.surface,
menu_mode: pendingPatch.value.menu_mode ?? layoutConfig.menuMode,
layout_variant: pendingPatch.value.layout_variant ?? layoutConfig.variant ?? 'classic',
updated_at: new Date().toISOString()
}