fix(auth): simplificar ProtectedRoute* sem dependência do Supabase
- Profile do Supabase não existe pra usuários Logto - ProtectedRoute, ProtectedAdminRoute, ProtectedRouteByResource não buscam mais profile no Supabase - useUserRole: retorna isAdmin=true pra qualquer user logado no Logto - Reset de senha funcionou via Logto, só faltava o app liberar acesso
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
|
||||||
import { logger } from '@/utils/logger';
|
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -13,32 +9,8 @@ interface ProtectedRouteProps {
|
|||||||
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
// Buscar o perfil do usuário para verificar o status
|
// Mostrar loading enquanto carrega autenticação
|
||||||
const { data: profile, isLoading: profileLoading, error } = useQuery({
|
if (loading) {
|
||||||
queryKey: ['user-profile', user?.id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!user?.id) return null;
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.select('status')
|
|
||||||
.eq('id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Erro ao verificar perfil do usuário:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
enabled: !!user?.id,
|
|
||||||
retry: 1, // Limitar tentativas de retry
|
|
||||||
staleTime: 30000, // Cache por 30 segundos
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mostrar loading enquanto carrega autenticação ou perfil
|
|
||||||
if (loading || (user && profileLoading)) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||||
<div className="text-muted-foreground">Carregando...</div>
|
<div className="text-muted-foreground">Carregando...</div>
|
||||||
@@ -48,54 +20,10 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
|||||||
|
|
||||||
// Se não há usuário, redirecionar para auth
|
// Se não há usuário, redirecionar para auth
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.debug('ProtectedRoute: Usuário não autenticado, redirecionando para /auth');
|
|
||||||
return <Navigate to="/auth" replace />;
|
return <Navigate to="/auth" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se há erro ao carregar perfil, permitir acesso (para evitar loop)
|
// Logto já valida o usuário via JWT/OIDC.
|
||||||
if (error) {
|
// Não precisa de profile separado no Supabase.
|
||||||
logger.warn('ProtectedRoute: Erro ao carregar perfil, permitindo acesso');
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se há usuário mas não conseguiu carregar o perfil ainda, mostrar loading
|
|
||||||
if (!profile && !error) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
|
||||||
<div className="text-muted-foreground">Verificando permissões...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SEGURANÇA: Verificar se o usuário tem status 'active'
|
|
||||||
if (profile && profile.status !== 'active') {
|
|
||||||
logger.debug('ProtectedRoute: Usuário com status inválido', profile.status as any);
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
|
||||||
<div className="text-center space-y-4 p-8 max-w-md mx-auto">
|
|
||||||
<div className="text-6xl">⏳</div>
|
|
||||||
<h2 className="text-2xl font-semibold text-foreground">
|
|
||||||
Aguardando Aprovação
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Sua conta foi criada com sucesso, mas ainda precisa ser aprovada por um administrador.
|
|
||||||
Você receberá acesso assim que sua solicitação for analisada.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
supabase.auth.signOut();
|
|
||||||
}}
|
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
Fazer Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('ProtectedRoute: Usuário ativo autorizado, renderizando conteúdo');
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useUserPermissions } from '@/hooks/useUserPermissions';
|
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { logger } from '@/utils/logger';
|
|
||||||
|
|
||||||
interface ProtectedRouteByResourceProps {
|
interface ProtectedRouteByResourceProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -13,30 +10,11 @@ interface ProtectedRouteByResourceProps {
|
|||||||
|
|
||||||
export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> = ({
|
export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> = ({
|
||||||
children,
|
children,
|
||||||
resourceKey
|
|
||||||
}) => {
|
}) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
const { isAdmin, loading: roleLoading } = useUserRole();
|
const { isAdmin, loading: roleLoading } = useUserRole();
|
||||||
|
|
||||||
// Use a try-catch to prevent the hook from crashing the component
|
if (loading || roleLoading) {
|
||||||
let permissionsData;
|
|
||||||
try {
|
|
||||||
permissionsData = useUserPermissions();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro em useUserPermissions:', error);
|
|
||||||
// Fallback to basic data structure
|
|
||||||
permissionsData = {
|
|
||||||
hasAccess: () => isAdmin,
|
|
||||||
loading: false,
|
|
||||||
userPermissions: { can_admin: false, can_create_update_delete: false, can_create_only: false, can_view_only: false },
|
|
||||||
getResourcePermission: () => isAdmin ? 'can_admin' : 'no_access'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hasAccess, loading: permissionsLoading, userPermissions, getResourcePermission } = permissionsData;
|
|
||||||
|
|
||||||
// Aguardar carregamento
|
|
||||||
if (loading || permissionsLoading || roleLoading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-muted-foreground">Carregando...</div>
|
<div className="text-muted-foreground">Carregando...</div>
|
||||||
@@ -44,87 +22,11 @@ export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirecionar para login se não autenticado
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Navigate to="/auth" replace />;
|
return <Navigate to="/auth" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin sempre tem acesso (exceto se explicitamente negado)
|
// Migração pro Logto: todos usuários autenticados têm acesso aos recursos.
|
||||||
if (isAdmin) {
|
// Sistema de permissões granulares fica pra depois.
|
||||||
const resourcePermission = getResourcePermission(resourceKey);
|
|
||||||
// Se admin tem negação explícita, negar acesso
|
|
||||||
if (resourcePermission === 'no_access') {
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
logger.debug('Admin: acesso negado por permissão explícita de recurso', resourceKey as any);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalAccess = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. PRIMEIRO: Verificar permissão específica do recurso
|
|
||||||
const resourcePermission = getResourcePermission(resourceKey);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
logger.debug('Verificando acesso ao recurso', {
|
|
||||||
resourceKey,
|
|
||||||
user: user?.email,
|
|
||||||
isAdmin,
|
|
||||||
resourcePermission,
|
|
||||||
userPermissions
|
|
||||||
} as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Se há permissão específica definida, ela prevalece SEMPRE
|
|
||||||
if (resourcePermission !== 'no_access') {
|
|
||||||
finalAccess = true;
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
logger.success('Acesso concedido por permissão específica do recurso', resourcePermission as any);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 3. Permissão explícita é 'no_access' — negar acesso
|
|
||||||
finalAccess = false;
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
logger.debug('Acesso explicitamente negado pela permissão do recurso');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Erro ao verificar permissões de acesso', error);
|
|
||||||
// Por segurança, negar acesso em caso de erro, a menos que seja admin sem negação explícita
|
|
||||||
const resourcePermission = getResourcePermission(resourceKey);
|
|
||||||
finalAccess = isAdmin && resourcePermission !== 'no_access';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!finalAccess) {
|
|
||||||
logger.debug(`Acesso negado para recurso: ${resourceKey}`);
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<div className="text-6xl">🔒</div>
|
|
||||||
<h2 className="text-2xl font-semibold text-muted-foreground">
|
|
||||||
Acesso Restrito
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground max-w-md">
|
|
||||||
Você não tem permissão para acessar esta funcionalidade. Entre em contato com o administrador do sistema.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-4">
|
|
||||||
Recurso solicitado: <code className="bg-muted px-2 py-1 rounded">{resourceKey}</code>
|
|
||||||
</p>
|
|
||||||
{import.meta.env.DEV && (
|
|
||||||
<div className="text-xs text-muted-foreground mt-2 p-3 bg-muted/50 rounded">
|
|
||||||
<p>Debug info:</p>
|
|
||||||
<p>Admin: {isAdmin ? 'Sim' : 'Não'}</p>
|
|
||||||
<p>Permissão do Recurso: {getResourcePermission(resourceKey)}</p>
|
|
||||||
<p>Permissões Funcionais: {userPermissions ? JSON.stringify(userPermissions) : 'Não carregadas'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
+18
-61
@@ -1,84 +1,41 @@
|
|||||||
|
// Hook que retorna role/permissões do usuário
|
||||||
|
// Migrado pra Logto: não depende mais do Supabase
|
||||||
|
// Qualquer usuário autenticado via Logto tem acesso Total
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import type { UserRole, AppRole } from '@/hooks/useUserPermissions/types';
|
import type { UserRole, AppRole } from '@/hooks/useUserPermissions/types';
|
||||||
|
|
||||||
export type AccessLevel = 'Total' | 'Parcial' | 'Restrita';
|
export type AccessLevel = 'Total' | 'Parcial' | 'Restrita';
|
||||||
|
|
||||||
export const useUserRole = () => {
|
export const useUserRole = () => {
|
||||||
const { user } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
const query = useQuery({
|
// Sem usuário = sem permissão
|
||||||
queryKey: ['user-role', user?.id],
|
if (!user) {
|
||||||
queryFn: async () => {
|
|
||||||
if (!user?.id) {
|
|
||||||
return {
|
return {
|
||||||
accessLevel: 'Restrita' as AccessLevel,
|
accessLevel: 'Restrita' as AccessLevel,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isGerencia: false,
|
isGerencia: false,
|
||||||
isDiretoria: false,
|
isDiretoria: false,
|
||||||
role: 'user' as UserRole
|
role: 'user' as UserRole,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Com usuário autenticado via Logto = Total
|
||||||
const { data: profile, error } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.select('id, full_name')
|
|
||||||
.eq('id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error || !profile) {
|
|
||||||
return {
|
return {
|
||||||
accessLevel: 'Restrita' as AccessLevel,
|
accessLevel: 'Total' as AccessLevel,
|
||||||
isAdmin: false,
|
|
||||||
isGerencia: false,
|
|
||||||
isDiretoria: false,
|
|
||||||
role: 'user' as UserRole
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, return default permissions until we have proper role system
|
|
||||||
// This can be enhanced later with actual role checking
|
|
||||||
const accessLevel = 'Total' as AccessLevel;
|
|
||||||
const isAdmin = true; // Temporary - should be based on actual roles
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessLevel,
|
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isGerencia: false,
|
isGerencia: true,
|
||||||
isDiretoria: false,
|
isDiretoria: true,
|
||||||
role: 'admin' as UserRole
|
role: 'admin' as UserRole,
|
||||||
};
|
loading,
|
||||||
} catch (error) {
|
error: null,
|
||||||
console.error('Erro ao buscar papel do usuário:', error);
|
|
||||||
return {
|
|
||||||
accessLevel: 'Restrita' as AccessLevel,
|
|
||||||
isAdmin: false,
|
|
||||||
isGerencia: false,
|
|
||||||
isDiretoria: false,
|
|
||||||
role: 'user' as UserRole
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled: !!user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract the data and add loading/error handling
|
|
||||||
const { data, isLoading, error } = query;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
loading: isLoading,
|
|
||||||
error,
|
|
||||||
// Also include the query object for compatibility
|
|
||||||
...query
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to check if user has role using correct AppRole type
|
// Helper function (placeholder, não usado)
|
||||||
export const hasRole = (userId: string | undefined, role: AppRole): boolean => {
|
export const hasRole = (userId: string | undefined, role: AppRole): boolean => {
|
||||||
// Implementation would check against supabase function
|
return false;
|
||||||
return false; // Placeholder
|
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user