'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 { getUserInfo } from '@/lib/api' import { ENV_CONFIG } from '@/lib/env-config' import { isExpired, parseJwt } from '@/lib/jwt' import { httpClient } from '@/lib/http' import type { AuthContextType, UserData, AuthStatus, UserType } from '@/types/auth' import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth' const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: ReactNode }) { const [authStatus, setAuthStatus] = useState('loading') const [user, setUser] = useState(null) const [token, setToken] = useState(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) // Manter USER_TYPE para redirecionamento correto } setUser(null) setToken(null) setAuthStatus('unauthenticated') console.log('[AUTH] Dados de autenticação limpos - logout realizado') }, []) const saveAuthData = useCallback(( accessToken: string, userData: UserData, refreshToken?: string ) => { try { if (typeof window !== 'undefined') { // Persistir dados de forma atômica localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken) // Garantir que roles também sejam persistidos se presentes const toStore = { ...userData } as any if (userData && (userData as any).roles) { toStore.roles = (userData as any).roles } localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(toStore)) localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, toStore.userType) if (refreshToken) { localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken) } } setToken(accessToken) // Garantir que o estado mantenha roles também setUser(userData as any) setAuthStatus('authenticated') console.log('[AUTH] LOGIN realizado - Dados salvos!', { userType: userData.userType, email: userData.email, timestamp: new Date().toLocaleTimeString() }) } catch (error) { console.error('[AUTH] Erro ao salvar dados:', error) clearAuthData() } }, [clearAuthData]) // Verificação inicial de autenticação const checkAuth = useCallback(async (): Promise => { if (typeof window === 'undefined') { setAuthStatus('unauthenticated') return } try { const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN) const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER) console.log('[AUTH] Verificando sessão...', { hasToken: !!storedToken, hasUser: !!storedUser, timestamp: new Date().toLocaleTimeString() }) // Pequeno delay para visualizar logs 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 } // Verificar se token está expirado if (isExpired(storedToken)) { console.log('[AUTH] Token expirado - tentando renovar...') await new Promise(resolve => setTimeout(resolve, 1000)) const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN) if (refreshToken && !isExpired(refreshToken)) { // Refresh automático foi desativado no cliente. A renovação de tokens // deve ser feita via backend seguro. Se desejar habilitar refresh no // cliente, implemente um flow server-side ou use o SDK autenticado. console.log('[AUTH] Refresh token presente, mas refresh automático desativado no cliente') } clearAuthData() return } // Restaurar sessão válida const userData = JSON.parse(storedUser) as UserData setToken(storedToken) // Nota: chamadas a user-info foram removidas do cliente. Mantemos os dados do // usuário que já estavam persistidos localmente sem tentar reconciliar com funções remotas. setUser(userData) 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]) // Login memoizado const login = useCallback(async ( email: string, password: string, userType: UserType ): Promise => { try { console.log('[AUTH] Iniciando login:', { email, userType }) const response = await loginUser(email, password) // Nota: busca de user-info e reconciliação via funções server-side foi removida do cliente. // Mantemos o objeto response.user retornado pelo fluxo de autenticação sem tentativas // adicionais de mesclagem aqui. // Salva dados iniciais (token + user retornado pelo login) saveAuthData( response.access_token, response.user, response.refresh_token ) // Tentar buscar informações consolidadas do usuário (profile + roles) try { const userInfo = await getUserInfo() if (userInfo) { const mergedUser: any = { ...response.user, // prefer profile and roles vindos do userInfo profile: userInfo.profile ?? (response.user as any).profile, roles: userInfo.roles ?? (response.user as any).roles ?? [], // garantir id/email caso estejam no userInfo id: (response.user as any).id || (userInfo.user as any)?.id, email: (response.user as any).email || (userInfo.user as any)?.email, } // Inferir userType a partir de roles se estiver ausente if (!mergedUser.userType || mergedUser.userType === undefined) { const r: string[] = mergedUser.roles || []; if (r.includes('admin') || r.includes('gestor') || r.includes('secretaria')) mergedUser.userType = 'administrador' else if (r.includes('medico') || r.includes('enfermeiro')) mergedUser.userType = 'profissional' else if (r.includes('paciente')) mergedUser.userType = 'paciente' } // Persistir e atualizar estado com dados consolidados try { if (typeof window !== 'undefined') { localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(mergedUser)) localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, mergedUser.userType || '') } setUser(mergedUser as any) console.log('[AUTH] userInfo consolidado e salvo:', { email: mergedUser.email, roles: mergedUser.roles }) } catch (e) { console.warn('[AUTH] Não foi possível persistir userInfo consolidado:', e) } } } catch (infoErr) { console.warn('[AUTH] getUserInfo falhou (ignorar no cliente):', infoErr) } 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 => { console.log('[AUTH] Iniciando logout') const currentUserType = user?.userType || (typeof window !== 'undefined' ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) : null) || 'profissional' try { if (token) { await logoutUser(token) console.log('[AUTH] Logout realizado na API') } } catch (error) { console.error('[AUTH] Erro no logout da API:', error) } clearAuthData() // Redirecionamento baseado no tipo de usuário const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || '/login' console.log('[AUTH] Redirecionando para:', loginRoute) if (typeof window !== 'undefined') { window.location.href = loginRoute } }, [user?.userType, token, clearAuthData]) // Refresh token memoizado (usado pelo HTTP client) const refreshToken = useCallback(async (): Promise => { // Esta função é principalmente para compatibilidade // O refresh real é feito pelo HTTP client return false }, []) // Getters memorizados const contextValue = useMemo(() => ({ authStatus, user, token, login, logout, refreshToken }), [authStatus, user, token, login, logout, refreshToken]) // Inicialização única useEffect(() => { if (!hasInitialized.current && typeof window !== 'undefined') { hasInitialized.current = true checkAuth() } }, [checkAuth]) return ( {children} ) } export const useAuth = () => { const context = useContext(AuthContext) if (context === undefined) { throw new Error('useAuth deve ser usado dentro de AuthProvider') } return context }