420 lines
12 KiB
TypeScript
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;
|
|
}
|