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
|
# TLSync (TLDraw Sync) Configuration
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
VITE_TLSYNC_URL=https://app.classroomcopilot.ai/tldraw
|
VITE_TLSYNC_URL=https://app.classroomcopilot.ai/tldraw
|
||||||
VITE_TLSYNC_SECRET=your-tlsync-secret
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# WhisperLive (Transcription) Configuration
|
# 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 { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Tldraw,
|
Tldraw,
|
||||||
@ -15,7 +15,7 @@ import { useNeoInstitute } from '../../contexts/NeoInstituteContext';
|
|||||||
// Tldraw services
|
// Tldraw services
|
||||||
import { multiplayerOptions } from '../../services/tldraw/optionsService';
|
import { multiplayerOptions } from '../../services/tldraw/optionsService';
|
||||||
import { PresentationService } from '../../services/tldraw/presentationService';
|
import { PresentationService } from '../../services/tldraw/presentationService';
|
||||||
import { createSyncConnectionOptions, handleExternalAsset } from '../../services/tldraw/syncService';
|
import { createSyncConnectionOptions, handleExternalAsset, fetchTlsyncToken } from '../../services/tldraw/syncService';
|
||||||
// Tldraw utils
|
// Tldraw utils
|
||||||
import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides';
|
import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides';
|
||||||
import { customAssets } from '../../utils/tldraw/assets';
|
import { customAssets } from '../../utils/tldraw/assets';
|
||||||
@ -48,6 +48,10 @@ export default function TldrawMultiUser() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const editorRef = useRef<Editor | null>(null);
|
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
|
// Get room ID from URL params
|
||||||
const roomId = searchParams.get('room') || 'multiplayer';
|
const roomId = searchParams.get('room') || 'multiplayer';
|
||||||
|
|
||||||
@ -72,13 +76,28 @@ export default function TldrawMultiUser() {
|
|||||||
setUserPreferences: setTldrawPreferences
|
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({
|
const connectionOptions = useMemo(() => createSyncConnectionOptions({
|
||||||
userId: userInfo.id,
|
userId: userInfo.id,
|
||||||
displayName: userInfo.name,
|
displayName: userInfo.name,
|
||||||
color: userInfo.color,
|
color: userInfo.color,
|
||||||
roomId,
|
roomId,
|
||||||
baseUrl: SYNC_WORKER_URL
|
baseUrl: SYNC_WORKER_URL,
|
||||||
}), [userInfo, roomId]);
|
token: tlsyncToken,
|
||||||
|
}), [userInfo, roomId, tlsyncToken]);
|
||||||
|
|
||||||
const store = useSync({
|
const store = useSync({
|
||||||
...connectionOptions,
|
...connectionOptions,
|
||||||
@ -134,7 +153,7 @@ export default function TldrawMultiUser() {
|
|||||||
const uiComponents = useMemo(() => getUiComponents(presentationMode), [presentationMode]);
|
const uiComponents = useMemo(() => getUiComponents(presentationMode), [presentationMode]);
|
||||||
|
|
||||||
// Render conditionally to avoid unnecessary rerenders
|
// Render conditionally to avoid unnecessary rerenders
|
||||||
if (!user) {
|
if (!user || !tlsyncTokenReady) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,37 @@ export interface SyncConnectionOptions {
|
|||||||
color: string;
|
color: string;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
baseUrl: 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) {
|
export function createSyncConnectionOptions(options: SyncConnectionOptions) {
|
||||||
@ -72,8 +103,9 @@ export function createSyncConnectionOptions(options: SyncConnectionOptions) {
|
|||||||
roomId: effectiveRoomId
|
roomId: effectiveRoomId
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = import.meta.env.VITE_TLSYNC_SECRET ?? ''
|
// Token is supplied at runtime via the API (not from build-time env)
|
||||||
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : ''
|
const token = options.token ?? '';
|
||||||
|
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri: `${baseUrl}/connect/${effectiveRoomId}${tokenParam}`,
|
uri: `${baseUrl}/connect/${effectiveRoomId}${tokenParam}`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user