nav items declare their permission

This commit is contained in:
2026-08-27 14:51:04 -04:00
parent 0c1fbfcc1e
commit e8f1de811f
12 changed files with 45 additions and 16 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1652,6 +1652,7 @@ class ComputersPlugin(BasePlugin):
'name': 'PCs',
'icon': 'desktop',
'route': '/pcs',
'permission': 'computers.view',
'position': 15,
'section': 'assets',
},

View File

@@ -68,6 +68,7 @@ class KnowledgeBasePlugin(BasePlugin):
'name': 'Knowledge Base',
'icon': 'book-open',
'route': '/knowledgebase',
'permission': 'kb.view',
'position': 35,
'section': 'information',
},

View File

@@ -231,6 +231,7 @@ class MachinesPlugin(BasePlugin):
'name': 'Machines',
'icon': 'cog',
'route': '/machines',
'permission': 'machines.view',
'position': 10,
'section': 'assets',
},

View File

@@ -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',
},
]

View File

@@ -207,6 +207,7 @@ class NetworkPlugin(BasePlugin):
'name': 'Network',
'icon': 'network-wired',
'route': '/network',
'permission': 'network.view',
'position': 18,
'section': 'assets',
},

View File

@@ -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,
},
]

View File

@@ -385,6 +385,7 @@ class PrintersPlugin(BasePlugin):
'name': 'Printers',
'icon': 'printer',
'route': '/printers',
'permission': 'printers.view',
'position': 20,
'section': 'assets',
},

View File

@@ -185,6 +185,7 @@ class USBPlugin(BasePlugin):
'name': 'USB Devices',
'icon': 'usb',
'route': '/usb',
'permission': 'usb.view',
'position': 25,
'section': 'assets',
},

View File

@@ -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',
},
]

View File

@@ -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