diff --git a/.env.example b/.env.example index 775b7af..8a73503 100644 --- a/.env.example +++ b/.env.example @@ -27,7 +27,6 @@ VITE_SUPABASE_ANON_KEY=your-supabase-anon-key # TLSync (TLDraw Sync) Configuration # ============================================================================= VITE_TLSYNC_URL=https://app.classroomcopilot.ai/tldraw -VITE_TLSYNC_SECRET=your-tlsync-secret # ============================================================================= # WhisperLive (Transcription) Configuration diff --git a/src/pages/tldraw/multiplayerUser.tsx b/src/pages/tldraw/multiplayerUser.tsx index f870ddb..8037afd 100644 --- a/src/pages/tldraw/multiplayerUser.tsx +++ b/src/pages/tldraw/multiplayerUser.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useMemo } from 'react'; +import { useEffect, useRef, useMemo, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { Tldraw, @@ -15,7 +15,7 @@ import { useNeoInstitute } from '../../contexts/NeoInstituteContext'; // Tldraw services import { multiplayerOptions } from '../../services/tldraw/optionsService'; import { PresentationService } from '../../services/tldraw/presentationService'; -import { createSyncConnectionOptions, handleExternalAsset } from '../../services/tldraw/syncService'; +import { createSyncConnectionOptions, handleExternalAsset, fetchTlsyncToken } from '../../services/tldraw/syncService'; // Tldraw utils import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides'; import { customAssets } from '../../utils/tldraw/assets'; @@ -48,6 +48,10 @@ export default function TldrawMultiUser() { const [searchParams] = useSearchParams(); const editorRef = useRef(null); + // TLSync token fetched at runtime from API (never embedded in bundle) + const [tlsyncTokenReady, setTlsyncTokenReady] = useState(false); + const [tlsyncToken, setTlsyncToken] = useState(''); + // Get room ID from URL params const roomId = searchParams.get('room') || 'multiplayer'; @@ -72,13 +76,28 @@ export default function TldrawMultiUser() { setUserPreferences: setTldrawPreferences }); + // Fetch TLSync token from API on mount / user change + useEffect(() => { + let cancelled = false; + async function loadToken() { + const token = await fetchTlsyncToken(); + if (!cancelled) { + setTlsyncToken(token); + setTlsyncTokenReady(true); + } + } + loadToken(); + return () => { cancelled = true; }; + }, [user?.id]); + const connectionOptions = useMemo(() => createSyncConnectionOptions({ userId: userInfo.id, displayName: userInfo.name, color: userInfo.color, roomId, - baseUrl: SYNC_WORKER_URL - }), [userInfo, roomId]); + baseUrl: SYNC_WORKER_URL, + token: tlsyncToken, + }), [userInfo, roomId, tlsyncToken]); const store = useSync({ ...connectionOptions, @@ -134,7 +153,7 @@ export default function TldrawMultiUser() { const uiComponents = useMemo(() => getUiComponents(presentationMode), [presentationMode]); // Render conditionally to avoid unnecessary rerenders - if (!user) { + if (!user || !tlsyncTokenReady) { return null; } diff --git a/src/services/tldraw/syncService.ts b/src/services/tldraw/syncService.ts index 21c31e2..ea89299 100644 --- a/src/services/tldraw/syncService.ts +++ b/src/services/tldraw/syncService.ts @@ -15,6 +15,37 @@ export interface SyncConnectionOptions { color: string; roomId?: string; baseUrl: string; + /** Runtime-fetched TLSync auth token (from API, not baked into bundle). */ + token?: string; +} + +/** + * Fetch a short-lived TLSync auth token from the API backend. + * The API holds the shared secret server-side and issues tokens + * authenticated via the user's Supabase session. + */ +export async function fetchTlsyncToken(apiBase?: string): Promise { + const base = apiBase || import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; + try { + const { supabase } = await import('../../supabaseClient'); + const { data: { session } } = await supabase.auth.getSession(); + if (!session?.access_token) { + logger.warn('sync-service', 'No Supabase session for TLSync token fetch'); + return ''; + } + const res = await fetch(`${base}/tlsync/token`, { + headers: { Authorization: `Bearer ${session.access_token}` }, + }); + if (!res.ok) { + logger.warn('sync-service', `TLSync token endpoint returned ${res.status}`); + return ''; + } + const { token } = await res.json(); + return token || ''; + } catch (err) { + logger.warn('sync-service', 'Failed to fetch TLSync token from API', { err }); + return ''; + } } export function createSyncConnectionOptions(options: SyncConnectionOptions) { @@ -72,8 +103,9 @@ export function createSyncConnectionOptions(options: SyncConnectionOptions) { roomId: effectiveRoomId }); - const token = import.meta.env.VITE_TLSYNC_SECRET ?? '' - const tokenParam = token ? `?token=${encodeURIComponent(token)}` : '' + // Token is supplied at runtime via the API (not from build-time env) + const token = options.token ?? ''; + const tokenParam = token ? `?token=${encodeURIComponent(token)}` : ''; return { uri: `${baseUrl}/connect/${effectiveRoomId}${tokenParam}`,