feat: ativacao automatica de ponta a ponta das permissoes no BD e travas funcionais no app
This commit is contained in:
@@ -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<string, string> = {
|
||||
'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;
|
||||
|
||||
@@ -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<ProtectedRouteByResourceProps> = ({
|
||||
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 (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-muted-foreground">Carregando...</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<div className="text-sm text-muted-foreground">Validando permissões de acesso...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,18 +38,66 @@ export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> =
|
||||
return <Navigate to="/auth" replace />;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-background text-center p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Acesso Bloqueado</h1>
|
||||
<p className="text-muted-foreground mb-2">Seu usuário existe, mas encontra-se com status: <strong>{profile.status || 'pendente'}</strong>.</p>
|
||||
<p className="text-muted-foreground mb-2">
|
||||
Seu usuário existe, mas encontra-se com status: <strong>{profile.status || 'pendente'}</strong>.
|
||||
</p>
|
||||
<p className="text-muted-foreground">Por favor, contate o administrador para aprovar o seu acesso.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-[75vh] p-6 text-center">
|
||||
<div className="p-4 rounded-2xl bg-amber-500/10 border border-amber-500/20 text-amber-500 mb-4 shadow-sm">
|
||||
<ShieldAlert className="w-12 h-12 stroke-[1.5]" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2">Acesso Restrito a Este Módulo</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-md mb-6">
|
||||
Seu usuário não possui permissão concedida para acessar o recurso{' '}
|
||||
<code className="px-2 py-0.5 rounded bg-muted font-mono text-xs text-foreground">
|
||||
{resourceKey}
|
||||
</code>
|
||||
. Solicite liberação na Matriz de Acessos ao administrador do sistema.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
{temAcessoSmart && (
|
||||
<Button asChild className="bg-amber-600 hover:bg-amber-700 text-white font-medium">
|
||||
<Link to="/smart">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Ir para o Modo Smart
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/dashboard">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Voltar ao Início
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -32,6 +32,19 @@ export const AppSidebarMenuItem: React.FC<SidebarMenuItemProps> = ({
|
||||
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 (
|
||||
<SidebarMenuItem key={item.key || item.title}>
|
||||
@@ -51,29 +64,22 @@ export const AppSidebarMenuItem: React.FC<SidebarMenuItemProps> = ({
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.subItems?.map((subItem) => {
|
||||
// Filter theme customization for non-admins
|
||||
if (subItem.url === "/admin/theme-customization" && !isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuSubItem key={subItem.key || subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={isActive(subItem.url)}
|
||||
{subItemsPermitidos.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.key || subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={isActive(subItem.url)}
|
||||
>
|
||||
<Link
|
||||
to={subItem.url}
|
||||
onClick={() => onMenuItemClick(false)}
|
||||
>
|
||||
<Link
|
||||
to={subItem.url}
|
||||
onClick={() => onMenuItemClick(false)}
|
||||
>
|
||||
{subItem.icon && <subItem.icon className="mr-2 h-4 w-4" />}
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
{subItem.icon && <subItem.icon className="mr-2 h-4 w-4" />}
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
@@ -173,7 +173,8 @@ export const routeConfig: RouteDefinition[] = [
|
||||
{
|
||||
path: '/visualizador-3d',
|
||||
component: () => import('@/pages/Visualizador3D'),
|
||||
guard: 'protected',
|
||||
guard: 'resource',
|
||||
resourceKey: 'visualizador-3d',
|
||||
},
|
||||
|
||||
// ── Painel Industrial ────────────────────────────────────
|
||||
|
||||
@@ -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<string, Record<string, string>> = {};
|
||||
(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<string, Set<string>> = {};
|
||||
(privResources || []).forEach((pr: any) => {
|
||||
if (!recursosPorPrivilege[pr.privilege_id]) {
|
||||
@@ -241,17 +250,28 @@ export const useMatrizAcessos = () => {
|
||||
? recursosPorPrivilege[privId]
|
||||
: new Set<string>();
|
||||
|
||||
const directPerms = userPermsMap[p.id] || {};
|
||||
|
||||
const permissoesMap: Record<string, NivelAcesso> = {};
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user