Bladeren bron

fix(library): send camera stream token for 3D Preview plate thumbnails (#2661)

The File Manager 3D Preview dialog (ModelViewerModal) rendered plate
thumbnails with the raw thumbnail_url. The plate-thumbnail endpoints are
gated behind a camera stream token passed as ?token= (an <img> can't send
an Authorization header), so with auth enabled the browser fetched without
a token and got 401 "Valid camera stream token required" — broken image
icons for every plate. The Slice dialog's picker (PlatePickerModal) and the
Print modal's PlateSelector already append the token via withStreamToken(),
which is why the same file's thumbnails showed there.

Wrap the thumbnail src in withStreamToken(), matching the other two call
sites. The token is synced app-wide and withStreamToken() is a no-op when
auth is off, so non-auth setups are unchanged.
maziggy 1 maand geleden
bovenliggende
commit
6530a6af06

File diff suppressed because it is too large
+ 1 - 0
CHANGELOG.md


+ 28 - 0
frontend/src/__tests__/components/ModelViewerModal.test.tsx

@@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { screen, fireEvent, waitFor } from '@testing-library/react';
 import { render } from '../utils';
 import { ModelViewerModal } from '../../components/ModelViewerModal';
+import { setStreamToken } from '../../api/client';
 import { http, HttpResponse } from 'msw';
 import { server } from '../mocks/server';
 
@@ -385,6 +386,33 @@ describe('ModelViewerModal', () => {
       });
     });
 
+    // #2661: plate-thumbnail endpoints are gated behind a camera stream token
+    // (an <img> can't send a Bearer header), so the src must carry ?token=.
+    // Without it the 3D Preview thumbnails 401 while the Slice dialog (which
+    // already appends the token) shows the same file's thumbnails fine.
+    it('appends the camera stream token to plate thumbnail URLs', async () => {
+      setStreamToken('tok-2661');
+      try {
+        render(
+          <ModelViewerModal
+            archiveId={1}
+            title="Test Model"
+            onClose={mockOnClose}
+          />
+        );
+
+        await waitFor(() => {
+          expect(screen.getByText('Plate 1')).toBeInTheDocument();
+        });
+
+        const thumb = screen.getByAltText('Plate 1') as HTMLImageElement;
+        expect(thumb.src).toContain('/api/v1/archives/1/plates/1/thumbnail');
+        expect(thumb.src).toContain('token=tok-2661');
+      } finally {
+        setStreamToken(null);
+      }
+    });
+
     it('allows plate selection via click', async () => {
       render(
         <ModelViewerModal

+ 2 - 2
frontend/src/components/ModelViewerModal.tsx

@@ -5,7 +5,7 @@ import { X, ExternalLink, Box, Code2, Cog, Loader2, Layers, Check, Maximize2, Mi
 import { ModelViewer } from './ModelViewer';
 import { GcodeViewer } from './GcodeViewer';
 import { Button } from './Button';
-import { api } from '../api/client';
+import { api, withStreamToken } from '../api/client';
 import { openInSlicer, type SlicerType } from '../utils/slicer';
 import type { ArchivePlatesResponse, LibraryFilePlatesResponse, PlateMetadata } from '../types/plates';
 
@@ -477,7 +477,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                           >
                             {plate.has_thumbnail && plate.thumbnail_url ? (
                               <img
-                                src={plate.thumbnail_url}
+                                src={withStreamToken(plate.thumbnail_url)}
                                 alt={`Plate ${plate.index}`}
                                 className={`${splitFullscreen ? 'w-8 h-8' : 'w-10 h-10'} rounded object-cover bg-bambu-dark-tertiary`}
                               />

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DYQO8a-5.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Bx0t8q7D.js"></script>
+    <script type="module" crossorigin src="/assets/index-DYQO8a-5.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-kl51qImb.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff