From c07bb81cd29d101c779fb0ed3f0c12c9e611aaf4 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Tue, 22 Sep 2026 16:47:08 +0000 Subject: [PATCH] feat: ativacao automatica de ponta a ponta das permissoes no BD e travas funcionais no app --- src/components/AppSidebar.tsx | 63 +++++++++++++++- src/components/ProtectedRouteByResource.tsx | 71 ++++++++++++++++-- src/components/sidebar/SidebarMenuItem.tsx | 50 +++++++------ src/config/routeConfig.ts | 3 +- src/hooks/useMatrizAcessos.ts | 82 +++++++++++++++++++-- 5 files changed, 227 insertions(+), 42 deletions(-) diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index db9a32c..f8d3494 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -19,7 +19,7 @@ export function AppSidebar() { const { setOpenMobile } = useSidebar(); const { isAdmin, loading: roleLoading } = useUserRole(); const { iconStyle } = useIconStyle(); - const { hasAccess, loading: permissionsLoading } = useUserPermissions(); + const { hasAccess, loading: permissionsLoading, resourcePermissions } = useUserPermissions(); const { canAccessTools, canInteractWithSpecialMenus } = usePermissionControl(); const isMobile = useIsMobile(); const [openGroups, setOpenGroups] = useState<{ [key: string]: boolean }>({ @@ -99,6 +99,63 @@ export function AppSidebar() { try { // Admin can always access everything if (isAdmin) return true; + + // Mapeamento de chave de menu/submenu da sidebar para resource_key do BD + const itemToResourceMap: Record = { + 'pcp': 'producao-pcp', + 'dashboard-producao': 'producao-dashboard', + 'apontamento-producao': 'producao-apontamento', + 'diario-producao': 'diario-producao', + 'prioridades-fabricacao': 'prioridades-fabricacao', + 'visao-geral': 'producao-visao', + 'visualizador-3d': 'visualizador-3d', + 'ordens-fabricacao': 'ofs-lista', + 'ficha-tecnica-of': 'cadastro-of', + 'cronograma': 'ofs-cronograma', + 'cadastro-pecas': 'cadastro-pecas', + 'equipamentos': 'equipamentos', + 'expedicao': 'expedicao', + 'estoque-main': 'estoque', + 'estoque': 'estoque', + 'solicitacao-compras': 'estoque-solicitacao-compras', + 'dashboard-obras': 'obra-dashboard', + 'configuracoes-obra': 'obra-configuracoes', + 'obra': 'obra-dashboard', + 'gerenciar-usuarios': 'user-management', + 'user-management': 'user-management', + 'configuracoes-gerais': 'configuracoes-gerais', + 'inconsistencias': 'ferramentas-inconsistencias', + 'ferramentas': 'ferramentas', + 'sistema': 'sistema', + 'sugestoes': 'sugestoes', + 'admin': 'admin', + 'dashboard': 'dashboard', + }; + + const resKey = itemToResourceMap[itemKey] || itemKey; + + // Se há restrição direta configurada no BD para este recurso + if (resourcePermissions && resourcePermissions[resKey]) { + return resourcePermissions[resKey] !== 'no_access'; + } + + // Se o usuário tem perfil de "apenas Smart" (tem modo-smart mas não tem esse recurso) + const temAcessoSmart = + resourcePermissions && + resourcePermissions['modo-smart'] && + resourcePermissions['modo-smart'] !== 'no_access'; + + const permsCadastradas = Object.keys(resourcePermissions || {}); + const soTemSmart = + temAcessoSmart && + permsCadastradas.length > 0 && + permsCadastradas.every( + (k) => k.startsWith('smart') || k === 'modo-smart' || resourcePermissions[k] === 'no_access' + ); + + if (soTemSmart && !resKey.startsWith('smart') && resKey !== 'modo-smart') { + return false; + } // Se o item requer permissão especial, verificar permissões específicas if (requiresSpecialPermission) { @@ -110,9 +167,7 @@ export function AppSidebar() { } } - // Users with any functional permission can see most menus - // Restriction will be applied in the pages/components themselves - return hasAccess(); + return hasAccess(resKey); } catch (error) { console.warn('Error checking item access:', error); return false; diff --git a/src/components/ProtectedRouteByResource.tsx b/src/components/ProtectedRouteByResource.tsx index 50ffbe6..2eaa9f9 100644 --- a/src/components/ProtectedRouteByResource.tsx +++ b/src/components/ProtectedRouteByResource.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import { Navigate } from 'react-router-dom'; +import { Navigate, Link } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; - import { useUserProfile } from '@/hooks/useUserProfile'; +import { useUserPermissions } from '@/hooks/useUserPermissions/useUserPermissions'; +import { ShieldAlert, Zap, ArrowLeft } from 'lucide-react'; +import { Button } from '@/components/ui/button'; interface ProtectedRouteByResourceProps { children: React.ReactNode; @@ -12,17 +14,22 @@ interface ProtectedRouteByResourceProps { export const ProtectedRouteByResource: React.FC = ({ children, + resourceKey, }) => { const { user, loading: authLoading } = useAuth(); const { isAdmin, loading: roleLoading } = useUserRole(); const { profile, loading: profileLoading } = useUserProfile(); + const { hasAccess, loading: permsLoading, resourcePermissions } = useUserPermissions(); - const loading = authLoading || roleLoading || profileLoading; + const loading = authLoading || roleLoading || profileLoading || permsLoading; if (loading) { return (
-
Carregando...
+
+
+
Validando permissões de acesso...
+
); } @@ -31,18 +38,66 @@ export const ProtectedRouteByResource: React.FC = return ; } - // Verificar se o usuário está ativo no sistema + // 1. Verificar se o usuário está ativo no sistema if (profile && profile.status !== 'active') { return (

Acesso Bloqueado

-

Seu usuário existe, mas encontra-se com status: {profile.status || 'pendente'}.

+

+ Seu usuário existe, mas encontra-se com status: {profile.status || 'pendente'}. +

Por favor, contate o administrador para aprovar o seu acesso.

); } - // Migração pro Logto: todos usuários autenticados têm acesso aos recursos. - // Sistema de permissões granulares fica pra depois. + // 2. Administradores têm acesso irrestrito + if (isAdmin) { + return <>{children}; + } + + // 3. Validação real de permissão granular pelo resourceKey + const podeAcessar = hasAccess(resourceKey); + + if (!podeAcessar) { + // Se o usuário tem acesso ao Modo Smart, redirecionar para a experiência dele + const temAcessoSmart = + resourcePermissions['modo-smart'] && + resourcePermissions['modo-smart'] !== 'no_access'; + + return ( +
+
+ +
+

Acesso Restrito a Este Módulo

+

+ Seu usuário não possui permissão concedida para acessar o recurso{' '} + + {resourceKey} + + . Solicite liberação na Matriz de Acessos ao administrador do sistema. +

+ +
+ {temAcessoSmart && ( + + )} + +
+
+ ); + } + return <>{children}; }; \ No newline at end of file diff --git a/src/components/sidebar/SidebarMenuItem.tsx b/src/components/sidebar/SidebarMenuItem.tsx index ab590cc..f1cf130 100644 --- a/src/components/sidebar/SidebarMenuItem.tsx +++ b/src/components/sidebar/SidebarMenuItem.tsx @@ -32,6 +32,19 @@ export const AppSidebarMenuItem: React.FC = ({ if (!canAccess && !isAdmin) { return null; // Don't show item if no access } + + // Filtrar subitens permitidos para o usuário logado + const subItemsPermitidos = (item.subItems || []).filter((subItem) => { + if (subItem.url === "/admin/theme-customization" && !isAdmin) { + return false; + } + return isAdmin || canAccessItem(subItem.key); + }); + + // Se o item for apenas um agrupador de subitens e todos estiverem bloqueados, não renderizar o agrupador + if (item.subItems && item.subItems.length > 0 && subItemsPermitidos.length === 0 && !isAdmin) { + return null; + } return ( @@ -51,29 +64,22 @@ export const AppSidebarMenuItem: React.FC = ({ - {item.subItems?.map((subItem) => { - // Filter theme customization for non-admins - if (subItem.url === "/admin/theme-customization" && !isAdmin) { - return null; - } - - return ( - - ( + + + onMenuItemClick(false)} > - onMenuItemClick(false)} - > - {subItem.icon && } - {subItem.title} - - - - ); - })} + {subItem.icon && } + {subItem.title} + + + + ))} diff --git a/src/config/routeConfig.ts b/src/config/routeConfig.ts index c2b2561..3ab0d0e 100644 --- a/src/config/routeConfig.ts +++ b/src/config/routeConfig.ts @@ -173,7 +173,8 @@ export const routeConfig: RouteDefinition[] = [ { path: '/visualizador-3d', component: () => import('@/pages/Visualizador3D'), - guard: 'protected', + guard: 'resource', + resourceKey: 'visualizador-3d', }, // ── Painel Industrial ──────────────────────────────────── diff --git a/src/hooks/useMatrizAcessos.ts b/src/hooks/useMatrizAcessos.ts index 11575be..4a4ad6f 100644 --- a/src/hooks/useMatrizAcessos.ts +++ b/src/hooks/useMatrizAcessos.ts @@ -216,14 +216,23 @@ export const useMatrizAcessos = () => { if (profError) throw profError; - // 2. Carregar recursos por privilégio - const { data: privResources, error: privResError } = await supabase + // 2. Carregar recursos específicos diretos por usuário (user_interface_permissions) + const { data: userInterfacePerms } = await supabase + .from('user_interface_permissions') + .select('user_id, resource_key, permission'); + + // Indexar por user_id -> resource_key -> permission + const userPermsMap: Record> = {}; + (userInterfacePerms || []).forEach((uip: any) => { + if (!userPermsMap[uip.user_id]) userPermsMap[uip.user_id] = {}; + userPermsMap[uip.user_id][uip.resource_key] = uip.permission; + }); + + // 3. Carregar recursos por privilégio como fallback + const { data: privResources } = await supabase .from('privilege_interface_resources') .select('privilege_id, resource_key'); - if (privResError) console.warn('Aviso ao carregar privilege_interface_resources:', privResError); - - // Indexar recursos por privilege_id const recursosPorPrivilege: Record> = {}; (privResources || []).forEach((pr: any) => { if (!recursosPorPrivilege[pr.privilege_id]) { @@ -241,17 +250,28 @@ export const useMatrizAcessos = () => { ? recursosPorPrivilege[privId] : new Set(); + const directPerms = userPermsMap[p.id] || {}; + const permissoesMap: Record = {}; GRUPOS_MODULOS.forEach((g) => { g.recursos.forEach((r) => { if (isAdmin) { permissoesMap[r.key] = 'criar'; + } else if (directPerms[r.key]) { + // Prioridade 1: Permissão direta do usuário + const perm = directPerms[r.key]; + if (perm === 'can_create_update_delete' || perm === 'can_admin' || perm === 'can_create_only') { + permissoesMap[r.key] = 'criar'; + } else if (perm === 'can_view_only') { + permissoesMap[r.key] = 'visualizar'; + } else { + permissoesMap[r.key] = 'nenhum'; + } } else if (recursosSet.has(`${r.key}:edit`) || recursosSet.has(`${r.key}_edit`)) { permissoesMap[r.key] = 'criar'; } else if (recursosSet.has(r.key)) { - // Se tiver o recurso base, verificar se o perfil é apenas visualizador ou editor - permissoesMap[r.key] = 'criar'; // padrão ativo é criar + permissoesMap[r.key] = 'visualizar'; } else { permissoesMap[r.key] = 'nenhum'; } @@ -324,6 +344,32 @@ export const useMatrizAcessos = () => { ); try { + // 1. Gravação direta e individual por usuário na tabela user_interface_permissions (Prioridade Máxima) + const dbPermission = + novoNivel === 'criar' + ? 'can_create_update_delete' + : novoNivel === 'visualizar' + ? 'can_view_only' + : 'no_access'; + + const { error: uipError } = await supabase + .from('user_interface_permissions') + .upsert( + { + user_id: userId, + resource_key: resourceKey, + permission: dbPermission as any, + updated_at: new Date().toISOString(), + }, + { onConflict: 'user_id,resource_key' } + ); + + if (uipError) { + console.error('Erro ao salvar em user_interface_permissions:', uipError); + throw uipError; + } + + // 2. Compatibilidade legada com privilégios de grupo (se houver) if (user.privilegeId) { // Remover permissões antigas do recurso await supabase @@ -379,6 +425,28 @@ export const useMatrizAcessos = () => { ); try { + // 1. Gravação direta em lote na tabela user_interface_permissions + const uipRecords = Object.entries(novoMap).map(([resKey, nivel]) => ({ + user_id: userId, + resource_key: resKey, + permission: (nivel === 'criar' + ? 'can_create_update_delete' + : nivel === 'visualizar' + ? 'can_view_only' + : 'no_access') as any, + updated_at: new Date().toISOString(), + })); + + const { error: batchError } = await supabase + .from('user_interface_permissions') + .upsert(uipRecords, { onConflict: 'user_id,resource_key' }); + + if (batchError) { + console.error('Erro no batch upsert em user_interface_permissions:', batchError); + throw batchError; + } + + // 2. Compatibilidade legada com privilégios de grupo if (user.privilegeId) { // Limpar recursos atuais do privilégio await supabase