From f9f1d39f080799182ea473b1fc3f003389414638 Mon Sep 17 00:00:00 2001 From: Proudlock <270015376@geaerospace.com> Date: Fri, 11 Sep 2026 10:45:12 -0400 Subject: [PATCH] Consolidate settings into 8 hubs and fix security vulnerabilities Settings Consolidation (37+ pages to 8 hubs): - Asset Type Hub: Unified type management with plugin descriptors - Model Catalog Hub: Vendors, types, models, operating systems - Asset Configuration Hub: Statuses, relationships, fields, identifiers - Security Hub: Authentication, tokens, users, audit - Site Configuration Hub: Site settings, floor map, branding, imports - Integrations Hub: ServiceNow, Zabbix, Dell warranty - Locations Hub: Locations, business units, employees - Network Infrastructure Hub: VLANs, subnets Plugin Descriptor Pattern: - Add get_asset_type_descriptor hook to BasePlugin - Implement descriptors in 6 plugins (printers, computers, machines, network, measuringtools, locations) - Add /api/pluginui/asset-type-descriptors and core-type-descriptors endpoints - Generic SubtypesTab component renders CRUD UI from descriptor schema Security Fixes: - Fix stored XSS via javascript: URIs in Models and Support Teams - Add URL scheme validation (allow only http/https) in create/update endpoints - Fix plugin contract violations in endpoints plugin (use shopdb.api imports) - Add missing requiresAdmin guard to printer model supplies route - Add user-facing error messages to API token load failures Integration Improvements: - Add persistent tab navigation to ServiceNow, Zabbix, and Dell Warranty settings - Standardize integration settings UI across all three pages - Fix tab visibility when navigating between integrations Tested with 94-agent orchestrated review for ADR compliance and security. --- frontend/src/router/routes/core.js | 265 +++++++++------ frontend/src/views/settings/ApiTokensList.vue | 5 + .../src/views/settings/AssetConfigHub.vue | 111 +++++++ frontend/src/views/settings/AssetTypeHub.vue | 306 ++++++++++++++++++ .../src/views/settings/IntegrationsHub.vue | 112 +++++++ .../src/views/settings/IntegrationsLayout.vue | 90 ++++++ frontend/src/views/settings/LocationsHub.vue | 104 ++++++ .../src/views/settings/ModelCatalogHub.vue | 112 +++++++ frontend/src/views/settings/NetworkHub.vue | 104 ++++++ frontend/src/views/settings/SecurityHub.vue | 118 +++++++ .../src/views/settings/ServiceNowSettings.vue | 63 +++- frontend/src/views/settings/SiteConfigHub.vue | 111 +++++++ .../settings/asset-types/SubtypesTab.vue | 303 +++++++++++++++++ .../settings/asset-types/SuppliesTab.vue | 29 ++ .../settings/asset-types/TypesTabWrapper.vue | 45 +++ frontend/src/views/settings/settingsNav.js | 47 +-- .../settings/types/NotificationTypesTab.vue | 286 ++++++++++++++++ plugins/computers/plugin.py | 13 + plugins/employees/frontend/routes.js | 6 + plugins/endpoints/api/routes.py | 17 +- plugins/machines/plugin.py | 13 + plugins/measuringtools/manifest.json | 2 +- plugins/measuringtools/plugin.py | 13 + plugins/network/frontend/routes.js | 12 + plugins/network/plugin.py | 13 + plugins/printers/frontend/routes.js | 8 +- .../frontend/views/ZabbixSettings.vue | 63 +++- plugins/printers/plugin.py | 13 + plugins/warranty/frontend/routes.js | 6 + .../frontend/views/DellWarrantySettings.vue | 63 +++- shopdb/core/api/models.py | 27 ++ shopdb/core/api/pluginui.py | 101 ++++++ shopdb/core/api/supportteams.py | 28 ++ shopdb/plugins/base.py | 42 ++- 34 files changed, 2485 insertions(+), 166 deletions(-) create mode 100644 frontend/src/views/settings/AssetConfigHub.vue create mode 100644 frontend/src/views/settings/AssetTypeHub.vue create mode 100644 frontend/src/views/settings/IntegrationsHub.vue create mode 100644 frontend/src/views/settings/IntegrationsLayout.vue create mode 100644 frontend/src/views/settings/LocationsHub.vue create mode 100644 frontend/src/views/settings/ModelCatalogHub.vue create mode 100644 frontend/src/views/settings/NetworkHub.vue create mode 100644 frontend/src/views/settings/SecurityHub.vue create mode 100644 frontend/src/views/settings/SiteConfigHub.vue create mode 100644 frontend/src/views/settings/asset-types/SubtypesTab.vue create mode 100644 frontend/src/views/settings/asset-types/SuppliesTab.vue create mode 100644 frontend/src/views/settings/asset-types/TypesTabWrapper.vue create mode 100644 frontend/src/views/settings/types/NotificationTypesTab.vue diff --git a/frontend/src/router/routes/core.js b/frontend/src/router/routes/core.js index 287a5f51..f171c784 100644 --- a/frontend/src/router/routes/core.js +++ b/frontend/src/router/routes/core.js @@ -36,40 +36,158 @@ export default [ meta: { requiresAuth: true, requiresAdmin: true } }, { - path: 'settings/vendors', - name: 'vendors', - component: () => import('../../views/vendors/VendorsList.vue'), + path: 'settings/catalog', + name: 'model-catalog', + component: () => import('../../views/settings/ModelCatalogHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: 'vendors', + name: 'model-catalog-vendors', + component: () => import('../../views/vendors/VendorsList.vue') + }, + { + path: 'types', + name: 'model-catalog-types', + component: () => import('../../views/settings/ModelTypesList.vue') + }, + { + path: 'models', + name: 'model-catalog-models', + component: () => import('../../views/settings/ModelsList.vue') + }, + { + path: 'operatingsystems', + name: 'model-catalog-os', + component: () => import('../../.plugins-staged/computers/views/OperatingSystemsList.vue') + } + ] + }, + { + path: 'settings/assetconfig', + name: 'asset-config', + component: () => import('../../views/settings/AssetConfigHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: 'statuses', + name: 'asset-config-statuses', + component: () => import('../../views/settings/StatusesList.vue') + }, + { + path: 'relationships', + name: 'asset-config-relationships', + component: () => import('../../views/settings/RelationshipTypesList.vue') + }, + { + path: 'fields', + name: 'asset-config-fields', + component: () => import('../../views/settings/CustomFieldsList.vue') + }, + { + path: 'identifiers', + name: 'asset-config-identifiers', + component: () => import('../../views/settings/AssetIdentifiersSettings.vue') + } + ] + }, + { + path: 'settings/security', + name: 'security', + component: () => import('../../views/settings/SecurityHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: 'authentication', + name: 'security-authentication', + component: () => import('../../views/settings/AuthenticationSettings.vue') + }, + { + path: 'tokens', + name: 'security-tokens', + component: () => import('../../views/settings/ApiTokensList.vue') + }, + { + path: 'users', + name: 'security-users', + component: () => import('../../views/settings/UsersList.vue') + }, + { + path: 'audit', + name: 'security-audit', + component: () => import('../../views/settings/AuditSettings.vue') + }, + { + path: 'auditlogs', + name: 'security-auditlogs', + component: () => import('../../views/settings/AuditLogs.vue') + } + ] + }, + { + path: 'settings/siteconfig', + name: 'site-config', + component: () => import('../../views/settings/SiteConfigHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: 'site', + name: 'site-config-site', + component: () => import('../../views/settings/SiteSettings.vue') + }, + { + path: 'floormap', + name: 'site-config-floormap', + component: () => import('../../views/settings/FloorMapSettings.vue') + }, + { + path: 'branding', + name: 'site-config-branding', + component: () => import('../../views/settings/BrandingSettings.vue') + }, + { + path: 'imports', + name: 'site-config-imports', + component: () => import('../../views/settings/ImportSiteAssets.vue') + } + ] + }, + { + path: 'settings/integrations', + name: 'integrations', + component: () => import('../../views/settings/IntegrationsHub.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { - path: 'settings/modeltypes', - name: 'modeltypes', - component: () => import('../../views/settings/ModelTypesList.vue'), + path: 'settings/servicenow', + name: 'servicenow-settings', + component: () => import('../../views/settings/ServiceNowSettings.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { path: 'settings/locations', - name: 'locations', - component: () => import('../../views/settings/LocationsList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } + name: 'locations-hub', + component: () => import('../../views/settings/LocationsHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: 'list', + name: 'locations-hub-locations', + component: () => import('../../views/settings/LocationsList.vue') + }, + { + path: 'businessunits', + name: 'locations-hub-businessunits', + component: () => import('../../views/settings/BusinessUnitsList.vue') + } + ] }, { - path: 'settings/statuses', - name: 'statuses', - component: () => import('../../views/settings/StatusesList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/models', - name: 'models', - component: () => import('../../views/settings/ModelsList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/businessunits', - name: 'businessunits', - component: () => import('../../views/settings/BusinessUnitsList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } + path: 'settings/network', + name: 'network-hub', + component: () => import('../../views/settings/NetworkHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [] }, { path: 'settings/supportteams', @@ -83,18 +201,6 @@ export default [ component: () => import('../../views/settings/DashboardDefaultsList.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, - { - path: 'settings/site', - name: 'site-settings', - component: () => import('../../views/settings/SiteSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/relationshiptypes', - name: 'relationship-types', - component: () => import('../../views/settings/RelationshipTypesList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, { path: 'settings/locationtypes', name: 'location-types', @@ -103,15 +209,22 @@ export default [ }, { path: 'settings/assettypes', - name: 'asset-types', - component: () => import('../../views/settings/AssetTypesList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/customfields', - name: 'custom-fields', - component: () => import('../../views/settings/CustomFieldsList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } + component: () => import('../../views/settings/AssetTypeHub.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: '', + name: 'asset-types-hub', + component: () => import('../../views/settings/asset-types/TypesTabWrapper.vue'), + props: route => ({ assettypeid: route.params.assettypeid || route.params.assettypeid }) + }, + { + path: ':assettypeid/subtypes', + name: 'asset-types-hub-subtypes', + component: () => import('../../views/settings/asset-types/TypesTabWrapper.vue'), + props: route => ({ assettypeid: route.params.assettypeid }) + }, + ] }, // The old monolithic System Settings tab page is split into one routed page // per section (below). This redirect keeps old bookmarks working: bare @@ -138,24 +251,6 @@ export default [ }, meta: { requiresAuth: true, requiresAdmin: true } }, - { - path: 'settings/servicenow', - name: 'servicenow-settings', - component: () => import('../../views/settings/ServiceNowSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/branding', - name: 'branding-settings', - component: () => import('../../views/settings/BrandingSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/importsiteassets', - name: 'import-site-assets', - component: () => import('../../views/settings/ImportSiteAssets.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, { path: 'settings/printing', name: 'printing-settings', @@ -168,60 +263,18 @@ export default [ component: () => import('../../views/settings/EmailSettings.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, - { - path: 'settings/audit', - name: 'audit-settings', - component: () => import('../../views/settings/AuditSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/authentication', - name: 'authentication-settings', - component: () => import('../../views/settings/AuthenticationSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/apitokens', - name: 'apitokens-settings', - component: () => import('../../views/settings/ApiTokensList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/assetidentifiers', - name: 'asset-identifiers-settings', - component: () => import('../../views/settings/AssetIdentifiersSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, { path: 'settings/globalsearch', name: 'global-search-settings', component: () => import('../../views/settings/GlobalSearchSettings.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, - { - path: 'settings/floormap', - name: 'floor-map-settings', - component: () => import('../../views/settings/FloorMapSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, { path: 'settings/plugins', name: 'plugins', component: () => import('../../views/settings/PluginsList.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, - { - path: 'settings/auditlogs', - name: 'audit-logs', - component: () => import('../../views/settings/AuditLogs.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, - { - path: 'settings/users', - name: 'users', - component: () => import('../../views/settings/UsersList.vue'), - meta: { requiresAuth: true, requiresAdmin: true } - }, { path: 'settings/shopdbbackups', name: 'shopdbbackups', diff --git a/frontend/src/views/settings/ApiTokensList.vue b/frontend/src/views/settings/ApiTokensList.vue index 8860b9c7..b39f5eba 100644 --- a/frontend/src/views/settings/ApiTokensList.vue +++ b/frontend/src/views/settings/ApiTokensList.vue @@ -327,6 +327,11 @@ async function loadData() { } } catch (err) { console.error('Error loading API tokens:', err) + if (err.response?.data?.error) { + toast.error(err.response.data.error) + } else { + toast.error('Failed to load tokens or permissions. Please refresh the page.') + } } finally { loading.value = false } diff --git a/frontend/src/views/settings/AssetConfigHub.vue b/frontend/src/views/settings/AssetConfigHub.vue new file mode 100644 index 00000000..2e377183 --- /dev/null +++ b/frontend/src/views/settings/AssetConfigHub.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/frontend/src/views/settings/AssetTypeHub.vue b/frontend/src/views/settings/AssetTypeHub.vue new file mode 100644 index 00000000..8ce5d917 --- /dev/null +++ b/frontend/src/views/settings/AssetTypeHub.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/frontend/src/views/settings/IntegrationsHub.vue b/frontend/src/views/settings/IntegrationsHub.vue new file mode 100644 index 00000000..c851ae57 --- /dev/null +++ b/frontend/src/views/settings/IntegrationsHub.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/frontend/src/views/settings/IntegrationsLayout.vue b/frontend/src/views/settings/IntegrationsLayout.vue new file mode 100644 index 00000000..7e350672 --- /dev/null +++ b/frontend/src/views/settings/IntegrationsLayout.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/views/settings/LocationsHub.vue b/frontend/src/views/settings/LocationsHub.vue new file mode 100644 index 00000000..1720f339 --- /dev/null +++ b/frontend/src/views/settings/LocationsHub.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/frontend/src/views/settings/ModelCatalogHub.vue b/frontend/src/views/settings/ModelCatalogHub.vue new file mode 100644 index 00000000..bc4c20bd --- /dev/null +++ b/frontend/src/views/settings/ModelCatalogHub.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/frontend/src/views/settings/NetworkHub.vue b/frontend/src/views/settings/NetworkHub.vue new file mode 100644 index 00000000..2af0ddd8 --- /dev/null +++ b/frontend/src/views/settings/NetworkHub.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/frontend/src/views/settings/SecurityHub.vue b/frontend/src/views/settings/SecurityHub.vue new file mode 100644 index 00000000..c9c9e618 --- /dev/null +++ b/frontend/src/views/settings/SecurityHub.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/src/views/settings/ServiceNowSettings.vue b/frontend/src/views/settings/ServiceNowSettings.vue index ffcd7a8b..8de20c4e 100644 --- a/frontend/src/views/settings/ServiceNowSettings.vue +++ b/frontend/src/views/settings/ServiceNowSettings.vue @@ -1,7 +1,32 @@