فهرست منبع

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 ماه پیش
والد
کامیت
6530a6af06

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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`}
                               />

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است