import { useState, useEffect, createContext, useContext, ReactNode } from 'react'; import { User, Session } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; import { logger } from '@/utils/logger'; interface AuthContextType { user: User | null; session: Session | null; loading: boolean; signIn: (email: string, password: string) => Promise<{ error: any }>; signUp: (email: string, password: string) => Promise<{ error: any }>; signOut: () => Promise; updatePassword: (password: string) => Promise<{ error: any }>; isRecoveryFlow: boolean; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); const [isRecoveryFlow, setIsRecoveryFlow] = useState(false); const [authInitialized, setAuthInitialized] = useState(false); // Verifica se o usuário é admin ou desenvolvedor (excluídos dos logs de sessão) const isAdminOrDeveloper = async (userId: string): Promise => { try { const { data: adminRoles, error: adminError } = await supabase .from('user_roles') .select('role') .eq('user_id', userId) .eq('role', 'admin') .maybeSingle(); if (!adminError && adminRoles) { return true; } const { data: profile, error: profileError } = await supabase .from('profiles') .select('function_id, functions(name)') .eq('id', userId) .maybeSingle(); if (profileError) { return false; } if (profile?.functions?.name === 'Desenvolvedor') { return true; } return false; } catch { return false; } }; // Processa tokens de recuperação de senha na URL const processRecoveryTokens = () => { try { const currentUrl = window.location.href; const urlObj = new URL(currentUrl); const recoveryType = urlObj.searchParams.get('type'); const hashParams = new URLSearchParams(urlObj.hash.substring(1)); const accessToken = hashParams.get('access_token'); const refreshToken = hashParams.get('refresh_token'); const tokenType = hashParams.get('type'); const isRecovery = recoveryType === 'recovery' || tokenType === 'recovery' || (accessToken && refreshToken); if (isRecovery) { setIsRecoveryFlow(true); if (accessToken && refreshToken) { supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }).then(({ data, error }) => { if (!error) { setSession(data.session); setUser(data.session?.user || null); setIsRecoveryFlow(true); } const cleanUrl = `${window.location.origin}/auth?type=recovery`; window.history.replaceState({}, '', cleanUrl); }); } return true; } return false; } catch { return false; } }; // Marca usuário como online no sistema const setUserOnline = async (userId: string) => { if (!authInitialized) return; try { const shouldSkipLogging = await isAdminOrDeveloper(userId); if (shouldSkipLogging) return; try { await supabase.rpc('set_user_online', { user_id_param: userId }); } catch { // RPC não encontrada — continua sem erro } } catch (error) { logger.error('Erro ao marcar usuário como online:', error); } }; const setUserOffline = async (userId: string) => { if (!authInitialized) return; try { const shouldSkipLogging = await isAdminOrDeveloper(userId); if (shouldSkipLogging) return; try { await supabase.rpc('set_user_offline', { user_id_param: userId }); } catch { // RPC não encontrada — continua sem erro } } catch (error) { logger.error('Erro ao marcar usuário como offline:', error); } }; const startSessionLog = async (userId: string) => { if (!authInitialized) return; try { const shouldSkipLogging = await isAdminOrDeveloper(userId); if (shouldSkipLogging) return; // Desativar qualquer sessão antiga que ficou presa (fechar abas sem logout) await supabase .from('user_session_logs') .update({ is_active: false, session_end: new Date().toISOString() }) .eq('user_id', userId) .eq('is_active', true); const { data, error } = await supabase .from('user_session_logs') .insert({ user_id: userId, user_agent: navigator.userAgent, is_active: true }) .select() .single(); if (error) { logger.error('Erro ao registrar início de sessão:', error); } else if (data) { localStorage.setItem('currentSessionId', data.id); } } catch (error) { logger.error('Erro inesperado ao registrar início de sessão:', error); } }; const endSessionLog = async () => { if (!authInitialized) return; try { const sessionId = localStorage.getItem('currentSessionId'); if (sessionId) { try { await supabase.rpc('end_user_session', { session_id: sessionId }); } catch { // RPC não encontrada — continua sem erro } localStorage.removeItem('currentSessionId'); } } catch (error) { logger.error('Erro ao finalizar sessão:', error); } }; useEffect(() => { let mounted = true; const initializeAuth = async () => { try { const hasRecoveryTokens = processRecoveryTokens(); const { data: { subscription } } = supabase.auth.onAuthStateChange( (event, session) => { if (!mounted) return; setSession(session); setUser(session?.user ?? null); if (event === 'SIGNED_IN' && session?.user) { setTimeout(() => { if (mounted && authInitialized) { setUserOnline(session.user.id); startSessionLog(session.user.id); } }, 100); } else if (event === 'SIGNED_OUT') { setIsRecoveryFlow(false); const currentUser = user; if (currentUser && mounted && authInitialized) { setTimeout(() => { setUserOffline(currentUser.id); endSessionLog(); }, 100); } } } ); if (!hasRecoveryTokens) { try { const { data: { session } } = await supabase.auth.getSession(); if (mounted && session?.user) { setSession(session); setUser(session.user); setTimeout(() => { if (mounted) { setAuthInitialized(true); setUserOnline(session.user.id); // Sempre cria uma nova sessão, resetando contadores anteriores startSessionLog(session.user.id); } }, 100); } else if (mounted) { setSession(null); setUser(null); setAuthInitialized(true); } } catch (sessionError) { logger.error('Erro ao verificar sessão:', sessionError); if (mounted) { setSession(null); setUser(null); setAuthInitialized(true); } } } else { setAuthInitialized(true); } if (mounted) { setLoading(false); } return () => { mounted = false; subscription.unsubscribe(); }; } catch (error) { logger.error('Erro na inicialização da autenticação:', error); if (mounted) { setLoading(false); setSession(null); setUser(null); setAuthInitialized(true); } } }; const cleanup = initializeAuth(); return () => { cleanup.then((cleanupFn) => { if (cleanupFn) cleanupFn(); }); }; }, []); // Limpeza periódica de usuários offline e listener de fechamento de janela useEffect(() => { let cleanupInterval: NodeJS.Timeout; if (user && authInitialized) { cleanupInterval = setInterval(async () => { // Enviar Heartbeat global da sessão (mantém o usuário online de fato) const sessionId = localStorage.getItem('currentSessionId'); if (sessionId) { try { await supabase .from('user_session_logs') .update({ updated_at: new Date().toISOString() }) .eq('id', sessionId); } catch (e) { console.error('Falha no heartbeat da sessão', e); } } try { await supabase.rpc('cleanup_offline_users'); } catch { // Ignora se RPC não existir } }, 30000); // 30 segundos const handleBeforeUnload = () => { if (user) { const sessionId = localStorage.getItem('currentSessionId'); if (sessionId) { try { supabase.rpc('end_user_session', { session_id: sessionId }); } catch { // Ignora no unload } localStorage.removeItem('currentSessionId'); } } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { if (cleanupInterval) { clearInterval(cleanupInterval); } window.removeEventListener('beforeunload', handleBeforeUnload); }; } }, [user?.id, authInitialized]); const signIn = async (email: string, password: string) => { try { const { error } = await supabase.auth.signInWithPassword({ email, password, }); return { error }; } catch (error) { logger.error('Erro crítico no login:', error); return { error }; } }; const signUp = async (email: string, password: string) => { try { const redirectUrl = `${window.location.origin}/`; const { error } = await supabase.auth.signUp({ email, password, options: { emailRedirectTo: redirectUrl } }); return { error }; } catch (error) { logger.error('Erro crítico no signup:', error); return { error }; } }; const signOut = async () => { const currentUser = user; if (currentUser && authInitialized) { setTimeout(() => { setUserOffline(currentUser.id); endSessionLog(); }, 100); } setIsRecoveryFlow(false); try { await supabase.auth.signOut(); } catch (error) { logger.error('Erro no signOut:', error); } }; const updatePassword = async (password: string) => { try { const { error } = await supabase.auth.updateUser({ password: password }); if (!error) { setIsRecoveryFlow(false); } return { error }; } catch (error) { logger.error('Erro crítico ao atualizar senha:', error); return { error }; } }; const value = { user, session, loading, signIn, signUp, signOut, updatePassword, isRecoveryFlow, }; return {children}; } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }