app/src/services/auth/authService.ts
2025-11-14 14:47:26 +00:00

274 lines
7.0 KiB
TypeScript

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<SessionResponse> {
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<CCUser | null> {
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<LoginResponse> {
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<void> {
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();