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.
This commit is contained in:
Proudlock
2026-09-11 10:45:12 -04:00
parent 9f78d0bfc7
commit f9f1d39f08
34 changed files with 2485 additions and 166 deletions

View File

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