Improve authentication state management and user session handling

Refactors the `AuthProviderComponent` in `useAuth.tsx` to enhance the handling of Supabase authentication state changes. Updates console logs for better debugging, prioritizes setting pending email state before early returns, and refines the logic for `SIGNED_IN`, `INITIAL_SESSION`, and `SIGNED_OUT` events. Also defers Novu updates and profile fetches to prevent blocking the authentication process.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e7030bef-c72b-4948-9378-ee5e345d91ce
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/e7030bef-c72b-4948-9378-ee5e345d91ce/zQglahT
This commit is contained in:
pac7
2025-10-12 17:05:15 +00:00
parent da3a5247a1
commit 0b8fe60823
2 changed files with 40 additions and 55 deletions

View File

@@ -37,3 +37,7 @@ externalPort = 80
[[ports]] [[ports]]
localPort = 5001 localPort = 5001
externalPort = 3000 externalPort = 3000
[[ports]]
localPort = 37143
externalPort = 3001

View File

@@ -157,21 +157,25 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
}, [loading]); }, [loading]);
useEffect(() => { useEffect(() => {
console.log('[Auth] Initializing auth provider');
// CRITICAL: Set up listener FIRST to catch all events // CRITICAL: Set up listener FIRST to catch all events
const { const {
data: { subscription }, data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => { } = supabase.auth.onAuthStateChange((event, session) => {
console.log('[Auth] State change:', event, 'User:', session?.user?.email || 'none'); console.log('[Auth] State change:', event, 'User:', session?.user?.email || 'none', 'Has session:', !!session);
if (!isMountedRef.current) return;
// Extract email info early for cleanup
const currentEmail = session?.user?.email; const currentEmail = session?.user?.email;
const newEmailPending = session?.user?.new_email; const newEmailPending = session?.user?.new_email;
// CRITICAL: Always clear/update pending email state BEFORE any early returns
setPendingEmail(newEmailPending ?? null);
// Clear any error // Clear any error
setSessionError(null); setSessionError(null);
// Explicitly handle SIGNED_IN and INITIAL_SESSION events // Update session and user state based on event
if (event === 'SIGNED_IN' && session) { if (event === 'SIGNED_IN' && session) {
console.log('[Auth] SIGNED_IN detected, setting session and user'); console.log('[Auth] SIGNED_IN detected, setting session and user');
setSession(session); setSession(session);
@@ -179,33 +183,33 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
sessionVerifiedRef.current = true; sessionVerifiedRef.current = true;
} else if (event === 'INITIAL_SESSION') { } else if (event === 'INITIAL_SESSION') {
if (session?.user) { if (session?.user) {
console.log('[Auth] INITIAL_SESSION detected with session, setting user'); console.log('[Auth] INITIAL_SESSION with user, setting session');
setSession(session); setSession(session);
setUser(session.user); setUser(session.user);
sessionVerifiedRef.current = true; sessionVerifiedRef.current = true;
} else { } else {
console.log('[Auth] INITIAL_SESSION detected with NO session - user not logged in'); console.log('[Auth] INITIAL_SESSION with no user - setting loading to false');
setSession(null); setSession(null);
setUser(null); setUser(null);
setProfile(null); setProfile(null);
sessionVerifiedRef.current = false; sessionVerifiedRef.current = false;
// CRITICAL: Set loading to false immediately when no session // CRITICAL: Set loading to false immediately when no session
setLoading(false); setLoading(false);
return; // Exit early, no need to fetch profile
} }
} else if (event === 'SIGNED_OUT') { } else if (event === 'SIGNED_OUT') {
console.log('[Auth] SIGNED_OUT detected, clearing session'); console.log('[Auth] SIGNED_OUT detected, clearing all state');
setSession(null); setSession(null);
setUser(null); setUser(null);
setProfile(null); setProfile(null);
sessionVerifiedRef.current = false; sessionVerifiedRef.current = false;
setLoading(false);
return; // Exit early, no need to fetch profile
} else { } else {
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
} }
// Track pending email changes
setPendingEmail(newEmailPending ?? null);
// Detect confirmed email change: email changed AND no longer pending // Detect confirmed email change: email changed AND no longer pending
if ( if (
session?.user && session?.user &&
@@ -222,8 +226,6 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Defer Novu update and notifications to avoid blocking auth // Defer Novu update and notifications to avoid blocking auth
const oldEmail = previousEmailRef.current; const oldEmail = previousEmailRef.current;
novuUpdateTimeoutRef.current = setTimeout(async () => { novuUpdateTimeoutRef.current = setTimeout(async () => {
if (!isMountedRef.current) return;
try { try {
// Update Novu subscriber with confirmed email // Update Novu subscriber with confirmed email
const { notificationService } = await import('@/lib/notificationService'); const { notificationService } = await import('@/lib/notificationService');
@@ -271,19 +273,20 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
previousEmailRef.current = currentEmail; previousEmailRef.current = currentEmail;
} }
// Handle profile fetching for authenticated users
if (session?.user) { if (session?.user) {
// Clear any existing profile fetch timeout // Clear any existing profile fetch timeout
if (profileFetchTimeoutRef.current) { if (profileFetchTimeoutRef.current) {
clearTimeout(profileFetchTimeoutRef.current); clearTimeout(profileFetchTimeoutRef.current);
} }
// Defer profile fetch to avoid deadlock // Only wait for profile on initial auth events
const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session !== null; const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION');
console.log('[Auth] Profile fetch deferred, shouldWaitForProfile:', shouldWaitForProfile, 'event:', event); console.log('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile);
profileFetchTimeoutRef.current = setTimeout(() => { profileFetchTimeoutRef.current = setTimeout(() => {
if (!isMountedRef.current) return;
fetchProfile(session.user.id, 0, () => { fetchProfile(session.user.id, 0, () => {
if (isMountedRef.current && shouldWaitForProfile) { if (shouldWaitForProfile) {
console.log('[Auth] Profile fetch complete, setting loading to false'); console.log('[Auth] Profile fetch complete, setting loading to false');
setLoading(false); setLoading(false);
} }
@@ -291,27 +294,15 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
profileFetchTimeoutRef.current = null; profileFetchTimeoutRef.current = null;
}, 0); }, 0);
} else { } else {
if (isMountedRef.current) { // No session/user - clear profile and resolve loading
setProfile(null); setProfile(null);
// CRITICAL: Always resolve loading when there's no session console.log('[Auth] No user, setting loading to false');
if (event === 'INITIAL_SESSION' || event === 'SIGNED_OUT') {
console.log('[Auth] No session for event:', event, '- setting loading to false');
setLoading(false);
}
}
}
// CRITICAL: Always set loading false for non-session events
if (event !== 'SIGNED_IN' && event !== 'INITIAL_SESSION') {
console.log('[Auth] Setting loading to false immediately for event:', event);
setLoading(false); setLoading(false);
} }
}); });
// THEN get initial session // THEN get initial session (this may trigger INITIAL_SESSION event)
supabase.auth.getSession().then(({ data: { session }, error }) => { supabase.auth.getSession().then(({ data: { session }, error }) => {
if (!isMountedRef.current) return;
if (error) { if (error) {
console.error('[Auth] Initial session fetch error:', error); console.error('[Auth] Initial session fetch error:', error);
setSessionError(error.message); setSessionError(error.message);
@@ -319,41 +310,30 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
return; return;
} }
setSession(session); // Note: onAuthStateChange will handle the INITIAL_SESSION event
setUser(session?.user ?? null); // This is just a backup in case the event doesn't fire
console.log('[Auth] getSession completed, session exists:', !!session);
if (session?.user) {
sessionVerifiedRef.current = true;
// Fetch profile with completion callback
fetchProfile(session.user.id, 0, () => {
if (isMountedRef.current) {
setLoading(false);
}
});
} else {
setLoading(false);
}
}); });
// Add a safety timeout to force loading to resolve // Add a STRONG safety timeout to force loading to resolve
loadingTimeoutRef.current = setTimeout(() => { loadingTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && loadingStateRef.current) { if (loadingStateRef.current) {
console.warn('[Auth] Loading timeout reached after 5 seconds, forcing loading to false'); console.warn('[Auth] ⚠️ SAFETY TIMEOUT: Forcing loading to false after 3 seconds');
setLoading(false); setLoading(false);
} }
}, 5000); }, 3000);
// Session verification fallback after 2 seconds // Session verification fallback
const verificationTimeout = setTimeout(() => { const verificationTimeout = setTimeout(() => {
if (!sessionVerifiedRef.current && isMountedRef.current) { if (!sessionVerifiedRef.current) {
console.log('[Auth] Session not verified, attempting manual verification'); console.log('[Auth] Session not verified after 2s, attempting manual verification');
verifySession(); verifySession();
} }
}, 2000); }, 2000);
// Handle page visibility changes // Handle page visibility changes
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && isMountedRef.current) { if (document.visibilityState === 'visible') {
console.log('[Auth] Tab became visible, verifying session'); console.log('[Auth] Tab became visible, verifying session');
verifySession(); verifySession();
} }
@@ -362,6 +342,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
return () => { return () => {
console.log('[Auth] Cleaning up auth provider');
isMountedRef.current = false; isMountedRef.current = false;
subscription.unsubscribe(); subscription.unsubscribe();
clearTimeout(verificationTimeout); clearTimeout(verificationTimeout);