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
+202 -62
View File
@@ -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) {