latest
This commit is contained in:
+202
-62
@@ -1,8 +1,11 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
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 { DatabaseNameService } from '../services/graph/databaseNameService';
|
||||
import { storageService, StorageKeys } from '../services/auth/localStorageService';
|
||||
|
||||
export interface AuthContextType {
|
||||
user: CCUser | null;
|
||||
@@ -28,64 +31,205 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState<CCUser | null>(null);
|
||||
const [user_role, setUserRole] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (user) {
|
||||
const metadata = user.user_metadata as CCUserMetadata;
|
||||
setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`,
|
||||
school_db_name: 'cc.institutes.development.default',
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at
|
||||
});
|
||||
setUserRole(metadata.user_role || null);
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('auth-context', '❌ Failed to load user', { error });
|
||||
setError(error instanceof Error ? error : new Error('Failed to load user'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const persistSession = useCallback((session: Session | null) => {
|
||||
if (session) {
|
||||
storageService.set(StorageKeys.SUPABASE_SESSION, session);
|
||||
} else {
|
||||
storageService.remove(StorageKeys.SUPABASE_SESSION);
|
||||
}
|
||||
}, []);
|
||||
|
||||
loadUser();
|
||||
const restoreSessionFromStorage = useCallback(async (): Promise<Session | null> => {
|
||||
const persistedSession = storageService.get(StorageKeys.SUPABASE_SESSION);
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
if (event === 'SIGNED_IN' && session?.user) {
|
||||
const metadata = session.user.user_metadata as CCUserMetadata;
|
||||
setUser({
|
||||
id: session.user.id,
|
||||
email: session.user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`,
|
||||
school_db_name: 'cc.institutes.development.default',
|
||||
created_at: session.user.created_at,
|
||||
updated_at: session.user.updated_at
|
||||
});
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setUser(null);
|
||||
}
|
||||
if (!persistedSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!persistedSession.access_token || !persistedSession.refresh_token) {
|
||||
storageService.remove(StorageKeys.SUPABASE_SESSION);
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data: restored, error: restoreError } = await supabase.auth.setSession({
|
||||
access_token: persistedSession.access_token,
|
||||
refresh_token: persistedSession.refresh_token,
|
||||
});
|
||||
|
||||
if (restoreError) {
|
||||
logger.warn('auth-context', '⚠️ Failed to restore persisted Supabase session', {
|
||||
error: restoreError.message ?? restoreError,
|
||||
});
|
||||
storageService.remove(StorageKeys.SUPABASE_SESSION);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (restored.session) {
|
||||
persistSession(restored.session);
|
||||
return restored.session;
|
||||
}
|
||||
|
||||
storageService.remove(StorageKeys.SUPABASE_SESSION);
|
||||
return null;
|
||||
}, [persistSession]);
|
||||
|
||||
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 storedUserDb = DatabaseNameService.getStoredUserDatabase();
|
||||
const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
|
||||
|
||||
const userDbName = storedUserDb || DatabaseNameService.getUserPrivateDB(userType || 'standard', supabaseUser.id);
|
||||
const schoolDbName = storedSchoolDb || '';
|
||||
|
||||
const resolvedUser: CCUser = {
|
||||
id: supabaseUser.id,
|
||||
email: supabaseUser.email,
|
||||
user_type: userType,
|
||||
username: baseUsername,
|
||||
display_name: baseDisplayName,
|
||||
user_db_name: userDbName,
|
||||
school_db_name: schoolDbName,
|
||||
created_at: supabaseUser.created_at,
|
||||
updated_at: supabaseUser.updated_at
|
||||
};
|
||||
|
||||
const resolvedRole = metadata.user_role || userType || null;
|
||||
return { user: resolvedUser, role: resolvedRole };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadInitialSession = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let activeSession: Session | null = session ?? null;
|
||||
|
||||
if (!activeSession) {
|
||||
activeSession = await restoreSessionFromStorage();
|
||||
}
|
||||
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeSession?.user) {
|
||||
persistSession(activeSession);
|
||||
try {
|
||||
const { user: resolvedUser, role } = await buildUserFromSupabase(activeSession.user);
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
setUser(resolvedUser);
|
||||
setUserRole(role);
|
||||
} catch (buildError) {
|
||||
logger.error('auth-context', '❌ Failed to build user from initial session', {
|
||||
error: buildError,
|
||||
});
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
setUser(null);
|
||||
setUserRole(null);
|
||||
setError(buildError instanceof Error ? buildError : new Error('Failed to load user'));
|
||||
}
|
||||
} else {
|
||||
persistSession(null);
|
||||
setUser(null);
|
||||
setUserRole(null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('auth-context', '❌ Failed to load initial session', { error });
|
||||
if (isMounted) {
|
||||
setError(error instanceof Error ? error : new Error('Failed to load user'));
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadInitialSession();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
case 'SIGNED_IN':
|
||||
case 'TOKEN_REFRESHED':
|
||||
case 'INITIAL_SESSION': {
|
||||
persistSession(session ?? null);
|
||||
if (session?.user) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { user: resolvedUser, role } = await buildUserFromSupabase(session.user);
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
setUser(resolvedUser);
|
||||
setUserRole(role);
|
||||
} catch (buildError) {
|
||||
logger.error('auth-context', '❌ Failed to build user from session', {
|
||||
event,
|
||||
error: buildError,
|
||||
});
|
||||
setUser(null);
|
||||
setUserRole(null);
|
||||
setError(buildError instanceof Error ? buildError : new Error('Failed to load user'));
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
setUserRole(null);
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SIGNED_OUT': {
|
||||
persistSession(null);
|
||||
setUser(null);
|
||||
setUserRole(null);
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [buildUserFromSupabase, persistSession, restoreSessionFromStorage]);
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
try {
|
||||
@@ -97,19 +241,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (signInError) throw signInError;
|
||||
|
||||
if (data.session) {
|
||||
persistSession(data.session);
|
||||
}
|
||||
|
||||
if (data.user) {
|
||||
const metadata = data.user.user_metadata as CCUserMetadata;
|
||||
setUser({
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`,
|
||||
school_db_name: 'cc.institutes.development.default',
|
||||
created_at: data.user.created_at,
|
||||
updated_at: data.user.updated_at
|
||||
});
|
||||
const { user: resolvedUser, role } = await buildUserFromSupabase(data.user);
|
||||
setUser(resolvedUser);
|
||||
setUserRole(role);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('auth-context', '❌ Sign in failed', { error });
|
||||
@@ -124,6 +263,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
try {
|
||||
setLoading(true);
|
||||
await authService.logout();
|
||||
persistSession(null);
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
|
||||
@@ -29,6 +29,13 @@ export const NeoInstituteProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
logger.debug('neo-institute-context', '🔄 useEffect triggered', {
|
||||
isUserInitialized,
|
||||
hasProfile: !!profile,
|
||||
hasUser: !!user,
|
||||
isInitialized
|
||||
});
|
||||
|
||||
// Wait for user profile to be ready
|
||||
if (!isUserInitialized) {
|
||||
logger.debug('neo-institute-context', '⏳ Waiting for user initialization...');
|
||||
@@ -39,6 +46,7 @@ export const NeoInstituteProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
if (!profile || !profile.school_db_name) {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
logger.debug('neo-institute-context', 'ℹ️ No school database; marking institute context ready');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +62,7 @@ export const NeoInstituteProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
if (node) {
|
||||
setSchoolNode(node);
|
||||
logger.debug('neo-institute-context', '✅ School node loaded', {
|
||||
schoolId: node.unique_id,
|
||||
schoolId: node.uuid_string,
|
||||
dbName: profile.school_db_name
|
||||
});
|
||||
} else {
|
||||
@@ -68,14 +76,15 @@ export const NeoInstituteProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
schoolDbName: profile.school_db_name
|
||||
});
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
logger.debug('neo-institute-context', '✅ Institute context initialization complete');
|
||||
}
|
||||
};
|
||||
|
||||
loadSchoolNode();
|
||||
}, [user?.email, profile, isUserInitialized]);
|
||||
}, [user, profile, isUserInitialized, isInitialized]);
|
||||
|
||||
return (
|
||||
<NeoInstituteContext.Provider value={{
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAuth } from './AuthContext';
|
||||
import { useUser } from './UserContext';
|
||||
import { logger } from '../debugConfig';
|
||||
import { CCUserNodeProps, CCCalendarNodeProps, CCUserTeacherTimetableNodeProps } from '../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { DatabaseNameService } from '../services/graph/databaseNameService';
|
||||
import { CalendarStructure, WorkerStructure } from '../types/navigation';
|
||||
import { useNavigationStore } from '../stores/navigationStore';
|
||||
|
||||
@@ -11,7 +12,7 @@ export interface CalendarNode {
|
||||
id: string;
|
||||
label: string;
|
||||
title: string;
|
||||
tldraw_snapshot: string;
|
||||
node_storage_path: string;
|
||||
type?: CCCalendarNodeProps['__primarylabel__'];
|
||||
nodeData?: CCCalendarNodeProps;
|
||||
}
|
||||
@@ -20,7 +21,7 @@ export interface WorkerNode {
|
||||
id: string;
|
||||
label: string;
|
||||
title: string;
|
||||
tldraw_snapshot: string;
|
||||
node_storage_path: string;
|
||||
type?: CCUserTeacherTimetableNodeProps['__primarylabel__'];
|
||||
nodeData?: CCUserTeacherTimetableNodeProps;
|
||||
}
|
||||
@@ -162,8 +163,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
backgroundColor: '#ffffff',
|
||||
isLocked: false,
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: '',
|
||||
tldraw_snapshot: '',
|
||||
uuid_string: '',
|
||||
node_storage_path: '',
|
||||
created: new Date().toISOString(),
|
||||
merged: new Date().toISOString(),
|
||||
state: {
|
||||
@@ -177,7 +178,34 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
|
||||
// Initialize context when dependencies are ready
|
||||
useEffect(() => {
|
||||
if (!isUserInitialized || !profile || isInitialized || initializationRef.current.hasStarted) {
|
||||
logger.debug('neo-user-context', '🔄 useEffect triggered', {
|
||||
isUserInitialized,
|
||||
hasProfile: !!profile,
|
||||
hasUser: !!user,
|
||||
isInitialized,
|
||||
hasStarted: initializationRef.current.hasStarted
|
||||
});
|
||||
|
||||
if (!isUserInitialized) {
|
||||
logger.debug('neo-user-context', '⏳ Waiting for user context initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
if (!initializationRef.current.isComplete) {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
initializationRef.current.isComplete = true;
|
||||
logger.debug('neo-user-context', 'ℹ️ No profile available; marking context initialized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInitialized || initializationRef.current.hasStarted) {
|
||||
logger.debug('neo-user-context', 'ℹ️ Initialization already in progress or complete', {
|
||||
isInitialized,
|
||||
hasStarted: initializationRef.current.hasStarted
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,8 +217,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
|
||||
// Set database names
|
||||
const userDb = profile.user_db_name || (user?.email ?
|
||||
`cc.users.${user.email.replace('@', 'at').replace(/\./g, 'dot')}` : null);
|
||||
|
||||
DatabaseNameService.getStoredUserDatabase() || null : null);
|
||||
|
||||
if (!userDb) {
|
||||
throw new Error('No user database name available');
|
||||
}
|
||||
@@ -199,27 +227,42 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
logger.debug('neo-user-context', '🔄 Starting context initialization');
|
||||
|
||||
// Initialize user node
|
||||
await navigationStore.switchContext({
|
||||
main: 'profile',
|
||||
base: 'profile',
|
||||
extended: 'overview'
|
||||
}, userDb, profile.school_db_name);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
main: 'profile',
|
||||
base: 'profile',
|
||||
extended: 'overview'
|
||||
}, userDb, profile.school_db_name);
|
||||
|
||||
const userNavigationNode = navigationStore.context.node;
|
||||
if (userNavigationNode?.data) {
|
||||
const userNodeData: CCUserNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'User',
|
||||
unique_id: userNavigationNode.id,
|
||||
tldraw_snapshot: userNavigationNode.tldraw_snapshot || '',
|
||||
title: String(userNavigationNode.data?.user_name || 'User'),
|
||||
user_name: String(userNavigationNode.data?.user_name || 'User'),
|
||||
user_email: user?.email || '',
|
||||
user_type: 'User',
|
||||
user_id: userNavigationNode.id,
|
||||
worker_node_data: JSON.stringify(userNavigationNode.data || {})
|
||||
};
|
||||
setUserNode(userNodeData);
|
||||
const userNavigationNode = navigationStore.context.node;
|
||||
if (userNavigationNode?.id && userNavigationNode?.data) {
|
||||
const userNodeData: CCUserNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'User',
|
||||
uuid_string: userNavigationNode.id,
|
||||
node_storage_path: userNavigationNode.node_storage_path || '',
|
||||
title: String(userNavigationNode.data?.user_name || 'User'),
|
||||
user_name: String(userNavigationNode.data?.user_name || 'User'),
|
||||
user_email: user?.email || '',
|
||||
user_type: 'User',
|
||||
user_id: userNavigationNode.id,
|
||||
worker_node_data: JSON.stringify(userNavigationNode.data || {})
|
||||
};
|
||||
setUserNode(userNodeData);
|
||||
logger.debug('neo-user-context', '✅ User node loaded from navigation store');
|
||||
} else if (userNavigationNode?.id) {
|
||||
logger.debug('neo-user-context', 'ℹ️ User node exists but data not yet loaded - will retry later', {
|
||||
nodeId: userNavigationNode.id,
|
||||
hasData: !!userNavigationNode.data
|
||||
});
|
||||
} else {
|
||||
logger.debug('neo-user-context', 'ℹ️ No user node in navigation store yet - will retry later');
|
||||
}
|
||||
} catch (navError) {
|
||||
logger.warn('neo-user-context', '⚠️ Navigation store initialization failed - continuing without user node', {
|
||||
error: navError instanceof Error ? navError.message : String(navError)
|
||||
});
|
||||
// Continue without user node - this is not critical for basic functionality
|
||||
}
|
||||
|
||||
// Set final state
|
||||
@@ -228,7 +271,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
setIsInitialized(true);
|
||||
setIsLoading(false);
|
||||
initializationRef.current.isComplete = true;
|
||||
|
||||
|
||||
logger.debug('neo-user-context', '✅ Context initialization complete');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to initialize user context';
|
||||
@@ -241,7 +284,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
};
|
||||
|
||||
initializeContext();
|
||||
}, [user?.email, profile, isUserInitialized, navigationStore, isInitialized]);
|
||||
}, [user, profile, isUserInitialized, navigationStore, isInitialized]);
|
||||
|
||||
// Calendar Navigation Functions
|
||||
const navigateToDay = async (id: string) => {
|
||||
@@ -258,8 +301,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarDay',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'day',
|
||||
@@ -272,7 +315,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'CalendarDay',
|
||||
nodeData
|
||||
});
|
||||
@@ -298,8 +341,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarWeek',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'week',
|
||||
@@ -312,7 +355,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'CalendarWeek',
|
||||
nodeData
|
||||
});
|
||||
@@ -338,8 +381,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarMonth',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'month',
|
||||
@@ -352,7 +395,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'CalendarMonth',
|
||||
nodeData
|
||||
});
|
||||
@@ -378,8 +421,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarYear',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'year',
|
||||
@@ -392,7 +435,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'CalendarYear',
|
||||
nodeData
|
||||
});
|
||||
@@ -419,8 +462,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: id || node.id
|
||||
@@ -430,7 +473,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
@@ -456,8 +499,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: id || node.id
|
||||
@@ -467,7 +510,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
@@ -493,8 +536,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: id || node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: id || node.id
|
||||
@@ -504,7 +547,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
@@ -531,8 +574,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: node.id
|
||||
@@ -542,7 +585,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
@@ -569,8 +612,8 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
uuid_string: node.id,
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: node.id
|
||||
@@ -580,7 +623,7 @@ export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children })
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
node_storage_path: node.node_storage_path || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
|
||||
+377
-62
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user