mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 09:11:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
264
src-old/hooks/useAuth.tsx
Normal file
264
src-old/hooks/useAuth.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
|
||||
import type { User, Session } from '@supabase/supabase-js';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { Profile } from '@/types/database';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { authLog, authWarn, authError } from '@/lib/authLogger';
|
||||
import type { AALLevel, CheckAalResult } from '@/types/auth';
|
||||
import { getSessionAal, checkAalStepUp as checkAalStepUpService, signOutUser } from '@/lib/authService';
|
||||
import { clearAllAuthFlags } from '@/lib/sessionFlags';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
aal: AALLevel | null;
|
||||
loading: boolean;
|
||||
pendingEmail: string | null;
|
||||
sessionError: string | null;
|
||||
signOut: () => Promise<void>;
|
||||
verifySession: () => Promise<boolean>;
|
||||
clearPendingEmail: () => void;
|
||||
checkAalStepUp: () => Promise<CheckAalResult>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [aal, setAal] = useState<AALLevel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||
|
||||
// Refs for lifecycle and cleanup management
|
||||
const novuUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const previousEmailRef = useRef<string | null>(null);
|
||||
const orphanedPasswordToastShownRef = useRef(false);
|
||||
|
||||
// Verify session is still valid - simplified
|
||||
const verifySession = async () => {
|
||||
try {
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
authError('[Auth] Session verification failed:', error);
|
||||
setSessionError(error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
authLog('[Auth] No active session found');
|
||||
return false;
|
||||
}
|
||||
|
||||
authLog('[Auth] Session verified:', session.user.email);
|
||||
|
||||
// Update state if session was found but not set
|
||||
if (!user) {
|
||||
setSession(session);
|
||||
setUser(session.user);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
authError('[Auth] Session verification error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
authLog('[Auth] Initializing auth provider');
|
||||
|
||||
// CRITICAL: Set up listener FIRST to catch all events
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
authLog('[Auth] State change:', event, 'User:', session?.user?.email || 'none', 'Has session:', !!session);
|
||||
|
||||
// Extract email info early for cleanup
|
||||
const currentEmail = session?.user?.email;
|
||||
const newEmailPending = session?.user?.new_email;
|
||||
|
||||
// CRITICAL: Always clear/update pending email state BEFORE any early returns
|
||||
setPendingEmail(newEmailPending ?? null);
|
||||
|
||||
// Clear any error
|
||||
setSessionError(null);
|
||||
|
||||
// Synchronous state updates only
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
|
||||
// Handle loading state
|
||||
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') {
|
||||
setLoading(false);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
authLog('[Auth] SIGNED_OUT - clearing state');
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
setAal(null);
|
||||
setLoading(false);
|
||||
orphanedPasswordToastShownRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer async operations to avoid blocking the auth state change callback
|
||||
setTimeout(async () => {
|
||||
// Get AAL level from Supabase API (ground truth, not cached session data)
|
||||
if (session) {
|
||||
const currentAal = await getSessionAal(session);
|
||||
setAal(currentAal);
|
||||
authLog('[Auth] Current AAL:', currentAal);
|
||||
|
||||
// Check if user is banned
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('banned')
|
||||
.eq('user_id', session.user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (profile?.banned) {
|
||||
authWarn('[Auth] Banned user detected, signing out');
|
||||
await supabase.auth.signOut();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setAal(null);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Detect confirmed email change: email changed AND no longer pending
|
||||
if (
|
||||
session?.user &&
|
||||
previousEmailRef.current &&
|
||||
currentEmail &&
|
||||
currentEmail !== previousEmailRef.current &&
|
||||
!newEmailPending
|
||||
) {
|
||||
// Clear any existing Novu update timeout
|
||||
if (novuUpdateTimeoutRef.current) {
|
||||
clearTimeout(novuUpdateTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Defer Novu update and notifications to avoid blocking auth
|
||||
const oldEmail = previousEmailRef.current;
|
||||
novuUpdateTimeoutRef.current = setTimeout(async () => {
|
||||
try {
|
||||
// Update Novu subscriber with confirmed email
|
||||
const { notificationService } = await import('@/lib/notificationService');
|
||||
if (notificationService.isEnabled()) {
|
||||
await notificationService.updateSubscriber({
|
||||
subscriberId: session.user.id,
|
||||
email: currentEmail,
|
||||
});
|
||||
}
|
||||
|
||||
// Log the confirmed email change
|
||||
await supabase.from('admin_audit_log').insert({
|
||||
admin_user_id: session.user.id,
|
||||
target_user_id: session.user.id,
|
||||
action: 'email_change_completed',
|
||||
details: {
|
||||
old_email: oldEmail,
|
||||
new_email: currentEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
// Send final security notification
|
||||
if (notificationService.isEnabled()) {
|
||||
await notificationService.trigger({
|
||||
workflowId: 'email-changed',
|
||||
subscriberId: session.user.id,
|
||||
payload: {
|
||||
oldEmail: oldEmail,
|
||||
newEmail: currentEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
authError('Error updating Novu after email confirmation:', error);
|
||||
} finally {
|
||||
novuUpdateTimeoutRef.current = null;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Update tracked email
|
||||
if (currentEmail) {
|
||||
previousEmailRef.current = currentEmail;
|
||||
}
|
||||
});
|
||||
|
||||
// THEN get initial session (this may trigger INITIAL_SESSION event)
|
||||
supabase.auth.getSession().then(({ data: { session }, error }) => {
|
||||
if (error) {
|
||||
authError('[Auth] Initial session fetch error:', error);
|
||||
setSessionError(error.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: onAuthStateChange will handle the INITIAL_SESSION event
|
||||
// This is just a backup in case the event doesn't fire
|
||||
authLog('[Auth] getSession completed, session exists:', !!session);
|
||||
});
|
||||
|
||||
return () => {
|
||||
authLog('[Auth] Cleaning up auth provider');
|
||||
subscription.unsubscribe();
|
||||
|
||||
// Clear any pending timeouts
|
||||
if (novuUpdateTimeoutRef.current) {
|
||||
clearTimeout(novuUpdateTimeoutRef.current);
|
||||
novuUpdateTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const signOut = async () => {
|
||||
authLog('[Auth] Signing out...');
|
||||
const result = await signOutUser();
|
||||
if (!result.success) {
|
||||
authError('Error signing out:', result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
};
|
||||
|
||||
const clearPendingEmail = () => {
|
||||
setPendingEmail(null);
|
||||
};
|
||||
|
||||
const checkAalStepUp = async (): Promise<CheckAalResult> => {
|
||||
return checkAalStepUpService(session);
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
session,
|
||||
aal,
|
||||
loading,
|
||||
pendingEmail,
|
||||
sessionError,
|
||||
signOut,
|
||||
verifySession,
|
||||
clearPendingEmail,
|
||||
checkAalStepUp,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export const AuthProvider = AuthProviderComponent;
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
logger.error('AuthContext is undefined - component may be rendering outside AuthProvider', { component: 'useAuth' });
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user