diff --git a/backend/app/api/routes/liveview.py b/backend/app/api/routes/liveview.py index ce6492f..6b9bad2 100644 --- a/backend/app/api/routes/liveview.py +++ b/backend/app/api/routes/liveview.py @@ -1,3 +1,4 @@ +import hmac from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query @@ -26,7 +27,7 @@ async def liveview_canvas( """ if not settings.liveview_key: raise HTTPException(status_code=403, detail="Live view is disabled") - if not key or key != settings.liveview_key: + if not key or not hmac.compare_digest(key, settings.liveview_key): raise HTTPException(status_code=403, detail="Invalid live view key") nodes = (await db.execute(select(Node))).scalars().all() diff --git a/frontend/src/components/LiveView.tsx b/frontend/src/components/LiveView.tsx index ac0afa8..8d428f7 100644 --- a/frontend/src/components/LiveView.tsx +++ b/frontend/src/components/LiveView.tsx @@ -33,7 +33,7 @@ import type { NodeData } from '@/types' const STANDALONE = import.meta.env.VITE_STANDALONE === 'true' const STORAGE_KEY = 'homelable_canvas' -type ViewState = 'loading' | 'disabled' | 'invalid-key' | 'no-key' | 'ready' +type ViewState = 'loading' | 'disabled' | 'invalid-key' | 'no-key' | 'network-error' | 'ready' function LiveViewCanvas() { const { nodes, edges, loadCanvas } = useCanvasStore() @@ -81,7 +81,8 @@ function LiveViewCanvas() { setViewState('ready') }) .catch((err) => { - const detail: string = err.response?.data?.detail ?? '' + if (!err.response) { setViewState('network-error'); return } + const detail: string = err.response.data?.detail ?? '' setViewState(detail === 'Live view is disabled' ? 'disabled' : 'invalid-key') }) }, [loadCanvas]) @@ -104,6 +105,7 @@ function LiveViewCanvas() { disabled: 'Live view is disabled on this instance.', 'invalid-key': 'Invalid or expired live view key.', 'no-key': 'Missing key — use ?key=your-secret in the URL.', + 'network-error': 'Could not reach the server. Check your connection.', } return (
diff --git a/frontend/src/components/__tests__/LiveView.test.tsx b/frontend/src/components/__tests__/LiveView.test.tsx index 0b63af7..8983796 100644 --- a/frontend/src/components/__tests__/LiveView.test.tsx +++ b/frontend/src/components/__tests__/LiveView.test.tsx @@ -88,12 +88,12 @@ describe('LiveView (non-standalone)', () => { }) }) - it('shows invalid-key error for unexpected API errors', async () => { + it('shows network-error for non-response errors (offline, CORS, 500)', async () => { setSearch('?key=anything') vi.mocked(liveviewApi.load).mockRejectedValue(new Error('network')) render() await waitFor(() => { - expect(screen.getByText(/Invalid or expired/)).toBeDefined() + expect(screen.getByText(/Could not reach the server/)).toBeDefined() }) })