import { User, AuthChangeEvent, Session } from '@supabase/supabase-js'; import { TLUserPreferences } from '@tldraw/tldraw'; import { supabase } from '../../supabaseClient'; import { storageService, StorageKeys } from './localStorageService'; import { logger } from '../../debugConfig'; import { DatabaseNameService } from '../graph/databaseNameService'; export interface CCUser { id: string; email?: string; user_type: string; username: string; display_name: string; user_db_name: string; school_db_name: string; created_at?: string; updated_at?: string; } export interface CCUserMetadata { username?: string; user_type?: string; display_name?: string; email?: string; name?: string; preferred_username?: string; [key: string]: string | undefined; } export function convertToCCUser(user: User, metadata: CCUserMetadata): CCUser { // Extract username from various possible sources const username = metadata.username || metadata.preferred_username || metadata.email?.split('@')[0] || user.email?.split('@')[0] || 'user'; // Extract display name from various possible sources const displayName = metadata.display_name || metadata.name || metadata.preferred_username || username; // Default to student if no user type specified const userType = metadata.user_type || 'student'; const storedUserDb = DatabaseNameService.getStoredUserDatabase(); const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase(); const userDbName = storedUserDb || DatabaseNameService.getUserPrivateDB( userType, user.id ); const schoolDbName = metadata.school_db_name || metadata.worker_db_name || storedSchoolDb || ''; DatabaseNameService.rememberDatabaseNames({ userDbName, schoolDbName }); return { id: user.id, email: user.email, user_type: userType, username: username, display_name: displayName, user_db_name: userDbName, school_db_name: schoolDbName, created_at: user.created_at, updated_at: user.updated_at, }; } export type UserRole = | 'email_teacher' | 'email_student' | 'cc_admin' | 'cc_developer' | 'super_admin'; // Login response export interface LoginResponse { user: CCUser | null; accessToken: string | null; userRole: string; message: string | null; } // Session response export interface SessionResponse { user: CCUser | null; accessToken: string | null; message: string | null; } // Registration response export interface RegistrationResponse extends LoginResponse { user: CCUser; accessToken: string | null; userRole: UserRole; message: string | null; } export interface EmailCredentials { email: string; password: string; role: 'email_teacher' | 'email_student'; } export type AuthCredentials = EmailCredentials; export const getTldrawPreferences = (user: CCUser): TLUserPreferences => { return { id: user.id, colorScheme: 'system', }; }; class AuthService { private static instance: AuthService; private constructor() {} onAuthStateChange( callback: (event: AuthChangeEvent, session: Session | null) => void ) { return supabase.auth.onAuthStateChange((event, session) => { logger.info('auth-service', '🔄 Auth state changed', { event, hasSession: !!session, userId: session?.user?.id, eventType: event, }); // Ensure we clear storage on signout if (event === 'SIGNED_OUT') { storageService.clearAll(); } callback(event, session); }); } static getInstance(): AuthService { if (!AuthService.instance) { AuthService.instance = new AuthService(); } return AuthService.instance; } async getCurrentSession(): Promise { try { const { data: { session }, error, } = await supabase.auth.getSession(); if (error) { throw error; } if (!session) { return { user: null, accessToken: null, message: 'No active session' }; } return { user: convertToCCUser( session.user, session.user.user_metadata as CCUserMetadata ), accessToken: session.access_token, message: 'Session retrieved', }; } catch (error) { logger.error('auth-service', 'Failed to get current session:', error); throw error; } } async getCurrentUser(): Promise { try { const { data: { user }, error, } = await supabase.auth.getUser(); if (error || !user) { return null; } return convertToCCUser(user, user.user_metadata as CCUserMetadata); } catch (error) { logger.error('auth-service', 'Failed to get current user:', error); return null; } } async login({ email, password, role, }: EmailCredentials): Promise { try { logger.info('auth-service', '🔄 Attempting login', { email, role, }); const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) { logger.error('auth-service', '❌ Supabase auth error', { error: error.message, status: error.status, }); throw error; } if (!data.session) { logger.error('auth-service', '❌ No session after login'); throw new Error('No session after login'); } const ccUser = convertToCCUser( data.user, data.user.user_metadata as CCUserMetadata ); // Store auth session in storage storageService.set(StorageKeys.USER_ROLE, ccUser.user_type); storageService.set(StorageKeys.USER, ccUser); storageService.set(StorageKeys.SUPABASE_TOKEN, data.session.access_token); storageService.set(StorageKeys.SUPABASE_SESSION, data.session); logger.info('auth-service', '✅ Login successful', { userId: ccUser.id, role: ccUser.user_type, username: ccUser.username, }); return { user: ccUser, accessToken: data.session.access_token, userRole: ccUser.user_type, message: 'Login successful', }; } catch (error) { logger.error('auth-service', '❌ Login failed:', error); throw error; } } async logout(): Promise { try { logger.debug('auth-service', '🔄 Attempting logout'); const { error } = await supabase.auth.signOut({ scope: 'local' }); if (error) { logger.error('auth-service', '❌ Logout failed:', error); throw error; } // Clear all stored data storageService.clearAll(); // Force a refresh of the auth state await supabase.auth.refreshSession(); logger.debug('auth-service', '✅ Logout successful'); } catch (error) { logger.error('auth-service', '❌ Logout failed:', error); throw error; } } } export const authService = AuthService.getInstance();