- Persist `roles` array in localStorage on login and session restore. - Reconcile `userType` from roles returned by the `user-info` function. - `ProtectedRoute` now accepts `requiredUserType?: UserType[]` and `requiredRoles?: string[]` and evaluates multi-role permission (OR semantics). - Minor adjustments in `useAuth` and debug logs to ensure consistent `profile` and `roles` restoration. - Main files changed: `hooks/useAuth.tsx`, `components/ProtectedRoute.tsx`, `types/auth.ts.
184 lines
6.8 KiB
TypeScript
184 lines
6.8 KiB
TypeScript
'use client'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useAuth } from '@/hooks/useAuth'
|
|
import type { UserType } from '@/types/auth'
|
|
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
|
|
|
|
interface ProtectedRouteProps {
|
|
children: React.ReactNode
|
|
requiredUserType?: UserType[]
|
|
requiredRoles?: string[]
|
|
}
|
|
|
|
export default function ProtectedRoute({
|
|
children,
|
|
requiredUserType,
|
|
requiredRoles
|
|
}: ProtectedRouteProps) {
|
|
const { authStatus, user } = useAuth()
|
|
const router = useRouter()
|
|
const isRedirecting = useRef(false)
|
|
const [mounted, setMounted] = useState(false)
|
|
const [accessDenied, setAccessDenied] = useState(false)
|
|
|
|
// Computa permissões de forma síncrona a partir do user e dos roles/userType
|
|
const computeHasPermission = () => {
|
|
// sem requisitos, permite
|
|
if ((!requiredUserType || requiredUserType.length === 0) && (!requiredRoles || requiredRoles.length === 0)) return true
|
|
if (!user) return false
|
|
|
|
const userRoles = (user as any).roles || []
|
|
|
|
// checa requiredRoles (strings arbitrárias de papéis)
|
|
const rolesOk = requiredRoles && requiredRoles.length > 0
|
|
? requiredRoles.some((req: string) => {
|
|
if (user.userType === req) return true
|
|
if (req === 'profissional' && (userRoles.includes('medico') || userRoles.includes('enfermeiro'))) return true
|
|
if (req === 'administrador' && (userRoles.includes('admin') || userRoles.includes('gestor') || userRoles.includes('secretaria'))) return true
|
|
if (req === 'paciente' && userRoles.includes('paciente')) return true
|
|
return userRoles.includes(req)
|
|
})
|
|
: false
|
|
|
|
// checa requiredUserType (compatibilidade com tipos altos do sistema)
|
|
const userTypeOk = requiredUserType && requiredUserType.length > 0
|
|
? requiredUserType.some((req: UserType) => {
|
|
if (user.userType === req) return true
|
|
if (req === 'profissional' && (userRoles.includes('medico') || userRoles.includes('enfermeiro'))) return true
|
|
if (req === 'administrador' && (userRoles.includes('admin') || userRoles.includes('gestor') || userRoles.includes('secretaria'))) return true
|
|
if (req === 'paciente' && userRoles.includes('paciente')) return true
|
|
return userRoles.includes(req)
|
|
})
|
|
: false
|
|
|
|
return rolesOk || userTypeOk
|
|
}
|
|
|
|
const hasPermission = computeHasPermission()
|
|
|
|
useEffect(() => {
|
|
// marca que o componente já montou no cliente
|
|
setMounted(true)
|
|
|
|
// Evitar múltiplos redirects
|
|
if (isRedirecting.current) return
|
|
|
|
// Durante loading, não fazer nada
|
|
if (authStatus === 'loading') return
|
|
|
|
// Se não autenticado, redirecionar para login
|
|
if (authStatus === 'unauthenticated') {
|
|
isRedirecting.current = true
|
|
|
|
console.log('[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...')
|
|
|
|
// Determinar página de login baseada no histórico
|
|
let userType: UserType = 'profissional'
|
|
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
const storedUserType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
|
if (storedUserType && ['profissional', 'paciente', 'administrador'].includes(storedUserType)) {
|
|
userType = storedUserType as UserType
|
|
}
|
|
} catch (error) {
|
|
console.warn('[PROTECTED-ROUTE] Erro ao ler localStorage:', error)
|
|
}
|
|
}
|
|
|
|
const loginRoute = LOGIN_ROUTES[userType]
|
|
console.log('[PROTECTED-ROUTE] Redirecionando para login:', {
|
|
userType,
|
|
loginRoute,
|
|
timestamp: new Date().toLocaleTimeString()
|
|
})
|
|
|
|
router.push(loginRoute)
|
|
return
|
|
}
|
|
|
|
// Se autenticado mas não tem permissão para esta página
|
|
if (authStatus === 'authenticated' && user && requiredUserType && !hasPermission) {
|
|
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página (accessDenied)', {
|
|
userType: user.userType,
|
|
userRoles: (user as any).roles || [],
|
|
requiredTypes: requiredUserType
|
|
})
|
|
|
|
// Marcar acesso negado para renderizar fallback sem redirecionamentos/speinners infinitos
|
|
setAccessDenied(true)
|
|
return
|
|
}
|
|
|
|
// Se chegou aqui, acesso está autorizado
|
|
if (authStatus === 'authenticated') {
|
|
console.log('[PROTECTED-ROUTE] ACESSO AUTORIZADO!', {
|
|
userType: user?.userType,
|
|
email: user?.email,
|
|
timestamp: new Date().toLocaleTimeString()
|
|
})
|
|
isRedirecting.current = false
|
|
}
|
|
}, [authStatus, user, requiredUserType, router, hasPermission])
|
|
|
|
// Durante loading, mostrar spinner
|
|
if (authStatus === 'loading') {
|
|
// evitar render no servidor para não causar mismatch de hidratação
|
|
if (!mounted) return null
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Verificando autenticação...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Se não autenticado ou redirecionando para login, mostrar spinner
|
|
if (authStatus === 'unauthenticated' || (isRedirecting.current && !accessDenied)) {
|
|
// evitar render no servidor para não causar mismatch de hidratação
|
|
if (!mounted) return null
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Redirecionando...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Se usuário não tem permissão, mostrar fallback (baseado em hasPermission)
|
|
if (requiredUserType && user && !hasPermission) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Acesso Negado</h2>
|
|
<p className="text-gray-600 mb-4">
|
|
Você não tem permissão para acessar esta página.
|
|
</p>
|
|
<p className="text-sm text-gray-500 mb-6">
|
|
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
|
|
<br />
|
|
Seu tipo de acesso: {user.userType}
|
|
<br />
|
|
Seus papéis: {(user as any).roles ? (user as any).roles.join(', ') : '—'}
|
|
</p>
|
|
<button
|
|
onClick={() => router.push(USER_TYPE_ROUTES[user.userType])}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 cursor-pointer"
|
|
>
|
|
Ir para minha área
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Finalmente, renderizar conteúdo protegido
|
|
return <>{children}</>
|
|
} |