diff --git a/src/composables/useAuth.js b/src/composables/useAuth.js index dc32818..2511cfd 100644 --- a/src/composables/useAuth.js +++ b/src/composables/useAuth.js @@ -1,3 +1,50 @@ +/** + * --------------------------------------------------------- + * useAuth() + * --------------------------------------------------------- + * + * Stack do projeto: + * - Vue 3 (Composition API) + * - PrimeVue (UI) + * - Supabase (Auth + Database) + * + * Responsabilidade: + * Camada global de AUTENTICAÇÃO baseada no Supabase. + * + * O que este composable faz: + * - Obtém a sessão atual do Supabase (auth.getSession) + * - Mantém o estado reativo do usuário autenticado + * - Escuta mudanças de autenticação (login, logout, refresh de token) + * - Expõe apenas a identidade do usuário (user) + * + * O que ele NÃO faz: + * - Não controla permissões + * - Não valida roles + * - Não decide acesso a telas ou botões + * - Não aplica regras de plano (Free/Pro) + * + * Conceito arquitetural: + * Este arquivo trata apenas de IDENTIDADE (Auth). + * + * Auth → "Quem é o usuário autenticado?" + * AuthZ → "O que esse usuário pode acessar ou executar?" + * + * A AUTORIZAÇÃO deve ser tratada em outra camada, + * como por exemplo: + * - useAuthz() + * - tenantStore (membership.role) + * - entitlementsStore (features do plano) + * + * Observação importante: + * O role do usuário NÃO vem do Supabase Auth. + * Ele é definido na tabela de membership (multi-tenant). + * + * Portanto: + * Nunca utilizar apenas `user` para controle de acesso. + * + * Esse composable é apenas a base de identidade do sistema. + */ + import { ref, onMounted } from 'vue' import { supabase } from '@/lib/supabase/client' diff --git a/src/composables/useRoleGuard.js b/src/composables/useRoleGuard.js new file mode 100644 index 0000000..d57e6f7 --- /dev/null +++ b/src/composables/useRoleGuard.js @@ -0,0 +1,115 @@ +import { computed } from 'vue' +import { useTenantStore } from '@/stores/tenantStore' + +/** + * --------------------------------------------------------- + * useRoleGuard() — RBAC puro (somente PAPEL do tenant) + * --------------------------------------------------------- + * + * Objetivo: + * Controlar visibilidade/ações por PAPEL dentro do tenant (clínica). + * 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) + * + * 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 + * + * O que ele NÃO resolve (de propósito): + * - liberar feature por plano (Free/Pro) + * - limitar módulos / recursos contratados + * + * Para controle por plano, use o entStore: + * - entStore.can('feature_key') + * + * Padrão recomendado (RBAC + Plano): + * Quando algo depende do PLANO e do PAPEL, combine no template: + * + * v-if="entStore.can('online_scheduling.manage') && canSee('settings.view')" + * + * Interpretação: + * - Gate A (Plano): o tenant tem a feature liberada? + * - Gate B (Papel): o usuário, pelo papel, pode ver/usar isso? + * + * Nota de segurança: + * Isso controla UI/rotas (experiência). 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', + 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 isTenantAdmin = computed(() => role.value === ROLES.ADMIN) + const isTherapist = computed(() => role.value === ROLES.THERAPIST) + const isPatient = computed(() => role.value === ROLES.PATIENT) + const isStaff = computed(() => [ROLES.ADMIN, ROLES.THERAPIST].includes(role.value)) + + // 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], + + // 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], + }) + + /** + * 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 + + const allowed = rbac[key] + if (!allowed) return false + + return allowed.includes(r) + } + + return { + // estado + role, + isReady, + + // constantes & helpers + ROLES, + isTenantAdmin, + isTherapist, + isPatient, + isStaff, + + // API + canSee + } +} \ No newline at end of file diff --git a/src/features/agenda/components/AgendaEventDialog.vue b/src/features/agenda/components/AgendaEventDialog.vue index d9b373a..041ab33 100644 --- a/src/features/agenda/components/AgendaEventDialog.vue +++ b/src/features/agenda/components/AgendaEventDialog.vue @@ -1,5 +1,6 @@ +