From 4e59fe30b20b249e861cb13fb5c90069662af3e3 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 27 Aug 2026 14:50:52 -0400 Subject: [PATCH] A nav item says which permission its page needs The sidebar hides what the caller cannot reach - that is what makes it useful to a guest, and what stops a member with a sparse role being offered links that can only answer 403. It reads the permission off the nav item to decide. Only four items ever declared one. Machines, PCs, Printers, Network, USB, Knowledge Base, Notifications, Calendar, Dashboard, Map, Applications and Reports declared nothing, so the filter had nothing to go on: granting the guest role machines.view opened the API and left the sidebar empty, which reads as the grant not working. Every item now names its read permission - in the plugins, in the core navigation endpoint, and in the frontend's fallback list, which is what renders when the endpoint is unreachable and would otherwise disagree with it. --- CHANGELOG.md | 9 +++++++++ frontend/src/views/AppLayout.vue | 24 ++++++++++++------------ plugins/computers/plugin.py | 1 + plugins/knowledgebase/plugin.py | 1 + plugins/machines/plugin.py | 1 + plugins/measuringtools/plugin.py | 2 ++ plugins/network/plugin.py | 1 + plugins/notifications/plugin.py | 2 ++ plugins/printers/plugin.py | 1 + plugins/usb/plugin.py | 1 + plugins/warranty/plugin.py | 2 ++ shopdb/core/api/dashboard.py | 16 ++++++++++++---- 12 files changed, 45 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff8d266..f70a7b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,15 @@ ADR-007 and ADR-002. ### Fixed +- **The sidebar offered pages the reader could not open, and hid ones they + could.** A nav item is hidden from anybody who does not hold its permission, + but only four items ever declared one - so the filter did nothing for the + rest, and a signed-out visitor granted `machines.view` still saw an empty + sidebar. Every item now names the read permission its page needs, in the + plugins, in the core navigation endpoint and in the frontend's fallback list. + A signed-in member with a sparse role will notice the same correction: a link + that could only answer 403 no longer appears. + - **Every report offers to email itself, not just two of them.** `POST /api/reports/email` has always taken any `{columns, rows}`, but the button was on the toner and warranty pages only - because those two happen to be their diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue index 8ea01593..036dc27d 100644 --- a/frontend/src/views/AppLayout.vue +++ b/frontend/src/views/AppLayout.vue @@ -187,18 +187,18 @@ const iconMap = { // Default navigation (used as fallback if API fails) const defaultNav = [ - { name: 'Dashboard', icon: 'layout-dashboard', route: '/', position: 0 }, - { name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 }, - { name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 }, - { name: 'Map', icon: 'map', route: '/map', position: 4 }, - { name: 'Machines', icon: 'cog', route: '/machines', position: 10, section: 'assets' }, - { name: 'PCs', icon: 'desktop', route: '/pcs', position: 15, section: 'assets' }, - { name: 'Network', icon: 'network-wired', route: '/network', position: 18, section: 'assets' }, - { name: 'Printers', icon: 'printer', route: '/printers', position: 20, section: 'assets' }, - { name: 'USB Devices', icon: 'usb', route: '/usb', position: 25, section: 'assets' }, - { name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' }, - { name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35, section: 'information' }, - { name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40, section: 'information' }, + { name: 'Dashboard', icon: 'layout-dashboard', route: '/', position: 0, permission: 'assets.view' }, + { name: 'Notifications', icon: 'bell', route: '/notifications', position: 5, permission: 'notifications.view' }, + { name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6, permission: 'notifications.view' }, + { name: 'Map', icon: 'map', route: '/map', position: 4, permission: 'assets.view' }, + { name: 'Machines', icon: 'cog', route: '/machines', position: 10, section: 'assets', permission: 'machines.view' }, + { name: 'PCs', icon: 'desktop', route: '/pcs', position: 15, section: 'assets', permission: 'computers.view' }, + { name: 'Network', icon: 'network-wired', route: '/network', position: 18, section: 'assets', permission: 'network.view' }, + { name: 'Printers', icon: 'printer', route: '/printers', position: 20, section: 'assets', permission: 'printers.view' }, + { name: 'USB Devices', icon: 'usb', route: '/usb', position: 25, section: 'assets', permission: 'usb.view' }, + { name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information', permission: 'applications.view' }, + { name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35, section: 'information', permission: 'kb.view' }, + { name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40, section: 'information', permission: 'reports.view' }, ] // The canonical sidebar sections, in the order they render. A nav item DECLARES diff --git a/plugins/computers/plugin.py b/plugins/computers/plugin.py index babf4b9a..dd15876f 100644 --- a/plugins/computers/plugin.py +++ b/plugins/computers/plugin.py @@ -1652,6 +1652,7 @@ class ComputersPlugin(BasePlugin): 'name': 'PCs', 'icon': 'desktop', 'route': '/pcs', + 'permission': 'computers.view', 'position': 15, 'section': 'assets', }, diff --git a/plugins/knowledgebase/plugin.py b/plugins/knowledgebase/plugin.py index 85fd73f6..02abdc3a 100644 --- a/plugins/knowledgebase/plugin.py +++ b/plugins/knowledgebase/plugin.py @@ -68,6 +68,7 @@ class KnowledgeBasePlugin(BasePlugin): 'name': 'Knowledge Base', 'icon': 'book-open', 'route': '/knowledgebase', + 'permission': 'kb.view', 'position': 35, 'section': 'information', }, diff --git a/plugins/machines/plugin.py b/plugins/machines/plugin.py index 192469d0..e89d582c 100644 --- a/plugins/machines/plugin.py +++ b/plugins/machines/plugin.py @@ -231,6 +231,7 @@ class MachinesPlugin(BasePlugin): 'name': 'Machines', 'icon': 'cog', 'route': '/machines', + 'permission': 'machines.view', 'position': 10, 'section': 'assets', }, diff --git a/plugins/measuringtools/plugin.py b/plugins/measuringtools/plugin.py index b563971f..445a0c80 100644 --- a/plugins/measuringtools/plugin.py +++ b/plugins/measuringtools/plugin.py @@ -73,6 +73,7 @@ class MeasuringToolsPlugin(BasePlugin): 'name': 'Measuring Tools', 'icon': 'ruler', 'route': '/measuringtools', + 'permission': 'measuringtools.view', 'position': 22, 'section': 'assets', }, @@ -86,6 +87,7 @@ class MeasuringToolsPlugin(BasePlugin): 'description': 'Measuring tools bucketed by calibration status', 'category': 'compliance', 'route': '/reports/calibration', + 'permission': 'measuringtools.view', }, ] diff --git a/plugins/network/plugin.py b/plugins/network/plugin.py index c88c291f..084e68e0 100644 --- a/plugins/network/plugin.py +++ b/plugins/network/plugin.py @@ -207,6 +207,7 @@ class NetworkPlugin(BasePlugin): 'name': 'Network', 'icon': 'network-wired', 'route': '/network', + 'permission': 'network.view', 'position': 18, 'section': 'assets', }, diff --git a/plugins/notifications/plugin.py b/plugins/notifications/plugin.py index 54570ea6..17066764 100644 --- a/plugins/notifications/plugin.py +++ b/plugins/notifications/plugin.py @@ -232,12 +232,14 @@ class NotificationsPlugin(BasePlugin): 'name': 'Notifications', 'icon': 'bell', 'route': '/notifications', + 'permission': 'notifications.view', 'position': 5, }, { 'name': 'Calendar', 'icon': 'calendar', 'route': '/calendar', + 'permission': 'notifications.view', 'position': 6, }, ] diff --git a/plugins/printers/plugin.py b/plugins/printers/plugin.py index 97164135..bb6fa548 100644 --- a/plugins/printers/plugin.py +++ b/plugins/printers/plugin.py @@ -385,6 +385,7 @@ class PrintersPlugin(BasePlugin): 'name': 'Printers', 'icon': 'printer', 'route': '/printers', + 'permission': 'printers.view', 'position': 20, 'section': 'assets', }, diff --git a/plugins/usb/plugin.py b/plugins/usb/plugin.py index cdfa4f1b..77754e36 100644 --- a/plugins/usb/plugin.py +++ b/plugins/usb/plugin.py @@ -185,6 +185,7 @@ class USBPlugin(BasePlugin): 'name': 'USB Devices', 'icon': 'usb', 'route': '/usb', + 'permission': 'usb.view', 'position': 25, 'section': 'assets', }, diff --git a/plugins/warranty/plugin.py b/plugins/warranty/plugin.py index 1d09ffcf..f519025c 100644 --- a/plugins/warranty/plugin.py +++ b/plugins/warranty/plugin.py @@ -57,6 +57,7 @@ class WarrantyPlugin(BasePlugin): 'name': 'Warranties', 'icon': 'shield', 'route': '/warranties', + 'permission': 'warranty.view', 'position': 9, }, ] @@ -69,6 +70,7 @@ class WarrantyPlugin(BasePlugin): 'description': 'Assets bucketed by coverage: expired, expiring soon, active', 'category': 'warranty', 'route': '/reports/warranty', + 'permission': 'warranty.view', }, ] diff --git a/shopdb/core/api/dashboard.py b/shopdb/core/api/dashboard.py index 5a3a525d..d8350d15 100644 --- a/shopdb/core/api/dashboard.py +++ b/shopdb/core/api/dashboard.py @@ -155,9 +155,14 @@ def get_navigation(): all_items = [] # Core navigation items (always present) + # Every item names the permission its page needs, so the filter below can + # hide what the caller cannot reach - a link that only leads to a 401 is + # worse than no link. all_items.extend([ - {'name': 'Dashboard', 'icon': 'layout-dashboard', 'route': '/', 'position': 0}, - {'name': 'Map', 'icon': 'map', 'route': '/map', 'position': 4}, + {'name': 'Dashboard', 'icon': 'layout-dashboard', 'route': '/', + 'position': 0, 'permission': 'assets.view'}, + {'name': 'Map', 'icon': 'map', 'route': '/map', 'position': 4, + 'permission': 'assets.view'}, ]) # Collect navigation items from enabled plugins. Disabling persists to the @@ -181,8 +186,11 @@ def get_navigation(): # Add core information section items all_items.extend([ - {'name': 'Applications', 'icon': 'app-window', 'route': '/applications', 'position': 30, 'section': 'information'}, - {'name': 'Reports', 'icon': 'bar-chart-3', 'route': '/reports', 'position': 40, 'section': 'information'}, + {'name': 'Applications', 'icon': 'app-window', 'route': '/applications', + 'position': 30, 'section': 'information', + 'permission': 'applications.view'}, + {'name': 'Reports', 'icon': 'bar-chart-3', 'route': '/reports', + 'position': 40, 'section': 'information', 'permission': 'reports.view'}, ]) # Sort by position