security: remove TLSync shared secret from frontend bundle
- Remove VITE_TLSYNC_SECRET from syncService.ts; token is now fetched at runtime via fetchTlsyncToken() from the API backend - Add token?: string to SyncConnectionOptions interface - Update multiplayerUser.tsx to fetch TLSync token from API on mount and pass it through createSyncConnectionOptions - Remove VITE_TLSYNC_SECRET from .env.example The API must implement GET /tlsync/token (authenticated via Supabase Bearer token) to complete the fix.
This commit is contained in:
parent
0db53bfd9c
commit
217f393de9
@ -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
|
||||
|
||||
@ -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<Editor | null>(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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<string> {
|
||||
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}`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user