diff --git a/CHANGELOG.md b/CHANGELOG.md index 043575f1..a57c98cf 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