Prechádzať zdrojové kódy

fix(permissions): hide MakerWorld nav entry from users without makerworld:view (#1175)

  Backend routes were already gated on makerworld:view, the permission
  was granted to admin + standard-user role defaults, and the frontend
  Permission type union already included 'makerworld:view' — but the
  sidebar's hand-maintained navPermissions map in Layout.tsx had no
  entry for `makerworld`. So `isHidden('makerworld')` always returned
  false, the entry rendered for every authenticated user regardless
  of group permissions, and the only way the user found out they
  couldn't use it was by clicking and getting 403'd by every API call.

  Fix is two lines:

  - Layout.tsx: add `makerworld: 'makerworld:view'` to navPermissions,
    matching every other sidebar entry's gating shape.
  - App.tsx: wrap the /makerworld route in PermissionRoute for defence
    in depth, so a user who knows the URL can no longer reach the page
    directly. Same pattern already used by settings, groups/new, and
    groups/:id/edit two lines below.

  Two new Layout tests pin the contract: with auth enabled and a user
  lacking makerworld:view, the sidebar <a href="/makerworld"> link is
  absent while other links still render; with the permission granted,
  the link renders.
maziggy 4 mesiacov pred
rodič
commit
32b5c42dfb

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
CHANGELOG.md


+ 1 - 1
frontend/src/App.tsx

@@ -197,7 +197,7 @@ function App() {
                   <Route path="inventory" element={<InventoryPage />} />
                   <Route path="files" element={<FileManagerPage />} />
                   <Route path="files/trash" element={<LibraryTrashPage />} />
-                  <Route path="makerworld" element={<MakerworldPage />} />
+                  <Route path="makerworld" element={<PermissionRoute permission="makerworld:view"><MakerworldPage /></PermissionRoute>} />
                   <Route path="settings" element={<PermissionRoute permission="settings:read"><SettingsPage /></PermissionRoute>} />
                   <Route path="groups/new" element={<PermissionRoute permission="groups:create"><GroupEditPage /></PermissionRoute>} />
                   <Route path="groups/:id/edit" element={<PermissionRoute permission="groups:update"><GroupEditPage /></PermissionRoute>} />

+ 74 - 0
frontend/src/__tests__/components/Layout.test.tsx

@@ -305,4 +305,78 @@ describe('Layout', () => {
       });
     });
   });
+
+  describe('MakerWorld sidebar permission gate (#1175)', () => {
+    // The MakerWorld sidebar entry was visible to every authenticated user
+    // regardless of group permissions because Layout's `navPermissions` map
+    // had no entry for `makerworld`. Backend routes already gated on
+    // `makerworld:view`, so users without the permission saw the entry,
+    // clicked, and got 403'd by every API call inside the page. The fix
+    // adds `makerworld: 'makerworld:view'` to the map so the entry is
+    // hidden when the permission is absent — same shape as every other
+    // sidebar entry.
+    const enableAuthWithUser = (permissions: string[]) => {
+      server.use(
+        http.get('/api/v1/auth/status', () =>
+          HttpResponse.json({ auth_enabled: true, requires_setup: false }),
+        ),
+        http.get('/api/v1/auth/me', () =>
+          HttpResponse.json({
+            id: 1,
+            username: 'tester',
+            role: 'user',
+            is_active: true,
+            is_admin: false,
+            groups: [{ id: 2, name: 'Standard Users' }],
+            permissions,
+            created_at: '2026-01-01T00:00:00Z',
+          }),
+        ),
+      );
+      // AuthProvider needs a token in localStorage to fetch /auth/me; the
+      // value isn't validated by the mocked server.
+      window.localStorage.setItem('auth_token', 'test-token');
+    };
+
+    const findMakerWorldNavLink = () => {
+      // Sidebar nav links use react-router's `to` prop, which renders as a
+      // plain `<a href="/makerworld">`. Match on the href so the test isn't
+      // coupled to whatever locale string is rendered.
+      return document.querySelector('aside a[href="/makerworld"]');
+    };
+
+    it('hides the MakerWorld nav entry when the user lacks makerworld:view', async () => {
+      // Standard user without the MakerWorld permission. Every other
+      // permission they hold (library:read, etc.) is irrelevant here — the
+      // gate is per-entry and the MakerWorld entry must not render.
+      enableAuthWithUser(['library:read', 'archives:read', 'queue:read']);
+
+      render(<Layout />);
+
+      await waitFor(() => {
+        // Wait for the auth resolution + sidebar render. Some other nav
+        // entry (Files / Archives) confirms the sidebar finished mounting.
+        const sidebar = document.querySelector('aside');
+        expect(sidebar).toBeInTheDocument();
+        expect(sidebar?.querySelector('a[href="/files"]')).toBeInTheDocument();
+      });
+
+      expect(findMakerWorldNavLink()).toBeNull();
+    });
+
+    it('shows the MakerWorld nav entry when the user has makerworld:view', async () => {
+      enableAuthWithUser([
+        'library:read',
+        'archives:read',
+        'queue:read',
+        'makerworld:view',
+      ]);
+
+      render(<Layout />);
+
+      await waitFor(() => {
+        expect(findMakerWorldNavLink()).toBeInTheDocument();
+      });
+    });
+  });
 });

+ 1 - 0
frontend/src/components/Layout.tsx

@@ -284,6 +284,7 @@ export function Layout() {
       projects: 'projects:read',
       inventory: 'inventory:read',
       files: 'library:read',
+      makerworld: 'makerworld:view',
       settings: 'settings:read',
       notifications: 'notifications:user_email',
     };

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/index-BeGsSdpN.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-BM5VeuBp.js"></script>
+    <script type="module" crossorigin src="/assets/index-BeGsSdpN.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-7GmlJb0k.css">
   </head>
   <body>

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov