"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); 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(), }); } 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)) { // Tentar renovar via HTTP client (que já tem a lógica) try { await httpClient.get("/auth/v1/me"); // Trigger refresh se necessário // Se chegou aqui, refresh foi bem-sucedido const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN); const userData = JSON.parse(storedUser) as UserData; if (newToken && newToken !== storedToken) { setToken(newToken); setUser(userData); setAuthStatus("authenticated"); console.log("[AUTH] Token RENOVADO automaticamente!"); await new Promise((resolve) => setTimeout(resolve, 800)); return; } } catch (refreshError) { console.log(" [AUTH] Falha no refresh automático"); await new Promise((resolve) => setTimeout(resolve, 400)); } } clearAuthData(); return; } // Restaurar sessão válida const userData = JSON.parse(storedUser) as UserData; setToken(storedToken); // Tentar buscar profile consolidado (user-info) e mesclar try { const info = await getUserInfo(); if (info?.profile) { const mapped = { cpf: (info.profile as any).cpf ?? userData.profile?.cpf, crm: (info.profile as any).crm ?? userData.profile?.crm, telefone: info.profile.phone ?? userData.profile?.telefone, foto_url: info.profile.avatar_url ?? userData.profile?.foto_url, }; if (userData.profile) { userData.profile = { ...userData.profile, ...mapped }; } else { userData.profile = mapped; } // Persistir o usuário atualizado no localStorage para evitar // que 'auth_user.profile' fique vazio após um reload completo try { if (typeof window !== "undefined") { localStorage.setItem( AUTH_STORAGE_KEYS.USER, JSON.stringify(userData) ); } } catch (e) { console.warn( "[AUTH] Falha ao persistir user (profile) no localStorage:", e ); } } } catch (err) { console.warn( "[AUTH] Falha ao buscar user-info na restauração de sessão:", err ); } 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, userType); // Após receber token, buscar roles/permissions reais e reconciliar userType try { const infoRes = await fetch( `${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${response.access_token}`, apikey: ENV_CONFIG.SUPABASE_ANON_KEY, }, } ); if (infoRes.ok) { const info = await infoRes.json().catch(() => null); const roles: string[] = Array.isArray(info?.roles) ? info.roles : info?.roles ? [info.roles] : []; // Derivar tipo de usuário a partir dos roles // Derivar tipo de usuário a partir dos roles retornados pela API let derived: UserType = userType; // 🔹 começa respeitando o tipo da tela de login if ( roles.includes("admin") || roles.includes("gestor") || roles.includes("secretaria") ) { derived = "administrador"; } else if ( roles.includes("medico") || roles.includes("enfermeiro") ) { derived = "profissional"; } else if (roles.length === 0 && userType) { // 🔹 se a API não retornou roles, mantemos o tipo informado pela tela derived = userType; } // 🔹 Se o tipo da tela e o das roles divergirem, prioriza o da tela (exceto admin) if ( response.user && response.user.userType !== derived && userType !== "administrador" ) { response.user.userType = userType; console.log( "[AUTH] userType mantido da tela de login ->", userType ); } else if (response.user && response.user.userType !== derived) { response.user.userType = derived; console.log( "[AUTH] userType reconciliado a partir das roles ->", derived ); } } else { console.warn( "[AUTH] Falha ao obter user-info para reconciliar roles:", infoRes.status ); } } catch (err) { console.warn( "[AUTH] Erro ao buscar user-info após login (não crítico):", err ); } // Após login, tentar buscar profile consolidado e mesclar antes de persistir try { const info = await getUserInfo(); if (info?.profile && response.user) { const mapped = { cpf: (info.profile as any).cpf ?? response.user.profile?.cpf, crm: (info.profile as any).crm ?? response.user.profile?.crm, telefone: info.profile.phone ?? response.user.profile?.telefone, foto_url: info.profile.avatar_url ?? response.user.profile?.foto_url, }; if (response.user.profile) { response.user.profile = { ...response.user.profile, ...mapped }; } else { response.user.profile = mapped; } } } catch (err) { console.warn( "[AUTH] Falha ao buscar user-info após login (não crítico):", err ); } 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 => { 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; };