ZERADO
This commit is contained in:
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user