Initial commit

This commit is contained in:
2025-07-11 13:21:49 +00:00
commit 8a7ab3ac24
262 changed files with 28219 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
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 userDbName = DatabaseNameService.getUserPrivateDB(
userType,
username
);
const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB();
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);
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();
+111
View File
@@ -0,0 +1,111 @@
import React from 'react';
import { TLUserPreferences, TLUser } from '@tldraw/tldraw';
import { CCUser } from '../../services/auth/authService';
import { logger } from '../../debugConfig';
// Type-safe storage keys
export enum StorageKeys {
USER = 'user',
USER_ROLE = 'user_role',
SUPABASE_TOKEN = 'supabase_token',
MS_TOKEN = 'msAccessToken',
NEO4J_USER_DB = 'neo4jUserDbName',
NEO4J_WORKER_DB = 'neo4jWorkerDbName',
USER_NODES = 'userNodes',
CALENDAR_DATA = 'calendarData',
IS_NEW_REGISTRATION = 'isNewRegistration',
TLDRAW_PREFERENCES = 'tldrawUserPreferences',
TLDRAW_FILE_PATH = 'tldrawUserFilePath',
LOCAL_SNAPSHOT = 'localSnapshot',
NODE_FILE_PATH = 'nodeFilePath',
ONENOTE_NOTEBOOK = 'oneNoteNotebook',
PRESENTATION_MODE = 'presentationMode',
TLDRAW_USER = 'tldrawUser'
}
interface StorageValueTypes {
[StorageKeys.USER]: CCUser;
[StorageKeys.USER_ROLE]: string;
[StorageKeys.SUPABASE_TOKEN]: string;
[StorageKeys.MS_TOKEN]: string;
[StorageKeys.NEO4J_USER_DB]: string;
[StorageKeys.NEO4J_WORKER_DB]: string;
[StorageKeys.USER_NODES]: any[];
[StorageKeys.CALENDAR_DATA]: any;
[StorageKeys.IS_NEW_REGISTRATION]: boolean;
[StorageKeys.TLDRAW_PREFERENCES]: TLUserPreferences;
[StorageKeys.TLDRAW_FILE_PATH]: string;
[StorageKeys.LOCAL_SNAPSHOT]: any;
[StorageKeys.NODE_FILE_PATH]: string;
[StorageKeys.ONENOTE_NOTEBOOK]: any;
[StorageKeys.PRESENTATION_MODE]: boolean;
[StorageKeys.TLDRAW_USER]: TLUser;
}
type StorageKey = keyof StorageValueTypes;
class StorageService {
private static instance: StorageService;
private constructor() {}
static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
get<K extends StorageKey>(key: K): StorageValueTypes[K] | null {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
logger.error('storage-service', `Error retrieving ${key}:`, error);
return null;
}
}
set<K extends StorageKey>(key: K, value: StorageValueTypes[K]): void {
try {
const serializedValue = JSON.stringify(value);
localStorage.setItem(key, serializedValue);
logger.debug('storage-service', `Stored ${key} in localStorage`);
} catch (error) {
logger.error('storage-service', `Error storing ${key}:`, error);
}
}
remove(key: StorageKey): void {
try {
localStorage.removeItem(key);
logger.debug('storage-service', `Removed ${key} from localStorage`);
} catch (error) {
logger.error('storage-service', `Error removing ${key}:`, error);
}
}
clearAll(): void {
try {
// Only clear app-specific storage, not Supabase's internal keys
Object.values(StorageKeys).forEach(key => {
localStorage.removeItem(key);
});
logger.debug('storage-service', 'Cleared all app items from localStorage');
} catch (error) {
logger.error('storage-service', 'Error clearing storage:', error);
}
}
// Helper method to update state and storage together
setStateAndStorage<K extends StorageKey>(
setter: React.Dispatch<React.SetStateAction<StorageValueTypes[K]>>,
key: K,
value: StorageValueTypes[K]
): void {
setter(value);
this.set(key, value);
}
}
export const storageService = StorageService.getInstance();
@@ -0,0 +1,107 @@
import { supabase } from '../../../supabaseClient';
import axios from '../../../axiosConfig';
export interface StandardizedOneNoteDetails {
id: string;
displayName: string;
createdDateTime: string;
lastModifiedDateTime: string;
links: {
oneNoteClientUrl: string;
oneNoteWebUrl: string;
};
}
export async function updateUserOneNoteDetails(userId: string, oneNoteDetails: StandardizedOneNoteDetails) {
const { error } = await supabase
.from('profiles')
.update({
one_note_details: oneNoteDetails,
updated_at: new Date().toISOString()
})
.eq('id', userId);
if (error) {
console.error('Error updating OneNote details:', error);
throw error;
}
}
export async function getOneNoteNotebooks(msAccessToken: string) {
try {
const response = await axios.get(`/msgraph/onenote/get-onenote-notebooks`, {
headers: {
'Authorization': `Bearer ${msAccessToken}`,
'Content-Type': 'application/json'
},
});
return response.data;
} catch (error) {
console.error('Error getting notebooks:', error);
throw error;
}
}
export async function createOneNoteNotebook(msAccessToken: string, uid: string): Promise<StandardizedOneNoteDetails> {
if (!msAccessToken) {
throw new Error('Microsoft token not found');
}
try {
const notebooks = await getOneNoteNotebooks(msAccessToken);
const existingNotebook = notebooks.value.find((notebook: any) =>
notebook.displayName === 'Classroom Copilot'
);
if (existingNotebook) {
const standardizedNotebook = standardizeNotebookDetails(existingNotebook);
await updateUserOneNoteDetails(uid, standardizedNotebook);
return standardizedNotebook;
}
const response = await axios.post(
`/msgraph/onenote/create-onenote-notebook?notebook_name=${encodeURIComponent('Classroom Copilot')}`,
{},
{
headers: {
'Authorization': `Bearer ${msAccessToken}`,
'Content-Type': 'application/json'
},
}
);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const standardizedNotebook = standardizeNotebookDetails(response.data);
await updateUserOneNoteDetails(uid, standardizedNotebook);
return standardizedNotebook;
} catch (error) {
console.error('Error creating notebook:', error);
throw error;
}
}
export async function registerOneNoteUser(msAccessToken: string, uid: string) {
try {
return await createOneNoteNotebook(msAccessToken, uid);
} catch (error) {
console.error('Error registering Microsoft user:', error);
throw error;
}
}
function standardizeNotebookDetails(notebook: any): StandardizedOneNoteDetails {
const notebookData = notebook.data || notebook;
return {
id: notebookData.id || '',
displayName: notebookData.displayName || '',
createdDateTime: notebookData.createdDateTime || '',
lastModifiedDateTime: notebookData.lastModifiedDateTime || '',
links: {
oneNoteClientUrl: notebookData.links?.oneNoteClientUrl?.href || '',
oneNoteWebUrl: notebookData.links?.oneNoteWebUrl?.href || '',
},
};
}
+86
View File
@@ -0,0 +1,86 @@
import { TLUserPreferences } from '@tldraw/tldraw';
import { supabase } from '../../supabaseClient';
import { logger } from '../../debugConfig';
import { CCUser } from './authService';
export type UserProfile = CCUser;
export interface UserProfileUpdate extends Partial<UserProfile> {
id: string; // ID is always required for updates
}
export interface UserPreferences {
tldraw?: TLUserPreferences;
theme?: 'light' | 'dark' | 'system';
notifications?: boolean;
}
export async function createUserProfile(profile: UserProfile): Promise<UserProfile | null> {
try {
const { data, error } = await supabase
.from('profiles')
.insert([profile])
.select()
.single();
if (error) {
logger.error('supabase-profile-service', '❌ Failed to create user profile', {
userId: profile.id,
error
});
throw error;
}
return data;
} catch (error) {
logger.error('supabase-profile-service', '❌ Error in createUserProfile', error);
return null;
}
}
export async function getUserProfile(userId: string): Promise<UserProfile | null> {
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single();
if (error) {
logger.error('supabase-profile-service', '❌ Failed to fetch user profile', {
userId,
error
});
throw error;
}
return data;
} catch (error) {
logger.error('supabase-profile-service', '❌ Error in getUserProfile', error);
return null;
}
}
export async function updateUserProfile(update: UserProfileUpdate): Promise<UserProfile | null> {
try {
const { data, error } = await supabase
.from('profiles')
.update(update)
.eq('id', update.id)
.select()
.single();
if (error) {
logger.error('supabase-profile-service', '❌ Failed to update user profile', {
userId: update.id,
error
});
throw error;
}
return data;
} catch (error) {
logger.error('supabase-profile-service', '❌ Error in updateUserProfile', error);
return null;
}
}
+118
View File
@@ -0,0 +1,118 @@
import { supabase } from '../../supabaseClient';
import { CCUser, convertToCCUser } from '../../services/auth/authService';
import { EmailCredentials } from '../../services/auth/authService';
import { formatEmailForDatabase } from '../graph/neoDBService';
import { RegistrationResponse } from '../../services/auth/authService';
import { neoRegistrationService } from '../graph/neoRegistrationService';
import { storageService, StorageKeys } from './localStorageService';
import { logger } from '../../debugConfig';
const REGISTRATION_SERVICE = 'registration-service';
export class RegistrationService {
private static instance: RegistrationService;
private constructor() {}
static getInstance(): RegistrationService {
if (!RegistrationService.instance) {
RegistrationService.instance = new RegistrationService();
}
return RegistrationService.instance;
}
async register(credentials: EmailCredentials, displayName: string): Promise<RegistrationResponse> {
try {
logger.debug(REGISTRATION_SERVICE, '🔄 Starting registration', {
email: credentials.email,
role: credentials.role,
hasDisplayName: !!displayName
});
// Generate username from email (or use another method)
const username = formatEmailForDatabase(credentials.email);
// 1. First sign up the user in auth
const { data: authData, error: signUpError } = await supabase.auth.signUp({
email: credentials.email,
password: credentials.password,
options: {
data: {
user_type: credentials.role,
username: username,
display_name: displayName
}
}
});
if (signUpError) {
logger.error(REGISTRATION_SERVICE, '❌ Supabase signup error', { error: signUpError });
throw signUpError;
}
if (!authData.user) {
logger.error(REGISTRATION_SERVICE, '❌ No user data after registration');
throw new Error('No user data after registration');
}
const ccUser: CCUser = convertToCCUser(authData.user, authData.user.user_metadata);
// 2. Update the profile with the correct user type
const { error: updateError } = await supabase
.from('profiles')
.update({
user_type: credentials.role,
username: username,
display_name: displayName
})
.eq('id', authData.user.id)
.select()
.single();
if (updateError) {
logger.error(REGISTRATION_SERVICE, '❌ Failed to update profile', updateError);
throw updateError;
}
storageService.set(StorageKeys.IS_NEW_REGISTRATION, true);
// 3. Create Neo4j nodes
try {
const userNode = await neoRegistrationService.registerNeo4JUser(
ccUser,
username, // Pass username for database operations
credentials.role
);
logger.info(REGISTRATION_SERVICE, '✅ Registration successful with Neo4j setup', {
userId: ccUser.id,
hasUserNode: !!userNode
});
return {
user: ccUser,
accessToken: authData.session?.access_token || null,
userRole: credentials.role,
message: 'Registration successful'
};
} catch (neo4jError) {
logger.warn(REGISTRATION_SERVICE, '⚠️ Neo4j setup problem', {
userId: ccUser.id,
error: neo4jError
});
// Return success even if Neo4j setup is pending
return {
user: ccUser,
accessToken: authData.session?.access_token || null,
userRole: credentials.role,
message: 'Registration successful - Neo4j setup pending'
};
}
} catch (error) {
logger.error(REGISTRATION_SERVICE, '❌ Registration failed:', error);
throw error;
}
}
}
export const registrationService = RegistrationService.getInstance();