feat(phase-a): remove Neo4j from startup chain, clean auth flow
- Remove NeoUserProvider + NeoInstituteProvider from App.tsx startup chain - Strip user_db_name/school_db_name from CCUser; add school_id (Phase B wires it to Supabase) - Remove DatabaseNameService from AuthContext and UserContext - Remove provisionUser() call from login path; API endpoint preserved for Phase B decision - Simplify UserContext.resolveProfile: fast-path JWT metadata then background Supabase fetch - Replace user.user_db_name reads in singlePlayerPage + snapshotService with null-safe guards - Add useDeviceContext hook (desktop/tablet/phone/iwb, persists to localStorage) App now loads to dashboard without any Neo4j dependency at startup. Canvas opens to blank TLDraw state; Phase B rebuilds navigation on Supabase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4139eb8fd3
commit
7b546c933e
@ -4,8 +4,6 @@ import { theme } from './services/themeService';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { TLDrawProvider } from './contexts/TLDrawContext';
|
||||
import { UserProvider } from './contexts/UserContext';
|
||||
import { NeoUserProvider } from './contexts/NeoUserContext';
|
||||
import { NeoInstituteProvider } from './contexts/NeoInstituteContext';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ 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 {
|
||||
@ -52,20 +51,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
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,
|
||||
school_id: null,
|
||||
created_at: supabaseUser.created_at,
|
||||
updated_at: supabaseUser.updated_at
|
||||
};
|
||||
|
||||
@ -4,8 +4,6 @@ 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 {
|
||||
@ -112,33 +110,23 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
let profileRow: Record<string, unknown> | null = null;
|
||||
|
||||
// Fast-path: build profile from auth metadata + localStorage immediately.
|
||||
// This clears the spinner before any network call so the page renders on refresh
|
||||
// without waiting for the Supabase profiles query (~200ms).
|
||||
// Fast-path: build profile from auth metadata immediately (no spinner on refresh).
|
||||
const fastMetadata = userInfo.user_metadata as CCUserMetadata;
|
||||
const fastStoredUserDb = DatabaseNameService.getStoredUserDatabase();
|
||||
const fastStoredSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
|
||||
const fastUserDb = fastStoredUserDb || DatabaseNameService.getUserPrivateDB(fastMetadata?.user_type || '', userInfo.id);
|
||||
const fastProfile: CCUser = {
|
||||
id: userInfo.id,
|
||||
email: userInfo.email,
|
||||
user_type: fastMetadata?.user_type || '',
|
||||
username: fastMetadata?.username || userInfo.email?.split('@')[0] || 'user',
|
||||
display_name: String(fastMetadata?.display_name || ''),
|
||||
user_db_name: String(fastUserDb || ''),
|
||||
school_db_name: String(fastStoredSchoolDb || ''),
|
||||
school_id: null,
|
||||
created_at: userInfo.created_at,
|
||||
updated_at: userInfo.updated_at
|
||||
};
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName: fastProfile.user_db_name,
|
||||
schoolDbName: fastProfile.school_db_name
|
||||
});
|
||||
if (mountedRef.current && !isInitialized) {
|
||||
setProfile(fastProfile);
|
||||
setLoading(false);
|
||||
setIsInitialized(true);
|
||||
logger.debug('user-context', '⚡ Fast-path: profile initialized from auth metadata, no spinner');
|
||||
logger.debug('user-context', '⚡ Fast-path: profile initialized from auth metadata');
|
||||
}
|
||||
|
||||
// Background: query Supabase profiles for authoritative data (user_db_name, school_db_name, display_name).
|
||||
@ -172,80 +160,15 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
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 || ''),
|
||||
school_id: (profileRow?.school_id as string) ?? null,
|
||||
created_at: userInfo.created_at,
|
||||
updated_at: userInfo.updated_at
|
||||
};
|
||||
@ -268,9 +191,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
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
|
||||
username: userProfile.username
|
||||
});
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
@ -295,22 +216,14 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
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: '',
|
||||
school_id: null,
|
||||
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
|
||||
userType: fallbackProfile.user_type
|
||||
});
|
||||
} else {
|
||||
setProfile(null);
|
||||
|
||||
37
src/hooks/useDeviceContext.ts
Normal file
37
src/hooks/useDeviceContext.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export type DeviceType = 'desktop' | 'tablet' | 'phone' | 'iwb';
|
||||
|
||||
function detectDeviceType(): DeviceType {
|
||||
const width = window.innerWidth;
|
||||
const touchPoints = navigator.maxTouchPoints ?? 0;
|
||||
const hasTouch = touchPoints > 0 || 'ontouchstart' in window;
|
||||
|
||||
if (width >= 1280 && !hasTouch) return 'desktop';
|
||||
if (width >= 768 && hasTouch) return 'tablet';
|
||||
if (width < 768) return 'phone';
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'cc_device_type';
|
||||
|
||||
export function useDeviceContext() {
|
||||
const [deviceType, setDeviceTypeState] = useState<DeviceType>(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY) as DeviceType | null;
|
||||
if (stored && ['desktop', 'tablet', 'phone', 'iwb'].includes(stored)) return stored;
|
||||
return detectDeviceType();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY, deviceType);
|
||||
}, [deviceType]);
|
||||
|
||||
const setDeviceType = (type: DeviceType) => {
|
||||
setDeviceTypeState(type);
|
||||
};
|
||||
|
||||
const isTouch = deviceType === 'tablet' || deviceType === 'phone';
|
||||
const isMobileLayout = deviceType === 'phone';
|
||||
|
||||
return { deviceType, setDeviceType, isTouch, isMobileLayout };
|
||||
}
|
||||
@ -122,7 +122,7 @@ export default function SinglePlayerPage() {
|
||||
const nodeStoragePath = getNodeStoragePath(context.node);
|
||||
if (nodeStoragePath) {
|
||||
logger.debug('single-player-page', '📥 Loading snapshot from database', {
|
||||
dbName: user.user_db_name,
|
||||
dbName: null,
|
||||
node: context.node,
|
||||
node_storage_path: nodeStoragePath,
|
||||
user_type: user.user_type,
|
||||
@ -131,7 +131,7 @@ export default function SinglePlayerPage() {
|
||||
|
||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||
nodeStoragePath,
|
||||
user.user_db_name,
|
||||
null,
|
||||
newStore,
|
||||
setLoadingState,
|
||||
undefined, // sharedStore
|
||||
|
||||
@ -3,7 +3,6 @@ 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;
|
||||
@ -11,8 +10,7 @@ export interface CCUser {
|
||||
user_type: string;
|
||||
username: string;
|
||||
display_name: string;
|
||||
user_db_name: string;
|
||||
school_db_name: string;
|
||||
school_id?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@ -44,28 +42,13 @@ export function convertToCCUser(user: User, metadata: CCUserMetadata): CCUser {
|
||||
// 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,
|
||||
username,
|
||||
display_name: displayName,
|
||||
user_db_name: userDbName,
|
||||
school_db_name: schoolDbName,
|
||||
school_id: null,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
};
|
||||
|
||||
@ -6,7 +6,6 @@ import { RegistrationResponse } from '../../services/auth/authService';
|
||||
import { storageService, StorageKeys } from './localStorageService';
|
||||
import { logger } from '../../debugConfig';
|
||||
import { provisionUser } from '../provisioningService';
|
||||
import { DatabaseNameService } from '../graph/databaseNameService';
|
||||
|
||||
const REGISTRATION_SERVICE = 'registration-service';
|
||||
|
||||
@ -87,14 +86,6 @@ export class RegistrationService {
|
||||
try {
|
||||
const provisioned = await provisionUser(ccUser.id, provisioningToken);
|
||||
if (provisioned) {
|
||||
ccUser.user_db_name = provisioned.user_db_name;
|
||||
if (provisioned.worker_db_name) {
|
||||
ccUser.school_db_name = provisioned.worker_db_name;
|
||||
}
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName: ccUser.user_db_name,
|
||||
schoolDbName: ccUser.school_db_name
|
||||
});
|
||||
logger.info(REGISTRATION_SERVICE, '✅ Provisioning successful', {
|
||||
userId: ccUser.id,
|
||||
userDbName: provisioned.user_db_name,
|
||||
@ -110,11 +101,6 @@ export class RegistrationService {
|
||||
});
|
||||
}
|
||||
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName: ccUser.user_db_name,
|
||||
schoolDbName: ccUser.school_db_name
|
||||
});
|
||||
|
||||
return {
|
||||
user: ccUser,
|
||||
accessToken: authData.session?.access_token || null,
|
||||
|
||||
@ -281,7 +281,11 @@ export class NavigationSnapshotService {
|
||||
throw new Error('No user found');
|
||||
}
|
||||
|
||||
const dbName = user.user_db_name;
|
||||
const dbName = (user as (typeof user & { user_db_name?: string })).user_db_name ?? '';
|
||||
if (!dbName) {
|
||||
logger.debug('snapshot-service', '⚠️ No db name - snapshot save skipped (Phase B will migrate to Supabase Storage)');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('snapshot-service', '💾 Saving snapshot', {
|
||||
nodePath,
|
||||
@ -315,7 +319,11 @@ export class NavigationSnapshotService {
|
||||
throw new Error('No user found');
|
||||
}
|
||||
|
||||
const dbName = user.user_db_name;
|
||||
const dbName = (user as (typeof user & { user_db_name?: string })).user_db_name ?? '';
|
||||
if (!dbName) {
|
||||
logger.debug('snapshot-service', '⚠️ No db name - snapshot load skipped (Phase B will migrate to Supabase Storage)');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('snapshot-service', '📥 Loading snapshot', {
|
||||
nodePath: node.node_storage_path,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user