M-Gabrielly 5655d0c607 feat(auth): implement user profile and access control
Adds user profile data fetching after login and protects the Doctors page so only administrators can access it.
2025-10-01 22:50:44 -03:00

231 lines
7.1 KiB
TypeScript

'use client'
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
import { isExpired, parseJwt } from '@/lib/jwt'
import { httpClient } from '@/lib/http'
import { buscarPerfilPorId, type UserProfile } from '@/lib/api' // <-- 1. IMPORTAR
import type {
AuthContextType,
UserData,
AuthStatus,
UserType
} from '@/types/auth'
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth'
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
const [user, setUser] = useState<UserData | null>(null)
const [profile, setProfile] = useState<UserProfile | null>(null) // <-- 2. NOVO ESTADO PARA PERFIL
const [token, setToken] = useState<string | null>(null)
const router = useRouter()
const hasInitialized = useRef(false)
// Utilitários de armazenamento memorizados
const clearAuthData = useCallback(() => {
if (typeof window !== 'undefined') {
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
localStorage.removeItem(AUTH_STORAGE_KEYS.PROFILE) // <-- 3. LIMPAR PERFIL
// Manter USER_TYPE para redirecionamento correto
}
setUser(null)
setProfile(null) // <-- 3. LIMPAR PERFIL
setToken(null)
setAuthStatus('unauthenticated')
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
}, [])
const fetchAndSetProfile = useCallback(async (userId: string) => {
try {
console.log('[AUTH] Buscando perfil completo...', { userId });
const userProfile = await buscarPerfilPorId(userId);
if (userProfile) {
setProfile(userProfile);
localStorage.setItem(AUTH_STORAGE_KEYS.PROFILE, JSON.stringify(userProfile));
console.log('[AUTH] Perfil completo armazenado.', userProfile);
}
} catch (error) {
console.error('[AUTH] Falha ao buscar perfil completo:', error);
}
}, []);
const saveAuthData = useCallback(async (
accessToken: string,
userData: UserData,
refreshToken?: string
) => {
try {
if (typeof window !== 'undefined') {
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken)
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType)
if (refreshToken) {
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
}
}
setToken(accessToken)
setUser(userData)
setAuthStatus('authenticated')
console.log('[AUTH] LOGIN realizado - Dados salvos!', {
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString()
})
await fetchAndSetProfile(userData.id); // <-- 4. BUSCAR PERFIL APÓS LOGIN
} catch (error) {
console.error('[AUTH] Erro ao salvar dados:', error)
clearAuthData()
}
}, [clearAuthData, fetchAndSetProfile])
// Verificação inicial de autenticação
const checkAuth = useCallback(async (): Promise<void> => {
if (typeof window === 'undefined') {
setAuthStatus('unauthenticated')
return
}
try {
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER)
const storedProfile = localStorage.getItem(AUTH_STORAGE_KEYS.PROFILE) // <-- 5. LER PERFIL DO STORAGE
console.log('[AUTH] Verificando sessão...', {
hasToken: !!storedToken,
hasUser: !!storedUser,
timestamp: new Date().toLocaleTimeString()
})
await new Promise(resolve => setTimeout(resolve, 800))
if (!storedToken || !storedUser) {
console.log('[AUTH] Dados ausentes - sessão inválida')
await new Promise(resolve => setTimeout(resolve, 500))
clearAuthData()
return
}
if (isExpired(storedToken)) {
// ... (lógica de refresh token existente)
clearAuthData()
return
}
// Restaurar sessão válida
const userData = JSON.parse(storedUser) as UserData
setToken(storedToken)
setUser(userData)
if (storedProfile) { // <-- 5. RESTAURAR PERFIL
setProfile(JSON.parse(storedProfile));
} else {
fetchAndSetProfile(userData.id); // ou buscar se não existir
}
setAuthStatus('authenticated')
console.log('[AUTH] Sessão RESTAURADA com sucesso!', {
userId: userData.id,
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString()
})
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.error('[AUTH] Erro na verificação:', error)
clearAuthData()
}
}, [clearAuthData, fetchAndSetProfile])
// Login memoizado
const login = useCallback(async (
email: string,
password: string,
userType: UserType
): Promise<boolean> => {
try {
console.log('[AUTH] Iniciando login:', { email, userType })
const response = await loginUser(email, password, userType)
await saveAuthData(
response.access_token,
response.user,
response.refresh_token
)
console.log('[AUTH] Login realizado com sucesso')
return true
} catch (error) {
console.error('[AUTH] Erro no login:', error)
if (error instanceof AuthenticationError) {
throw error
}
throw new AuthenticationError(
'Erro inesperado durante o login',
'UNKNOWN_ERROR',
error
)
}
}, [saveAuthData])
// Logout memoizado
const logout = useCallback(async (): Promise<void> => {
// ... (código de logout existente) ...
clearAuthData()
const loginRoute = LOGIN_ROUTES[user?.userType as UserType] || '/login'
if (typeof window !== 'undefined') {
window.location.href = loginRoute
}
}, [user?.userType, token, clearAuthData])
// Refresh token memoizado
const refreshToken = useCallback(async (): Promise<boolean> => {
return false
}, [])
// Getters memorizados
const contextValue = useMemo(() => ({
authStatus,
user,
profile, // <-- 6. EXPOR PERFIL NO CONTEXTO
token,
login,
logout,
refreshToken
}), [authStatus, user, profile, token, login, logout, refreshToken])
// Inicialização única
useEffect(() => {
if (!hasInitialized.current && typeof window !== 'undefined') {
hasInitialized.current = true
checkAuth()
}
}, [checkAuth])
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth deve ser usado dentro de AuthProvider')
}
return context
}