import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Session, User } from '@supabase/supabase-js'; import { CCUser, CCUserMetadata, authService } from '../services/auth/authService'; import { logger } from '../debugConfig'; import { supabase } from '../supabaseClient'; import { storageService, StorageKeys } from '../services/auth/localStorageService'; import { fetchBootstrapData, BootstrapResponse, invalidateBootstrapCache } from '../services/bootstrap/bootstrapService'; export interface AuthContextType { user: CCUser | null; user_role: string | null; accessToken: string | null; loading: boolean; error: Error | null; signIn: (email: string, password: string) => Promise; signOut: () => Promise; clearError: () => void; bootstrapData: BootstrapResponse | null; isAuthResolving: boolean; } export const AuthContext = createContext({ user: null, user_role: null, accessToken: null, loading: true, error: null, signIn: async () => {}, signOut: async () => {}, clearError: () => {}, bootstrapData: null, isAuthResolving: false }); export function AuthProvider({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const [user, setUser] = useState(null); const [user_role, setUserRole] = useState(null); const [accessToken, setAccessToken] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [bootstrapData, setBootstrapData] = useState(null); const [isAuthResolving, setIsAuthResolving] = useState(false); const persistSession = useCallback((session: Session | null) => { if (session) { storageService.set(StorageKeys.SUPABASE_SESSION, session); } else { storageService.remove(StorageKeys.SUPABASE_SESSION); } }, []); const buildUserFromSupabase = useCallback(async (supabaseUser: User | null): Promise<{ user: CCUser | null; role: string | null }> => { if (!supabaseUser) { return { user: null, role: null }; } const metadata = supabaseUser.user_metadata as CCUserMetadata; const baseUsername = metadata.username || metadata.preferred_username || metadata.email?.split('@')[0] || supabaseUser.email?.split('@')[0] || 'user'; const baseDisplayName = metadata.display_name || metadata.name || metadata.preferred_username || baseUsername; const userType = (metadata.user_type || 'email_teacher').trim(); const resolvedUser: CCUser = { id: supabaseUser.id, email: supabaseUser.email, user_type: userType, username: baseUsername, display_name: baseDisplayName, school_id: null, created_at: supabaseUser.created_at, updated_at: supabaseUser.updated_at }; const resolvedRole = metadata.user_role || userType || null; return { user: resolvedUser, role: resolvedRole }; }, []); const loadBootstrap = useCallback(async (token: string | null | undefined) => { if (!token) { setBootstrapData(null); return null; } const bootstrap = await fetchBootstrapData(token); setBootstrapData(bootstrap); return bootstrap; }, []); useEffect(() => { const { data: { subscription } } = supabase.auth.onAuthStateChange( async (event, session) => { logger.debug('auth-context', '🔄 Auth state change', { event, hasSession: !!session }); if (event === 'SIGNED_IN') { persistSession(session ?? null); setIsAuthResolving(!!session?.user); if (session?.user) { try { const { user: resolvedUser, role } = await buildUserFromSupabase(session.user); setUser(resolvedUser); setUserRole(role); setAccessToken(session.access_token ?? null); await loadBootstrap(session.access_token); } catch (buildError) { logger.error('auth-context', '❌ Failed to build user from session', { event, error: buildError }); setUser(null); setUserRole(null); setAccessToken(null); setError(buildError instanceof Error ? buildError : new Error('Failed to load user')); } } else { setUser(null); setUserRole(null); setAccessToken(null); } setLoading(false); setIsAuthResolving(false); return; } if (event === 'INITIAL_SESSION' || event === 'TOKEN_REFRESHED') { persistSession(session ?? null); setIsAuthResolving(!!session?.user); if (session?.user) { try { const { user: resolvedUser, role } = await buildUserFromSupabase(session.user); setUser(resolvedUser); setUserRole(role); setAccessToken(session.access_token ?? null); await loadBootstrap(session.access_token); } catch (buildError) { logger.error('auth-context', '❌ Failed to build user from session', { event, error: buildError }); setUser(null); setUserRole(null); setAccessToken(null); setError(buildError instanceof Error ? buildError : new Error('Failed to load user')); } } else { setUser(null); setUserRole(null); setAccessToken(null); } setLoading(false); setIsAuthResolving(false); return; } if (event === 'SIGNED_OUT') { persistSession(null); setIsAuthResolving(false); setUser(null); setUserRole(null); setAccessToken(null); setBootstrapData(null); invalidateBootstrapCache(); setLoading(false); } } ); return () => subscription.unsubscribe(); }, [buildUserFromSupabase, persistSession, loadBootstrap]); const signIn = async (email: string, password: string) => { try { setLoading(true); const { data, error: signInError } = await supabase.auth.signInWithPassword({ email, password }); if (signInError) throw signInError; if (data.session) { persistSession(data.session); } if (data.user) { const { user: resolvedUser, role } = await buildUserFromSupabase(data.user); setUser(resolvedUser); setUserRole(role); setAccessToken(data.session?.access_token ?? null); await loadBootstrap(data.session?.access_token ?? null); } } catch (error) { logger.error('auth-context', '❌ Sign in failed', { error }); setError(error instanceof Error ? error : new Error('Sign in failed')); throw error; } finally { setLoading(false); } }; const signOut = async () => { try { setLoading(true); await authService.logout(); persistSession(null); setUser(null); navigate('/'); } catch (error) { logger.error('auth-context', '❌ Sign out failed', { error }); setError(error instanceof Error ? error : new Error('Sign out failed')); throw error; } finally { setLoading(false); } }; const clearError = () => setError(null); return ( {children} ); } export const useAuth = () => useContext(AuthContext);