Files
tracksteel_app/src/hooks/useAuth.tsx
T

420 lines
12 KiB
TypeScript

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<void>;
updatePassword: (password: string) => Promise<{ error: any }>;
isRecoveryFlow: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(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<boolean> => {
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}