This commit is contained in:
2025-11-14 14:47:26 +00:00
parent 69ecf2c7c1
commit 3b4876793e
104 changed files with 231517 additions and 1029 deletions
+377 -62
View File
@@ -1,9 +1,12 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { Session, User } from '@supabase/supabase-js';
import { supabase } from '../supabaseClient';
import { logger } from '../debugConfig';
import { CCUser, CCUserMetadata } from '../services/auth/authService';
import { UserPreferences } from '../services/auth/profileService';
import { DatabaseNameService } from '../services/graph/databaseNameService';
import { provisionUser } from '../services/provisioningService';
import { storageService, StorageKeys } from '../services/auth/localStorageService';
export interface UserContextType {
user: CCUser | null;
@@ -31,7 +34,7 @@ export const UserContext = createContext<UserContextType>({
clearError: () => {}
});
export function UserProvider({ children }: { children: React.ReactNode }) {
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
const [user] = useState<CCUser | null>(null);
const [profile, setProfile] = useState<CCUser | null>(null);
const [preferences, setPreferences] = useState<UserPreferences>({});
@@ -39,72 +42,370 @@ export function UserProvider({ children }: { children: React.ReactNode }) {
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [isMobile] = useState(window.innerWidth <= 768);
const mountedRef = React.useRef(true);
useEffect(() => {
const loadUserProfile = async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
setProfile(null);
setLoading(false);
setIsInitialized(true);
// Use the main Supabase client for all operations to ensure proper session persistence
// This avoids the "Multiple GoTrueClient instances" warning and ensures session restoration works
const resolveProfile = useCallback(async (supabaseUser?: User | null, session?: Session | null) => {
// Prevent duplicate work when we already have the same user resolved
if (mountedRef.current && isInitialized) {
const resolvedUserId = profile?.id;
const incomingUserId = supabaseUser?.id ?? session?.user?.id ?? null;
if (!incomingUserId && !supabaseUser) {
logger.debug('user-context', '⚠️ Profile already initialized for guest session, skipping resolution');
return;
}
if (incomingUserId && resolvedUserId && resolvedUserId === incomingUserId) {
logger.debug('user-context', '⚠️ Profile already initialized for current user, skipping resolution');
return;
}
}
let userInfo: User | null = null; // Declare at function scope
try {
logger.debug('user-context', '🔄 Resolving user profile', {
hasSupabaseUser: !!supabaseUser,
isInitialized
});
logger.debug('user-context', '🔧 Step 1: Starting profile resolution...');
// Don't set loading to true immediately - let the UI show progress naturally
let authSession = session;
userInfo = supabaseUser ?? null;
logger.debug('user-context', '🔧 Step 2: Getting auth session...');
if (!authSession) {
const { data } = await supabase.auth.getSession();
authSession = data.session;
logger.debug('user-context', '🔧 Step 2a: Got session from supabase', {
hasSession: !!authSession
});
}
logger.debug('user-context', '🔧 Step 3: Getting user info...');
if (!userInfo) {
const { data } = await supabase.auth.getUser();
userInfo = data.user;
logger.debug('user-context', '🔧 Step 3a: Got user from supabase', {
hasUser: !!userInfo
});
}
if (!userInfo) {
logger.debug('user-context', '⚠️ No user info available - clearing profile');
if (!mountedRef.current) {
return;
}
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (error) {
throw error;
}
const metadata = user.user_metadata as CCUserMetadata;
const userDbName = DatabaseNameService.getUserPrivateDB(metadata.user_type || '', metadata.username || '');
const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB();
const userProfile: CCUser = {
id: user.id,
email: user.email,
user_type: metadata.user_type || '',
username: metadata.username || '',
display_name: metadata.display_name || '',
user_db_name: userDbName,
school_db_name: schoolDbName,
created_at: user.created_at,
updated_at: user.updated_at
};
setProfile(userProfile);
logger.debug('user-context', '✅ User profile loaded', {
userId: userProfile.id,
userType: userProfile.user_type,
username: userProfile.username,
userDbName: userProfile.user_db_name,
schoolDbName: userProfile.school_db_name
});
// Load preferences from profile data
setPreferences({
theme: data.theme || 'system',
notifications: data.notifications_enabled || false
});
} catch (error) {
logger.error('user-context', '❌ Failed to load user profile', { error });
setError(error instanceof Error ? error : new Error('Failed to load user profile'));
} finally {
setProfile(null);
setPreferences({});
setError(null);
setLoading(false);
setIsInitialized(true);
return;
}
};
loadUserProfile();
}, []);
logger.debug('user-context', '🔧 Step 4: User info available, proceeding...', {
userId: userInfo.id,
email: userInfo.email
});
let profileRow: Record<string, unknown> | null = null;
logger.debug('user-context', '🔧 Step 5: Querying profiles table...', {
userId: userInfo.id
});
// Set loading state when we start the actual database query
setLoading(true);
// Query profiles table without timeout to see actual error
logger.debug('user-context', '🔧 Step 5b: Starting profiles query...', {
userId: userInfo.id,
clientType: 'authenticated'
});
// Try direct fetch instead of Supabase client to bypass hanging issue
logger.debug('user-context', '🔧 Step 5b1: About to make profiles query with direct fetch...', {
userId: userInfo.id,
queryStarted: true
});
const { data, error } = await fetch(`http://localhost:8000/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
headers: {
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`,
'Content-Type': 'application/json'
}
})
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
return { data: result[0] || null, error: null };
})
.catch((err) => {
logger.debug('user-context', '🔧 Step 5b1: Direct fetch failed', {
userId: userInfo?.id,
error: err.message
});
return { data: null, error: { message: err.message, code: 'FETCH_ERROR' } };
});
logger.debug('user-context', '🔧 Step 5b2: Direct fetch completed...', {
userId: userInfo.id,
hasData: !!data,
hasError: !!error
});
logger.debug('user-context', '🔧 Step 5c: Profiles query completed', {
hasData: !!data,
hasError: !!error,
errorCode: error?.code,
errorMessage: error?.message
});
logger.debug('user-context', '🔧 Step 5a: Profiles query result', {
hasData: !!data,
hasError: !!error,
errorCode: error?.code,
errorMessage: error?.message
});
if (error && error.code !== 'PGRST116') {
logger.warn('user-context', '⚠️ Profiles query failed, using fallback', {
error: error.message,
code: error.code
});
// Don't throw error, just use fallback profile
profileRow = null;
} else if (data) {
profileRow = data;
logger.debug('user-context', '✅ Found profile data in database', {
userId: data.id,
userType: data.user_type,
userDbName: data.user_db_name
});
} else {
logger.debug('user-context', '⚠️ No profile data found - will create default');
profileRow = null;
}
// Clear loading state after profiles query completes
setLoading(false);
logger.debug('user-context', '🔧 Step 5d: Loading state cleared');
logger.debug('user-context', '🔧 Step 6: Processing profile data...', {
userId: userInfo.id,
hasProfileRow: !!profileRow,
hasUserDb: !!profileRow?.user_db_name,
hasSchoolDb: !!profileRow?.school_db_name
});
const metadata = userInfo.user_metadata as CCUserMetadata;
logger.debug('user-context', '🔧 Step 7: Processing user metadata...', {
hasMetadata: !!metadata,
userType: metadata?.user_type
});
let userDbName = profileRow?.user_db_name ?? null;
let schoolDbName = profileRow?.school_db_name ?? null;
const storedUserDb = DatabaseNameService.getStoredUserDatabase();
const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
// Start provisioning in background (non-blocking)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const provisioningPromise = provisionUser(userInfo.id, authSession?.access_token ?? null)
.then(provisioned => {
if (provisioned) {
logger.debug('user-context', '✅ Provisioning completed in background', {
userDbName: provisioned.user_db_name,
workerDbName: provisioned.worker_db_name
});
// Update localStorage with provisioned values
DatabaseNameService.rememberDatabaseNames({
userDbName: provisioned.user_db_name,
schoolDbName: provisioned.worker_db_name || ''
});
}
})
.catch(provisionError => {
logger.warn('user-context', '⚠️ Background provisioning failed', {
userId: userInfo?.id,
provisionError: provisionError instanceof Error ? provisionError.message : String(provisionError)
});
});
if (!userDbName && storedUserDb) {
userDbName = storedUserDb;
}
if (!schoolDbName && storedSchoolDb) {
schoolDbName = storedSchoolDb;
}
logger.debug('user-context', '️ Database name resolution', {
userDbName,
schoolDbName
});
if (!userDbName) {
userDbName = DatabaseNameService.getUserPrivateDB(metadata.user_type || '', userInfo.id);
}
if (!schoolDbName) {
schoolDbName = '';
}
DatabaseNameService.rememberDatabaseNames({
userDbName: String(userDbName || ''),
schoolDbName: String(schoolDbName || '')
});
logger.debug('user-context', '🔧 Creating user profile object...', {
userId: userInfo.id,
userDbName,
schoolDbName,
userType: metadata.user_type
});
const userProfile: CCUser = {
id: userInfo.id,
email: userInfo.email,
user_type: metadata.user_type || '',
username: metadata.username || '',
display_name: String(metadata.display_name || ''),
user_db_name: String(userDbName || ''),
school_db_name: String(schoolDbName || ''),
created_at: userInfo.created_at,
updated_at: userInfo.updated_at
};
if (!mountedRef.current) {
logger.debug('user-context', '❌ Component unmounted during profile creation');
return;
}
logger.debug('user-context', '🔧 Setting profile and preferences...', {
profileId: userProfile.id
});
setProfile(userProfile);
setPreferences({
theme: (profileRow?.theme && typeof profileRow.theme === 'string' && ['system', 'light', 'dark'].includes(profileRow.theme)) ? profileRow.theme as 'system' | 'light' | 'dark' : 'system',
notifications: Boolean(profileRow?.notifications_enabled)
});
logger.debug('user-context', '✅ User profile loaded', {
userId: userProfile.id,
userType: userProfile.user_type,
username: userProfile.username,
userDbName: userProfile.user_db_name,
schoolDbName: userProfile.school_db_name
});
setError(null);
} catch (error) {
logger.error('user-context', '❌ Failed to load user profile', { error });
if (!mountedRef.current) {
return;
}
logger.error('user-context', '❌ Resolving user profile failed', {
message: error instanceof Error ? error.message : String(error)
});
// Create fallback profile even when errors occur
logger.debug('user-context', '🔧 Creating fallback profile due to error...', {
userId: userInfo?.id,
email: userInfo?.email
});
if (userInfo) {
const metadata = userInfo.user_metadata as CCUserMetadata;
const fallbackProfile: CCUser = {
id: userInfo.id,
email: userInfo.email,
user_type: metadata?.user_type || 'email_teacher',
username: metadata?.username || userInfo.email?.split('@')[0] || 'user',
display_name: metadata?.display_name || userInfo.email?.split('@')[0] || 'User',
user_db_name: DatabaseNameService.getUserPrivateDB(metadata?.user_type || 'email_teacher', userInfo.id),
school_db_name: '',
created_at: userInfo.created_at,
updated_at: userInfo.updated_at
};
DatabaseNameService.rememberDatabaseNames({
userDbName: fallbackProfile.user_db_name,
schoolDbName: fallbackProfile.school_db_name
});
setProfile(fallbackProfile);
logger.debug('user-context', '✅ Fallback profile created', {
userId: fallbackProfile.id,
userType: fallbackProfile.user_type,
userDbName: fallbackProfile.user_db_name
});
} else {
setProfile(null);
}
setPreferences({});
setError(error instanceof Error ? error : new Error('Failed to load user profile'));
setLoading(false); // Ensure loading is cleared on error
} finally {
logger.debug('user-context', '🔧 Finalizing user context initialization...', {
isMounted: mountedRef.current
});
if (mountedRef.current) {
// Loading state is already managed above, just log completion
logger.debug('user-context', '✅ User context initialization complete');
}
logger.debug('user-context', '🔧 Step 10: Setting isInitialized to true');
setIsInitialized(true);
logger.debug('user-context', '✅ User context initialized flag set - initialization complete!', {
isInitialized: true,
profileId: profile?.id,
userType: profile?.user_type
});
}
}, [profile?.id, profile?.user_type, isInitialized]);
useEffect(() => {
mountedRef.current = true;
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
if (!mountedRef.current) {
return;
}
logger.debug('user-context', '🔄 Auth state change', {
event,
hasSession: !!session,
hasUser: !!session?.user
});
switch (event) {
case 'SIGNED_OUT':
setLoading(false);
setProfile(null);
setPreferences({});
setIsInitialized(true);
setError(null);
break;
case 'SIGNED_IN':
case 'TOKEN_REFRESHED':
case 'INITIAL_SESSION':
await resolveProfile(session?.user ?? null, session ?? null);
break;
default:
break;
}
});
return () => {
mountedRef.current = false;
subscription.unsubscribe();
};
}, [resolveProfile]);
const updateProfile = async (updates: Partial<CCUser>) => {
if (!user?.id || !profile) {
@@ -168,6 +469,20 @@ export function UserProvider({ children }: { children: React.ReactNode }) {
}
};
// Store profile in localStorage whenever it changes
useEffect(() => {
if (profile) {
storageService.set(StorageKeys.USER, profile);
logger.debug('user-context', '💾 Stored user profile in localStorage', {
userId: profile.id,
userType: profile.user_type
});
} else {
storageService.remove(StorageKeys.USER);
logger.debug('user-context', '🗑️ Removed user profile from localStorage');
}
}, [profile]);
return (
<UserContext.Provider
value={{