diff --git a/addon/components/fuel-integration/hub.hbs b/addon/components/fuel-integration/hub.hbs index 183657974..65ad63610 100644 --- a/addon/components/fuel-integration/hub.hbs +++ b/addon/components/fuel-integration/hub.hbs @@ -17,7 +17,7 @@
diff --git a/addon/components/layout/fleet-ops-sidebar.js b/addon/components/layout/fleet-ops-sidebar.js index c7394f079..0a0516da4 100644 --- a/addon/components/layout/fleet-ops-sidebar.js +++ b/addon/components/layout/fleet-ops-sidebar.js @@ -151,14 +151,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component { this.createItem('menu.contacts', 'address-book', 'management.contacts', 'fleet-ops list contact', 'fleet-ops see contact'), this.createItem('menu.places', 'location-dot', 'management.places', 'fleet-ops list place', 'fleet-ops see place'), this.createItem('menu.fuel-reports', 'gas-pump', 'management.fuel-reports', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report'), - this.createItem('menu.fuel-transactions', 'credit-card', 'management.fuel-transactions', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report'), + this.createItem('menu.fuel-transactions', 'credit-card', 'management.fuel-transactions', 'fleet-ops list fuel-provider-transaction', 'fleet-ops see fuel-provider-transaction'), this.createItem('menu.issues', 'triangle-exclamation', 'management.issues', 'fleet-ops list issue', 'fleet-ops see issue'), ]); } get maintenanceItems() { return this.withRegistryItems('maintenance', [ - this.createHubItem('Maintenance Hub', 'wrench', 'maintenance.index', 'fleet-ops list maintenance-schedule', 'fleet-ops see maintenance-schedule', [ + this.createHubItem('Maintenance Hub', 'wrench', 'maintenance.index', 'fleet-ops list work-order', 'fleet-ops see work-order', [ 'maintenance hub', 'service readiness', 'maintenance control panel', @@ -182,7 +182,9 @@ export default class LayoutFleetOpsSidebarComponent extends Component { get connectivityItems() { return this.withRegistryItems('connectivity', [ this.createHubItem(this.intl.t('menu.telematics'), 'satellite-dish', 'connectivity.telematics', 'fleet-ops list telematic', 'fleet-ops see telematic', ['connectivity hub']), - this.createItem('menu.fuel-providers', 'gas-pump', 'connectivity.fuel-providers', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report', ['fuel integrations']), + this.createItem('menu.fuel-providers', 'gas-pump', 'connectivity.fuel-providers', 'fleet-ops list fuel-provider-connection', 'fleet-ops see fuel-provider-connection', [ + 'fuel integrations', + ]), this.createItem('menu.devices', 'hard-drive', 'connectivity.devices', 'fleet-ops list device', 'fleet-ops see device'), this.createItem('menu.sensors', 'temperature-full', 'connectivity.sensors', 'fleet-ops list sensor', 'fleet-ops see sensor'), this.createItem('menu.events', 'stream', 'connectivity.events', 'fleet-ops list device-event', 'fleet-ops see device-event'), @@ -191,18 +193,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component { get analyticsItems() { return this.withRegistryItems('analytics', [ - this.createHubItem('Dashboard', 'chart-line', 'analytics.index', 'iam list report', 'fleet-ops see report', ['dashboard', 'fleetops dashboard', 'metrics']), - this.createItem('menu.reports', 'file-import', 'analytics.reports', 'iam list report', 'fleet-ops see report'), + this.createHubItem('Dashboard', 'chart-line', 'analytics.index', 'fleet-ops view analytics', 'fleet-ops see analytics', ['dashboard', 'fleetops dashboard', 'metrics']), + this.createItem('menu.reports', 'file-import', 'analytics.reports', 'iam list report', 'iam see report'), ]); } get settingsItems() { return this.withRegistryItems('settings', [ - this.createHubItem('Settings Hub', 'sliders', 'settings.index', 'fleet-ops view navigator-settings', 'fleet-ops see navigator-settings', [ - 'settings hub', - 'configuration dashboard', - 'setup focus', - ]), + this.createHubItem('Settings Hub', 'sliders', 'settings.index', null, null, ['settings hub', 'configuration dashboard', 'setup focus']), this.createItem('menu.navigator-app', 'location-arrow', 'settings.navigator-app', 'fleet-ops view navigator-settings', 'fleet-ops see navigator-settings'), this.createItem('menu.map', 'map', 'settings.map', 'fleet-ops view map-settings', 'fleet-ops see map-settings'), this.createItem('menu.payments', 'cash-register', 'settings.payments', 'fleet-ops view payments', 'fleet-ops see payments'), @@ -211,8 +209,8 @@ export default class LayoutFleetOpsSidebarComponent extends Component { this.createItem('menu.orchestrator', 'circle-nodes', 'settings.orchestrator', 'fleet-ops view routing-settings', 'fleet-ops see routing-settings'), this.createItem('menu.scheduling', 'calendar-days', 'settings.scheduling', 'fleet-ops view scheduling-settings', 'fleet-ops see scheduling-settings'), this.createItem('menu.telematics-settings', 'database', 'settings.telematics', 'fleet-ops view telematics-settings', 'fleet-ops see telematics-settings'), - this.createItem('menu.custom-fields', 'pen-to-square', 'settings.custom-fields', 'fleet-ops view custom-field', 'fleet-ops see custom-field'), - this.createItem('menu.avatars', 'icons', 'settings.avatars', 'fleet-ops view avatar', 'fleet-ops see avatar'), + this.createItem('menu.custom-fields', 'pen-to-square', 'settings.custom-fields', 'fleet-ops list custom-field', 'fleet-ops see custom-field'), + this.createItem('menu.avatars', 'icons', 'settings.avatars', 'fleet-ops list avatar', 'fleet-ops see avatar'), ]); } @@ -266,18 +264,36 @@ export default class LayoutFleetOpsSidebarComponent extends Component { } createBranch({ id, label, icon, route, defaultRoute, requiresVisibleChildren = false, children, keywords = [] }) { + const visibleChildren = children.filter((item) => item.visible !== false); + return { id, label, icon, route: this.fullRoute(route), - defaultRoute: this.fullRoute(defaultRoute), + defaultRoute: this.resolveDefaultRoute(this.fullRoute(defaultRoute), visibleChildren), requiresVisibleChildren, - children: children.filter((item) => item.visible !== false), + children: visibleChildren, keywords, }; } + /** + * Keep the branch's default route when the user may open it; otherwise fall back to the + * first child route they are permitted to open, so clicking the branch does not land on a + * route guard that bounces them back out. + */ + resolveDefaultRoute(defaultRoute, children = []) { + const permitted = (item) => [item.visiblePermission, item.permission].every((permission) => !permission || this.abilities.can(permission)); + const defaultChild = children.find((item) => item.route === defaultRoute); + + if (!defaultChild || permitted(defaultChild)) { + return defaultRoute; + } + + return children.find((item) => item.route && permitted(item))?.route ?? defaultRoute; + } + createItem(intl, icon, route, permission, ability, keywords = []) { return { priority: this.defaultPriorityForRoute(route), @@ -291,7 +307,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component { }; } - createHubItem(label, icon, route, _permission, _ability, keywords = []) { + createHubItem(label, icon, route, permission, ability, keywords = []) { return { pinnedFirst: true, priority: this.defaultPriorityForRoute(route), @@ -299,6 +315,8 @@ export default class LayoutFleetOpsSidebarComponent extends Component { description: label, icon, route: this.fullRoute(route), + permission, + visiblePermission: ability, isNavigationHub: true, keywords: [label, route, ...keywords].filter(Boolean), }; diff --git a/addon/components/map/leaflet-live-map.js b/addon/components/map/leaflet-live-map.js index ae7727f8a..ded9fcd65 100644 --- a/addon/components/map/leaflet-live-map.js +++ b/addon/components/map/leaflet-live-map.js @@ -298,7 +298,8 @@ export default class MapLeafletLiveMapComponent extends Component { } @task *loadResource(path, options = {}) { - if (this.abilities.cannot(`fleet-ops list ${path}`)) return []; + // Permissions use the singular resource name (e.g. `fleet-ops list vehicle`), not the plural endpoint path. + if (this.abilities.cannot(`fleet-ops list ${singularize(path)}`)) return []; if (path === 'service-areas') { const serviceAreas = yield this.serviceAreaActions.loadAll.perform(); diff --git a/addon/components/telematic/hub.hbs b/addon/components/telematic/hub.hbs index ad094657c..a567a6c97 100644 --- a/addon/components/telematic/hub.hbs +++ b/addon/components/telematic/hub.hbs @@ -15,7 +15,7 @@
diff --git a/addon/controllers/analytics/reports/index.js b/addon/controllers/analytics/reports/index.js index 489324109..56052e63c 100644 --- a/addon/controllers/analytics/reports/index.js +++ b/addon/controllers/analytics/reports/index.js @@ -29,6 +29,7 @@ export default class AnalyticsReportsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.reportActions.transition.create, + permission: 'iam create report', }, ]; } @@ -39,6 +40,7 @@ export default class AnalyticsReportsIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.reportActions.bulkDelete, + permission: 'iam delete report', }, ]; } @@ -81,10 +83,12 @@ export default class AnalyticsReportsIndexController extends Controller { { label: 'View report...', fn: this.reportActions.transition.view, + permission: 'iam view report', }, { label: 'Edit report...', fn: this.reportActions.transition.edit, + permission: 'iam update report', }, { separator: true, @@ -92,6 +96,7 @@ export default class AnalyticsReportsIndexController extends Controller { { label: 'Delete report...', fn: this.reportActions.delete, + permission: 'iam delete report', }, ], sortable: false, diff --git a/addon/controllers/connectivity/devices/index.js b/addon/controllers/connectivity/devices/index.js index dcb7c2536..b971d6681 100644 --- a/addon/controllers/connectivity/devices/index.js +++ b/addon/controllers/connectivity/devices/index.js @@ -65,12 +65,14 @@ export default class ConnectivityDevicesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.deviceActions.transition.create, + permission: 'fleet-ops create device', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.deviceActions.import, + permission: 'fleet-ops import device', }, { text: this.intl.t('common.export'), @@ -78,6 +80,7 @@ export default class ConnectivityDevicesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.deviceActions.export, + permission: 'fleet-ops export device', }, ]; @@ -87,6 +90,7 @@ export default class ConnectivityDevicesIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.deviceActions.bulkDelete, + permission: 'fleet-ops delete device', }, ]; diff --git a/addon/controllers/connectivity/fuel-providers/index.js b/addon/controllers/connectivity/fuel-providers/index.js index f0ef5a0c8..5b4ffe9fb 100644 --- a/addon/controllers/connectivity/fuel-providers/index.js +++ b/addon/controllers/connectivity/fuel-providers/index.js @@ -40,6 +40,7 @@ export default class ConnectivityFuelProvidersIndexController extends Controller text: 'Connect Integration', type: 'primary', onClick: () => this.fuelIntegrationActions.transition.create(), + permission: 'fleet-ops create fuel-provider-connection', }, ]; } @@ -51,6 +52,7 @@ export default class ConnectivityFuelProvidersIndexController extends Controller { label: `Sync ${selected.length} selected`, fn: () => selected.forEach((connection) => this.syncConnection(connection)), + permission: 'fleet-ops sync fuel-provider-connection', }, ]; } @@ -125,11 +127,11 @@ export default class ConnectivityFuelProvidersIndexController extends Controller sticky: 'right', width: 60, actions: [ - { label: 'Open Integration', fn: this.openConnection }, - { label: 'Edit Settings', fn: this.editConnection }, + { label: 'Open Integration', fn: this.openConnection, permission: 'fleet-ops view fuel-provider-connection' }, + { label: 'Edit Settings', fn: this.editConnection, permission: 'fleet-ops update fuel-provider-connection' }, { separator: true }, - { label: 'Test Connection', fn: this.testConnection }, - { label: 'Sync Transactions', fn: this.syncConnection }, + { label: 'Test Connection', fn: this.testConnection, permission: 'fleet-ops update fuel-provider-connection' }, + { label: 'Sync Transactions', fn: this.syncConnection, permission: 'fleet-ops sync fuel-provider-connection' }, ], sortable: false, filterable: false, diff --git a/addon/controllers/connectivity/sensors/index.js b/addon/controllers/connectivity/sensors/index.js index 4ed9390a1..492a537d1 100644 --- a/addon/controllers/connectivity/sensors/index.js +++ b/addon/controllers/connectivity/sensors/index.js @@ -92,12 +92,14 @@ export default class ConnectivitySensorsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.sensorActions.transition.create, + permission: 'fleet-ops create sensor', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.sensorActions.import, + permission: 'fleet-ops import sensor', }, { text: this.intl.t('common.export'), @@ -105,6 +107,7 @@ export default class ConnectivitySensorsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.sensorActions.export, + permission: 'fleet-ops export sensor', }, ]; @@ -114,6 +117,7 @@ export default class ConnectivitySensorsIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.sensorActions.bulkDelete, + permission: 'fleet-ops delete sensor', }, ]; diff --git a/addon/controllers/connectivity/telematics/index.js b/addon/controllers/connectivity/telematics/index.js index 37369823a..c44ba0556 100644 --- a/addon/controllers/connectivity/telematics/index.js +++ b/addon/controllers/connectivity/telematics/index.js @@ -38,12 +38,14 @@ export default class ConnectivityTelematicsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.telematicActions.transition.create, + permission: 'fleet-ops create telematic', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.telematicActions.import, + permission: 'fleet-ops import telematic', }, { text: this.intl.t('common.export'), @@ -51,6 +53,7 @@ export default class ConnectivityTelematicsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.telematicActions.export, + permission: 'fleet-ops export telematic', }, ]; @@ -60,6 +63,7 @@ export default class ConnectivityTelematicsIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.telematicActions.bulkDelete, + permission: 'fleet-ops delete telematic', }, ]; diff --git a/addon/controllers/maintenance/equipment/index.js b/addon/controllers/maintenance/equipment/index.js index 7c020cfb7..d0a95dac0 100644 --- a/addon/controllers/maintenance/equipment/index.js +++ b/addon/controllers/maintenance/equipment/index.js @@ -46,14 +46,21 @@ export default class MaintenanceEquipmentIndexController extends Controller { helpText: 'Change the layout', }, { icon: 'refresh', onClick: this.equipmentActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.equipmentActions.transition.create }, - { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.equipmentActions.import }, - { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.equipmentActions.export }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.equipmentActions.transition.create, permission: 'fleet-ops create equipment' }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.equipmentActions.import, permission: 'fleet-ops import equipment' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.equipmentActions.export, + permission: 'fleet-ops export equipment', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.equipmentActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.equipmentActions.bulkDelete, permission: 'fleet-ops delete equipment' }]; } get columns() { diff --git a/addon/controllers/maintenance/inspection-forms/index.js b/addon/controllers/maintenance/inspection-forms/index.js index 53d812903..f7fb1c0ff 100644 --- a/addon/controllers/maintenance/inspection-forms/index.js +++ b/addon/controllers/maintenance/inspection-forms/index.js @@ -17,12 +17,12 @@ export default class MaintenanceInspectionFormsIndexController extends Controlle get actionButtons() { return [ { icon: 'refresh', onClick: this.inspectionFormActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.inspectionFormActions.transition.create }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.inspectionFormActions.transition.create, permission: 'fleet-ops create inspection-form' }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionFormActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionFormActions.bulkDelete, permission: 'fleet-ops delete inspection-form' }]; } get columns() { diff --git a/addon/controllers/maintenance/inspection-submissions/index.js b/addon/controllers/maintenance/inspection-submissions/index.js index 888898dc5..0cfa6bd70 100644 --- a/addon/controllers/maintenance/inspection-submissions/index.js +++ b/addon/controllers/maintenance/inspection-submissions/index.js @@ -23,12 +23,18 @@ export default class MaintenanceInspectionSubmissionsIndexController extends Con get actionButtons() { return [ { icon: 'refresh', onClick: this.inspectionSubmissionActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.inspectionSubmissionActions.transition.create }, + { + text: this.intl.t('common.new'), + type: 'primary', + icon: 'plus', + onClick: this.inspectionSubmissionActions.transition.create, + permission: 'fleet-ops create inspection-submission', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionSubmissionActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionSubmissionActions.bulkDelete, permission: 'fleet-ops delete inspection-submission' }]; } get columns() { diff --git a/addon/controllers/maintenance/maintenances/index.js b/addon/controllers/maintenance/maintenances/index.js index 88d817048..a0d75c580 100644 --- a/addon/controllers/maintenance/maintenances/index.js +++ b/addon/controllers/maintenance/maintenances/index.js @@ -31,12 +31,14 @@ export default class MaintenanceMaintenancesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.maintenanceActions.transition.create, + permission: 'fleet-ops create maintenance', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.maintenanceActions.import, + permission: 'fleet-ops import maintenance', }, { text: this.intl.t('common.export'), @@ -44,6 +46,7 @@ export default class MaintenanceMaintenancesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.maintenanceActions.export, + permission: 'fleet-ops export maintenance', }, ]; } @@ -55,6 +58,7 @@ export default class MaintenanceMaintenancesIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.maintenanceActions.bulkDelete, + permission: 'fleet-ops delete maintenance', }, ]; } diff --git a/addon/controllers/maintenance/parts/index.js b/addon/controllers/maintenance/parts/index.js index 3a44dfb5e..55bfc41c2 100644 --- a/addon/controllers/maintenance/parts/index.js +++ b/addon/controllers/maintenance/parts/index.js @@ -46,14 +46,21 @@ export default class MaintenancePartsIndexController extends Controller { helpText: 'Change the layout', }, { icon: 'refresh', onClick: this.partActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.partActions.transition.create }, - { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.partActions.import }, - { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.partActions.export }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.partActions.transition.create, permission: 'fleet-ops create part' }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.partActions.import, permission: 'fleet-ops import part' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.partActions.export, + permission: 'fleet-ops export part', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.partActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.partActions.bulkDelete, permission: 'fleet-ops delete part' }]; } get columns() { diff --git a/addon/controllers/maintenance/schedules/index.js b/addon/controllers/maintenance/schedules/index.js index 094af86ef..16f4b098f 100644 --- a/addon/controllers/maintenance/schedules/index.js +++ b/addon/controllers/maintenance/schedules/index.js @@ -151,14 +151,27 @@ export default class MaintenanceSchedulesIndexController extends Controller { helpText: this.intl.t('common.change-layout'), }, { icon: 'refresh', onClick: this.maintenanceScheduleActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.maintenanceScheduleActions.transition.create }, - { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.maintenanceScheduleActions.import }, - { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.maintenanceScheduleActions.export }, + { + text: this.intl.t('common.new'), + type: 'primary', + icon: 'plus', + onClick: this.maintenanceScheduleActions.transition.create, + permission: 'fleet-ops create maintenance-schedule', + }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.maintenanceScheduleActions.import, permission: 'fleet-ops import maintenance-schedule' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.maintenanceScheduleActions.export, + permission: 'fleet-ops export maintenance-schedule', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.maintenanceScheduleActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.maintenanceScheduleActions.bulkDelete, permission: 'fleet-ops delete maintenance-schedule' }]; } get columns() { diff --git a/addon/controllers/maintenance/work-orders/index.js b/addon/controllers/maintenance/work-orders/index.js index fbc789696..082cb8ead 100644 --- a/addon/controllers/maintenance/work-orders/index.js +++ b/addon/controllers/maintenance/work-orders/index.js @@ -21,14 +21,21 @@ export default class MaintenanceWorkOrdersIndexController extends Controller { get actionButtons() { return [ { icon: 'refresh', onClick: this.workOrderActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.workOrderActions.transition.create }, - { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.workOrderActions.import }, - { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.workOrderActions.export }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.workOrderActions.transition.create, permission: 'fleet-ops create work-order' }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.workOrderActions.import, permission: 'fleet-ops import work-order' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.workOrderActions.export, + permission: 'fleet-ops export work-order', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.workOrderActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.workOrderActions.bulkDelete, permission: 'fleet-ops delete work-order' }]; } get columns() { diff --git a/addon/controllers/management/contacts/index.js b/addon/controllers/management/contacts/index.js index 0756dd08e..056a1a696 100644 --- a/addon/controllers/management/contacts/index.js +++ b/addon/controllers/management/contacts/index.js @@ -62,12 +62,14 @@ export default class ManagementContactsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.contactActions.transition.create, + permission: 'fleet-ops create contact', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.contactActions.import, + permission: 'fleet-ops import contact', }, { text: this.intl.t('common.export'), @@ -75,6 +77,7 @@ export default class ManagementContactsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.contactActions.export, + permission: 'fleet-ops export contact', }, ]; } @@ -88,6 +91,7 @@ export default class ManagementContactsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.contactActions.bulkDelete, + permission: 'fleet-ops delete contact', }, ]; } diff --git a/addon/controllers/management/drivers/index.js b/addon/controllers/management/drivers/index.js index 9762122d6..30612981f 100644 --- a/addon/controllers/management/drivers/index.js +++ b/addon/controllers/management/drivers/index.js @@ -103,12 +103,14 @@ export default class ManagementDriversIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.driverActions.transition.create, + permission: 'fleet-ops create driver', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.driverActions.import, + permission: 'fleet-ops import driver', }, { text: this.intl.t('common.export'), @@ -116,6 +118,7 @@ export default class ManagementDriversIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.driverActions.export, + permission: 'fleet-ops export driver', }, ]; } @@ -129,6 +132,7 @@ export default class ManagementDriversIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.driverActions.bulkDelete, + permission: 'fleet-ops delete driver', }, ]; } diff --git a/addon/controllers/management/fleets/index.js b/addon/controllers/management/fleets/index.js index 910dbaea3..80f6d84ae 100644 --- a/addon/controllers/management/fleets/index.js +++ b/addon/controllers/management/fleets/index.js @@ -42,12 +42,14 @@ export default class ManagementFleetsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.fleetActions.transition.create, + permission: 'fleet-ops create fleet', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.fleetActions.import, + permission: 'fleet-ops import fleet', }, { text: this.intl.t('common.export'), @@ -55,6 +57,7 @@ export default class ManagementFleetsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.fleetActions.export, + permission: 'fleet-ops export fleet', }, ]; } @@ -68,6 +71,7 @@ export default class ManagementFleetsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.fleetActions.bulkDelete, + permission: 'fleet-ops delete fleet', }, ]; } diff --git a/addon/controllers/management/fuel-reports/index.js b/addon/controllers/management/fuel-reports/index.js index 1374cfcaf..cdf74dfbe 100644 --- a/addon/controllers/management/fuel-reports/index.js +++ b/addon/controllers/management/fuel-reports/index.js @@ -56,12 +56,14 @@ export default class ManagementFuelReportsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.fuelReportActions.transition.create, + permission: 'fleet-ops create fuel-report', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.fuelReportActions.import, + permission: 'fleet-ops import fuel-report', }, { text: this.intl.t('common.export'), @@ -69,6 +71,7 @@ export default class ManagementFuelReportsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.fuelReportActions.export, + permission: 'fleet-ops export fuel-report', }, ]; } @@ -82,6 +85,7 @@ export default class ManagementFuelReportsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.fuelReportActions.bulkDelete, + permission: 'fleet-ops delete fuel-report', }, ]; } diff --git a/addon/controllers/management/fuel-transactions/index.js b/addon/controllers/management/fuel-transactions/index.js index e6873351a..30e940e3e 100644 --- a/addon/controllers/management/fuel-transactions/index.js +++ b/addon/controllers/management/fuel-transactions/index.js @@ -71,6 +71,7 @@ export default class ManagementFuelTransactionsIndexController extends Controlle icon: 'gas-pump', text: 'Fuel Integrations', onClick: () => this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'), + permission: 'fleet-ops list fuel-provider-connection', }, ]; } @@ -86,6 +87,7 @@ export default class ManagementFuelTransactionsIndexController extends Controlle { label: `Reprocess ${selected.length} selected`, fn: () => this.confirmAction('reprocess', selected), + permission: 'fleet-ops update fuel-provider-transaction', }, ]; } @@ -196,14 +198,14 @@ export default class ManagementFuelTransactionsIndexController extends Controlle sticky: 'right', width: 60, actions: [ - { label: 'Review Details', fn: this.openDetails }, - { label: 'Open Fuel Report', fn: this.openFuelReport, isVisible: (transaction) => Boolean(transaction.fuel_report_id) }, + { label: 'Review Details', fn: this.openDetails, permission: 'fleet-ops view fuel-provider-transaction' }, + { label: 'Open Fuel Report', fn: this.openFuelReport, isVisible: (transaction) => Boolean(transaction.fuel_report_id), permission: 'fleet-ops view fuel-report' }, { separator: true }, - { label: 'Match to Vehicle', fn: this.matchVehicle }, - { label: 'Match to Order', fn: this.matchOrder }, - { label: 'Reprocess / Rematch', fn: (transaction) => this.confirmAction('reprocess', transaction) }, - { label: 'Ignore Transaction', fn: (transaction) => this.confirmAction('ignored', transaction) }, - { label: 'Mark Reviewed', fn: (transaction) => this.confirmAction('reviewed', transaction) }, + { label: 'Match to Vehicle', fn: this.matchVehicle, permission: 'fleet-ops update fuel-provider-transaction' }, + { label: 'Match to Order', fn: this.matchOrder, permission: 'fleet-ops update fuel-provider-transaction' }, + { label: 'Reprocess / Rematch', fn: (transaction) => this.confirmAction('reprocess', transaction), permission: 'fleet-ops update fuel-provider-transaction' }, + { label: 'Ignore Transaction', fn: (transaction) => this.confirmAction('ignored', transaction), permission: 'fleet-ops review fuel-provider-transaction' }, + { label: 'Mark Reviewed', fn: (transaction) => this.confirmAction('reviewed', transaction), permission: 'fleet-ops review fuel-provider-transaction' }, ], sortable: false, filterable: false, diff --git a/addon/controllers/management/issues/index.js b/addon/controllers/management/issues/index.js index 9c3cfaecd..5774b8865 100644 --- a/addon/controllers/management/issues/index.js +++ b/addon/controllers/management/issues/index.js @@ -56,12 +56,14 @@ export default class ManagementIssuesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.issueActions.transition.create, + permission: 'fleet-ops create issue', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.issueActions.import, + permission: 'fleet-ops import issue', }, { text: this.intl.t('common.export'), @@ -69,6 +71,7 @@ export default class ManagementIssuesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.issueActions.export, + permission: 'fleet-ops export issue', }, ]; } @@ -82,6 +85,7 @@ export default class ManagementIssuesIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.issueActions.bulkDelete, + permission: 'fleet-ops delete issue', }, ]; } diff --git a/addon/controllers/management/places/index.js b/addon/controllers/management/places/index.js index f4236ea31..6fdc416b4 100644 --- a/addon/controllers/management/places/index.js +++ b/addon/controllers/management/places/index.js @@ -34,12 +34,14 @@ export default class ManagementPlacesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.placeActions.transition.create, + permission: 'fleet-ops create place', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.placeActions.import, + permission: 'fleet-ops import place', }, { text: this.intl.t('common.export'), @@ -47,6 +49,7 @@ export default class ManagementPlacesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.placeActions.export, + permission: 'fleet-ops export place', }, ]; } @@ -60,6 +63,7 @@ export default class ManagementPlacesIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.placeActions.bulkDelete, + permission: 'fleet-ops delete place', }, ]; } diff --git a/addon/controllers/management/vehicles/index.js b/addon/controllers/management/vehicles/index.js index 1662a134a..1e63fbf4d 100644 --- a/addon/controllers/management/vehicles/index.js +++ b/addon/controllers/management/vehicles/index.js @@ -102,12 +102,14 @@ export default class ManagementVehiclesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.vehicleActions.transition.create, + permission: 'fleet-ops create vehicle', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.vehicleActions.import, + permission: 'fleet-ops import vehicle', }, { text: this.intl.t('common.export'), @@ -115,6 +117,7 @@ export default class ManagementVehiclesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.vehicleActions.export, + permission: 'fleet-ops export vehicle', }, ]; } @@ -128,6 +131,7 @@ export default class ManagementVehiclesIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.vehicleActions.bulkDelete, + permission: 'fleet-ops delete vehicle', }, ]; } diff --git a/addon/controllers/management/vendors/index.js b/addon/controllers/management/vendors/index.js index 03d941bbe..e76a713ee 100644 --- a/addon/controllers/management/vendors/index.js +++ b/addon/controllers/management/vendors/index.js @@ -83,12 +83,14 @@ export default class ManagementVendorsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.vendorActions.transition.create, + permission: 'fleet-ops create vendor', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.vendorActions.import, + permission: 'fleet-ops import vendor', }, { text: this.intl.t('common.export'), @@ -96,6 +98,7 @@ export default class ManagementVendorsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.vendorActions.export, + permission: 'fleet-ops export vendor', }, ]; } @@ -109,6 +112,7 @@ export default class ManagementVendorsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.vendorActions.bulkDelete, + permission: 'fleet-ops delete vendor', }, ]; } diff --git a/addon/controllers/operations/orders/index.js b/addon/controllers/operations/orders/index.js index 5b40c7bc8..ab9563a89 100644 --- a/addon/controllers/operations/orders/index.js +++ b/addon/controllers/operations/orders/index.js @@ -86,6 +86,7 @@ export default class OperationsOrdersIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.orderActions.transition.create, + permission: 'fleet-ops create order', }, { text: this.intl.t('common.export'), @@ -93,12 +94,14 @@ export default class OperationsOrdersIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.orderActions.export, + permission: 'fleet-ops export order', }, { text: this.intl.t('common.import'), icon: 'file-import', wrapperClass: 'hidden md:flex', onClick: () => this.orderActions.importOrders({ onImportComplete: this.orderActions.refresh }), + permission: 'fleet-ops import order', }, ]; } @@ -110,23 +113,27 @@ export default class OperationsOrdersIndexController extends Controller { label: this.intl.t('common.cancel-resource', { resource: this.intl.t('resource.orders') }), icon: 'ban', fn: this.orderActions.bulkCancel, + permission: 'fleet-ops cancel order', }, { label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.orders') }), icon: 'trash', class: 'text-red-500', fn: this.orderActions.bulkDelete, + permission: 'fleet-ops delete order', }, { separator: true }, { label: this.intl.t('common.dispatch-orders'), icon: 'rocket', fn: this.orderActions.bulkDispatch, + permission: 'fleet-ops dispatch order', }, { label: this.intl.t('common.assign-drivers'), icon: 'user-plus', fn: this.orderActions.bulkAssignDriver, + permission: 'fleet-ops assign-driver-for order', }, ]; } diff --git a/addon/controllers/operations/orders/index/details.js b/addon/controllers/operations/orders/index/details.js index 188ec9256..333846cc9 100644 --- a/addon/controllers/operations/orders/index/details.js +++ b/addon/controllers/operations/orders/index/details.js @@ -17,6 +17,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller { @service hostRouter; @service universe; @service sidebar; + @service abilities; @tracked routingControl; @tracked routingCompleted = false; @tracked realtimeOrderPublicId = null; @@ -90,12 +91,14 @@ export default class OperationsOrdersIndexDetailsController extends Controller { { text: 'Edit details', icon: 'pencil', + permission: 'fleet-ops update order', disabled: this.model.status === 'canceled', fn: () => this.orderActions.editOrderDetails(this.model), }, { text: 'Update activity', icon: 'signal', + permission: 'fleet-ops update order', disabled: this.model.status === 'canceled', fn: () => this.orderActions.updateActivity(this.model, { @@ -105,12 +108,14 @@ export default class OperationsOrdersIndexDetailsController extends Controller { { text: this.model.has_driver_assigned ? 'Unassign driver' : 'Assign driver', icon: this.model.has_driver_assigned ? 'user-xmark' : 'edit', + permission: 'fleet-ops assign-driver-for order', disabled: this.model.has_driver_assigned ? !this.model.hasActiveStatus || !this.model.driver_assigned : !this.model.hasActiveStatus, fn: () => (this.model.has_driver_assigned ? this.orderActions.unassignDriver(this.model) : this.orderActions.assignDriver(this.model)), }, { text: 'View order label', icon: 'file-invoice', + permission: 'fleet-ops view order', fn: () => this.orderActions.viewLabel(this.model), }, { @@ -133,6 +138,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller { text: 'Cancel order', icon: 'ban', class: 'text-danger', + permission: 'fleet-ops cancel order', disabled: this.model.status === 'canceled', fn: () => this.orderActions.cancel(this.model), }, @@ -140,6 +146,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller { text: 'Delete order', icon: 'trash', class: 'text-danger', + permission: 'fleet-ops delete order', fn: () => this.orderActions.delete(this.model, { taskOptions: { @@ -151,7 +158,29 @@ export default class OperationsOrdersIndexDetailsController extends Controller { }, ].filter(Boolean), }, - ]; + ].map((actionButton) => ({ ...actionButton, items: this.permittedMenuItems(actionButton.items) })); + } + + /** + * The panel header dropdown renders `items` as-is and ignores `permission`, so items the + * user is not permitted to use are removed here, along with any separators left dangling. + */ + permittedMenuItems(items = []) { + const permitted = items.filter((item) => !item.permission || this.abilities.can(item.permission)); + const result = permitted.reduce((list, item) => { + if (item.separator && (list.length === 0 || list[list.length - 1].separator)) { + return list; + } + + list.push(item); + return list; + }, []); + + if (result.length && result[result.length - 1].separator) { + result.pop(); + } + + return result; } @action handleActivityModalFinish(options) { diff --git a/addon/controllers/operations/service-rates/index.js b/addon/controllers/operations/service-rates/index.js index cdb91974f..52413130a 100644 --- a/addon/controllers/operations/service-rates/index.js +++ b/addon/controllers/operations/service-rates/index.js @@ -29,6 +29,7 @@ export default class OperationsServiceRatesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.serviceRateActions.transition.create, + permission: 'fleet-ops create service-rate', }, { text: this.intl.t('common.export'), @@ -36,6 +37,7 @@ export default class OperationsServiceRatesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.serviceRateActions.export, + permission: 'fleet-ops export service-rate', }, ]; } @@ -47,6 +49,7 @@ export default class OperationsServiceRatesIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.serviceRateActions.bulkDelete, + permission: 'fleet-ops delete service-rate', }, ]; } diff --git a/addon/extension.js b/addon/extension.js index a9aba06fd..0ddec59dd 100644 --- a/addon/extension.js +++ b/addon/extension.js @@ -18,66 +18,77 @@ export default { description: 'Everything across resources, maintenance and staffing that needs a decision today.', icon: 'satellite-dish', route: 'console.fleet-ops.management.index', + permission: 'fleet-ops list driver', }, { title: 'Orders', description: 'Create, dispatch, and track delivery orders in real time.', icon: 'boxes-stacked', route: 'console.fleet-ops.operations.orders', + permission: 'fleet-ops list order', }, { title: 'Places', description: 'Manage saved locations, addresses, and points of interest.', icon: 'location-dot', route: 'console.fleet-ops.management.places', + permission: 'fleet-ops list place', }, { title: 'Drivers', description: 'Manage driver profiles, assignments, and live locations.', icon: 'id-card', route: 'console.fleet-ops.management.drivers', + permission: 'fleet-ops list driver', }, { title: 'Vehicles', description: 'View and manage your vehicle fleet and telematics.', icon: 'truck', route: 'console.fleet-ops.management.vehicles', + permission: 'fleet-ops list vehicle', }, { title: intlService.t('menu.trailers'), description: intlService.t('trailer.navigation-description'), icon: 'trailer', route: 'console.fleet-ops.management.trailers', + permission: 'fleet-ops list trailer', }, { title: 'Fleets', description: 'Organise drivers and vehicles into operational fleets.', icon: 'layer-group', route: 'console.fleet-ops.management.fleets', + permission: 'fleet-ops list fleet', }, { title: 'Service Rates', description: 'Configure pricing rules and service rate cards.', icon: 'tags', route: 'console.fleet-ops.operations.service-rates', + permission: 'fleet-ops list service-rate', }, { title: 'Devices', description: 'Manage connected telematics devices and their sensor data.', icon: 'microchip', route: 'console.fleet-ops.connectivity.devices', + permission: 'fleet-ops list device', }, { title: 'Reports', description: 'Generate and review operational analytics reports.', icon: 'chart-bar', route: 'console.fleet-ops.analytics.reports', + permission: 'iam list report', }, { title: 'Orchestrator', description: 'Intelligently allocate and dispatch orders to available drivers.', icon: 'diagram-project', route: 'console.fleet-ops.operations.orchestrator', + permission: 'fleet-ops list order', }, ], }); diff --git a/addon/routes/analytics/index.js b/addon/routes/analytics/index.js index 3d64374e9..7f2d927d2 100644 --- a/addon/routes/analytics/index.js +++ b/addon/routes/analytics/index.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class AnalyticsIndexRoute extends Route {} +export default class AnalyticsIndexRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view analytics')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/analytics/reports.js b/addon/routes/analytics/reports.js index fdd1396cf..da2b0f72f 100644 --- a/addon/routes/analytics/reports.js +++ b/addon/routes/analytics/reports.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class AnalyticsReportsRoute extends Route {} +export default class AnalyticsReportsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('iam list report')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/analytics/reports/index/details.js b/addon/routes/analytics/reports/index/details.js index aafa2b55a..d2d163626 100644 --- a/addon/routes/analytics/reports/index/details.js +++ b/addon/routes/analytics/reports/index/details.js @@ -17,7 +17,7 @@ export default class AnalyticsReportsIndexDetailsRoute extends Route { } beforeModel() { - if (this.abilities.cannot('fleet-ops view report')) { + if (this.abilities.cannot('iam view report')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops.analytics.reports.index'); } diff --git a/addon/routes/analytics/reports/index/edit.js b/addon/routes/analytics/reports/index/edit.js index 4ca59d92a..99e510235 100644 --- a/addon/routes/analytics/reports/index/edit.js +++ b/addon/routes/analytics/reports/index/edit.js @@ -17,7 +17,7 @@ export default class AnalyticsReportsIndexEditRoute extends Route { } beforeModel() { - if (this.abilities.cannot('fleet-ops update report')) { + if (this.abilities.cannot('iam update report')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops.analytics.reports.index'); } diff --git a/addon/routes/analytics/reports/index/new.js b/addon/routes/analytics/reports/index/new.js index 3bd42eb9d..88d4d9e1f 100644 --- a/addon/routes/analytics/reports/index/new.js +++ b/addon/routes/analytics/reports/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class AnalyticsReportsIndexNewRoute extends Route {} +export default class AnalyticsReportsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('iam create report')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.analytics.reports.index'); + } + } +} diff --git a/addon/routes/connectivity/devices.js b/addon/routes/connectivity/devices.js index f5a216674..e1fe1539e 100644 --- a/addon/routes/connectivity/devices.js +++ b/addon/routes/connectivity/devices.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityDevicesRoute extends Route {} +export default class ConnectivityDevicesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list device')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/devices/index/new.js b/addon/routes/connectivity/devices/index/new.js index 8c69bb213..450685ce7 100644 --- a/addon/routes/connectivity/devices/index/new.js +++ b/addon/routes/connectivity/devices/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityDevicesIndexNewRoute extends Route {} +export default class ConnectivityDevicesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create device')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index'); + } + } +} diff --git a/addon/routes/connectivity/events.js b/addon/routes/connectivity/events.js index 4f6e2b6e1..aaf94573c 100644 --- a/addon/routes/connectivity/events.js +++ b/addon/routes/connectivity/events.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityEventsRoute extends Route {} +export default class ConnectivityEventsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list device-event')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/fuel-providers.js b/addon/routes/connectivity/fuel-providers.js index 0d9c95fc7..95897b8f8 100644 --- a/addon/routes/connectivity/fuel-providers.js +++ b/addon/routes/connectivity/fuel-providers.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityFuelProvidersRoute extends Route {} +export default class ConnectivityFuelProvidersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/fuel-providers/details.js b/addon/routes/connectivity/fuel-providers/details.js index 04be2351a..f974a05c8 100644 --- a/addon/routes/connectivity/fuel-providers/details.js +++ b/addon/routes/connectivity/fuel-providers/details.js @@ -6,6 +6,15 @@ export default class ConnectivityFuelProvidersIndexDetailsRoute extends Route { @service store; @service notifications; @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } setupController(controller, model) { super.setupController(controller, model); diff --git a/addon/routes/connectivity/fuel-providers/details/transactions.js b/addon/routes/connectivity/fuel-providers/details/transactions.js index 6a457461f..0addb38f7 100644 --- a/addon/routes/connectivity/fuel-providers/details/transactions.js +++ b/addon/routes/connectivity/fuel-providers/details/transactions.js @@ -4,6 +4,18 @@ import { action } from '@ember/object'; export default class FuelIntegrationTransactionsRoute extends Route { @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list fuel-provider-transaction')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } + queryParams = Object.fromEntries(['page', 'limit', 'sort', 'query', 'sync_status', 'vehicle', 'transaction_at'].map((key) => [key, { refreshModel: true }])); model(params) { const connection = this.modelFor('connectivity.fuel-providers.details'); diff --git a/addon/routes/connectivity/fuel-providers/edit.js b/addon/routes/connectivity/fuel-providers/edit.js index 8d72929c4..478c81e75 100644 --- a/addon/routes/connectivity/fuel-providers/edit.js +++ b/addon/routes/connectivity/fuel-providers/edit.js @@ -6,6 +6,15 @@ export default class ConnectivityFuelProvidersIndexEditRoute extends Route { @service store; @service notifications; @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } @action error(error) { this.notifications.serverError(error); diff --git a/addon/routes/connectivity/fuel-providers/new.js b/addon/routes/connectivity/fuel-providers/new.js index b3605cce0..f20b9af67 100644 --- a/addon/routes/connectivity/fuel-providers/new.js +++ b/addon/routes/connectivity/fuel-providers/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityFuelProvidersIndexNewRoute extends Route {} +export default class ConnectivityFuelProvidersIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } +} diff --git a/addon/routes/connectivity/sensors.js b/addon/routes/connectivity/sensors.js index f3bc70a81..9bc86da27 100644 --- a/addon/routes/connectivity/sensors.js +++ b/addon/routes/connectivity/sensors.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivitySensorsRoute extends Route {} +export default class ConnectivitySensorsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list sensor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/sensors/index/new.js b/addon/routes/connectivity/sensors/index/new.js index 836d72730..d23d3cafe 100644 --- a/addon/routes/connectivity/sensors/index/new.js +++ b/addon/routes/connectivity/sensors/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivitySensorsIndexNewRoute extends Route {} +export default class ConnectivitySensorsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create sensor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.sensors.index'); + } + } +} diff --git a/addon/routes/connectivity/telematics.js b/addon/routes/connectivity/telematics.js index 68fd97c7c..39e33321e 100644 --- a/addon/routes/connectivity/telematics.js +++ b/addon/routes/connectivity/telematics.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityTelematicsRoute extends Route {} +export default class ConnectivityTelematicsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list telematic')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/telematics/new.js b/addon/routes/connectivity/telematics/new.js index f993f7b19..0c6d68dd9 100644 --- a/addon/routes/connectivity/telematics/new.js +++ b/addon/routes/connectivity/telematics/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityTelematicsNewRoute extends Route {} +export default class ConnectivityTelematicsNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create telematic')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.index'); + } + } +} diff --git a/addon/routes/connectivity/tracking.js b/addon/routes/connectivity/tracking.js index 81d44b954..de24cdf6f 100644 --- a/addon/routes/connectivity/tracking.js +++ b/addon/routes/connectivity/tracking.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityTrackingRoute extends Route {} +export default class ConnectivityTrackingRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list vehicle')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/equipment.js b/addon/routes/maintenance/equipment.js index 3a5bb687a..cec17bfb6 100644 --- a/addon/routes/maintenance/equipment.js +++ b/addon/routes/maintenance/equipment.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceEquipmentRoute extends Route {} +export default class MaintenanceEquipmentRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list equipment')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/equipment/index/details.js b/addon/routes/maintenance/equipment/index/details.js index 43035ca49..7f8efa9b7 100644 --- a/addon/routes/maintenance/equipment/index/details.js +++ b/addon/routes/maintenance/equipment/index/details.js @@ -12,14 +12,14 @@ export default class MaintenanceEquipmentIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.equipment.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops view equipment')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.equipment.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); } } diff --git a/addon/routes/maintenance/equipment/index/edit.js b/addon/routes/maintenance/equipment/index/edit.js index 136ea947c..51e06996b 100644 --- a/addon/routes/maintenance/equipment/index/edit.js +++ b/addon/routes/maintenance/equipment/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenanceEquipmentIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.equipment.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops update equipment')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.equipment.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); } } diff --git a/addon/routes/maintenance/equipment/index/new.js b/addon/routes/maintenance/equipment/index/new.js index 965997967..aee969df0 100644 --- a/addon/routes/maintenance/equipment/index/new.js +++ b/addon/routes/maintenance/equipment/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceEquipmentIndexNewRoute extends Route {} +export default class MaintenanceEquipmentIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create equipment')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + } +} diff --git a/addon/routes/maintenance/index.js b/addon/routes/maintenance/index.js index 04e2df618..767e3e292 100644 --- a/addon/routes/maintenance/index.js +++ b/addon/routes/maintenance/index.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceIndexRoute extends Route {} +export default class MaintenanceIndexRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/inspection-forms.js b/addon/routes/maintenance/inspection-forms.js index 97a8c467a..b427120f6 100644 --- a/addon/routes/maintenance/inspection-forms.js +++ b/addon/routes/maintenance/inspection-forms.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionFormsRoute extends Route {} +export default class MaintenanceInspectionFormsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/inspection-forms/index/details.js b/addon/routes/maintenance/inspection-forms/index/details.js index 202e83766..25f0e46b6 100644 --- a/addon/routes/maintenance/inspection-forms/index/details.js +++ b/addon/routes/maintenance/inspection-forms/index/details.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionFormsIndexDetailsRoute extends Route { @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-form', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionFormsIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-forms.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); } } diff --git a/addon/routes/maintenance/inspection-forms/index/edit.js b/addon/routes/maintenance/inspection-forms/index/edit.js index f8b15d6dd..531d9819c 100644 --- a/addon/routes/maintenance/inspection-forms/index/edit.js +++ b/addon/routes/maintenance/inspection-forms/index/edit.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionFormsIndexEditRoute extends Route { @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-form', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionFormsIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-forms.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); } } diff --git a/addon/routes/maintenance/inspection-forms/index/new.js b/addon/routes/maintenance/inspection-forms/index/new.js index 96a813892..b4867f549 100644 --- a/addon/routes/maintenance/inspection-forms/index/new.js +++ b/addon/routes/maintenance/inspection-forms/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionFormsIndexNewRoute extends Route {} +export default class MaintenanceInspectionFormsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); + } + } +} diff --git a/addon/routes/maintenance/inspection-submissions.js b/addon/routes/maintenance/inspection-submissions.js index 73b235dce..028acfb78 100644 --- a/addon/routes/maintenance/inspection-submissions.js +++ b/addon/routes/maintenance/inspection-submissions.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionSubmissionsRoute extends Route {} +export default class MaintenanceInspectionSubmissionsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/inspection-submissions/index/details.js b/addon/routes/maintenance/inspection-submissions/index/details.js index ebb148013..faa1cddbd 100644 --- a/addon/routes/maintenance/inspection-submissions/index/details.js +++ b/addon/routes/maintenance/inspection-submissions/index/details.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionSubmissionsIndexDetailsRoute extends R @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-submission', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionSubmissionsIndexDetailsRoute extends R @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-submissions.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); } } diff --git a/addon/routes/maintenance/inspection-submissions/index/edit.js b/addon/routes/maintenance/inspection-submissions/index/edit.js index 9e6f0bb2f..424b900eb 100644 --- a/addon/routes/maintenance/inspection-submissions/index/edit.js +++ b/addon/routes/maintenance/inspection-submissions/index/edit.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionSubmissionsIndexEditRoute extends Rout @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-submission', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionSubmissionsIndexEditRoute extends Rout @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-submissions.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); } } diff --git a/addon/routes/maintenance/inspection-submissions/index/new.js b/addon/routes/maintenance/inspection-submissions/index/new.js index f5c57fe09..c27872ab3 100644 --- a/addon/routes/maintenance/inspection-submissions/index/new.js +++ b/addon/routes/maintenance/inspection-submissions/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionSubmissionsIndexNewRoute extends Route {} +export default class MaintenanceInspectionSubmissionsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); + } + } +} diff --git a/addon/routes/maintenance/maintenances.js b/addon/routes/maintenance/maintenances.js index c38f607a0..67e4ade75 100644 --- a/addon/routes/maintenance/maintenances.js +++ b/addon/routes/maintenance/maintenances.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceMaintenancesRoute extends Route {} +export default class MaintenanceMaintenancesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list maintenance')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/maintenances/index/details.js b/addon/routes/maintenance/maintenances/index/details.js index 564b2777a..8bdbd46bc 100644 --- a/addon/routes/maintenance/maintenances/index/details.js +++ b/addon/routes/maintenance/maintenances/index/details.js @@ -12,14 +12,14 @@ export default class MaintenanceMaintenancesIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.maintenances.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops view maintenance')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.maintenances.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); } } diff --git a/addon/routes/maintenance/maintenances/index/edit.js b/addon/routes/maintenance/maintenances/index/edit.js index 5c81bd470..ec801d142 100644 --- a/addon/routes/maintenance/maintenances/index/edit.js +++ b/addon/routes/maintenance/maintenances/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenanceMaintenancesIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.maintenances.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops update maintenance')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.maintenances.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); } } diff --git a/addon/routes/maintenance/maintenances/index/new.js b/addon/routes/maintenance/maintenances/index/new.js index 8bcba94e0..5a5e18f24 100644 --- a/addon/routes/maintenance/maintenances/index/new.js +++ b/addon/routes/maintenance/maintenances/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceMaintenancesIndexNewRoute extends Route {} +export default class MaintenanceMaintenancesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create maintenance')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + } +} diff --git a/addon/routes/maintenance/parts.js b/addon/routes/maintenance/parts.js index 4d1a7132e..5850590f6 100644 --- a/addon/routes/maintenance/parts.js +++ b/addon/routes/maintenance/parts.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenancePartsRoute extends Route {} +export default class MaintenancePartsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list part')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/parts/index/details.js b/addon/routes/maintenance/parts/index/details.js index 9b27d7a72..5a311fb5c 100644 --- a/addon/routes/maintenance/parts/index/details.js +++ b/addon/routes/maintenance/parts/index/details.js @@ -12,14 +12,14 @@ export default class MaintenancePartsIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.parts.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops view part')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.parts.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); } } diff --git a/addon/routes/maintenance/parts/index/edit.js b/addon/routes/maintenance/parts/index/edit.js index 0ee51d4a5..18c9c8bbb 100644 --- a/addon/routes/maintenance/parts/index/edit.js +++ b/addon/routes/maintenance/parts/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenancePartsIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.parts.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops update part')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.parts.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); } } diff --git a/addon/routes/maintenance/parts/index/new.js b/addon/routes/maintenance/parts/index/new.js index 667dd94d8..022d147ee 100644 --- a/addon/routes/maintenance/parts/index/new.js +++ b/addon/routes/maintenance/parts/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenancePartsIndexNewRoute extends Route {} +export default class MaintenancePartsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create part')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + } +} diff --git a/addon/routes/maintenance/schedules.js b/addon/routes/maintenance/schedules.js index bcc5af8f2..4de37c319 100644 --- a/addon/routes/maintenance/schedules.js +++ b/addon/routes/maintenance/schedules.js @@ -1,2 +1,16 @@ import Route from '@ember/routing/route'; -export default class MaintenanceSchedulesRoute extends Route {} +import { inject as service } from '@ember/service'; + +export default class MaintenanceSchedulesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list maintenance-schedule')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/schedules/index/details.js b/addon/routes/maintenance/schedules/index/details.js index a222442e8..45048a427 100644 --- a/addon/routes/maintenance/schedules/index/details.js +++ b/addon/routes/maintenance/schedules/index/details.js @@ -10,13 +10,13 @@ export default class MaintenanceSchedulesIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops view maintenance-schedule')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } model({ public_id }) { diff --git a/addon/routes/maintenance/schedules/index/edit.js b/addon/routes/maintenance/schedules/index/edit.js index 65d160fdb..44023fc6e 100644 --- a/addon/routes/maintenance/schedules/index/edit.js +++ b/addon/routes/maintenance/schedules/index/edit.js @@ -10,13 +10,13 @@ export default class MaintenanceSchedulesIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops update maintenance-schedule')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } model({ public_id }) { diff --git a/addon/routes/maintenance/schedules/index/new.js b/addon/routes/maintenance/schedules/index/new.js index f0ce7286b..8ed97541c 100644 --- a/addon/routes/maintenance/schedules/index/new.js +++ b/addon/routes/maintenance/schedules/index/new.js @@ -1,2 +1,16 @@ import Route from '@ember/routing/route'; -export default class MaintenanceSchedulesIndexNewRoute extends Route {} +import { inject as service } from '@ember/service'; + +export default class MaintenanceSchedulesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create maintenance-schedule')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); + } + } +} diff --git a/addon/routes/maintenance/work-orders.js b/addon/routes/maintenance/work-orders.js index 13587f950..b9e40da18 100644 --- a/addon/routes/maintenance/work-orders.js +++ b/addon/routes/maintenance/work-orders.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceWorkOrdersRoute extends Route {} +export default class MaintenanceWorkOrdersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/work-orders/index/details.js b/addon/routes/maintenance/work-orders/index/details.js index 6cbee3e24..52738d3f2 100644 --- a/addon/routes/maintenance/work-orders/index/details.js +++ b/addon/routes/maintenance/work-orders/index/details.js @@ -12,14 +12,14 @@ export default class MaintenanceWorkOrdersIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.work-orders.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops view work-order')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.work-orders.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); } } diff --git a/addon/routes/maintenance/work-orders/index/edit.js b/addon/routes/maintenance/work-orders/index/edit.js index b5a3c8899..1ccfe4bdd 100644 --- a/addon/routes/maintenance/work-orders/index/edit.js +++ b/addon/routes/maintenance/work-orders/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenanceWorkOrdersIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.work-orders.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops update work-order')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.work-orders.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); } } diff --git a/addon/routes/maintenance/work-orders/index/new.js b/addon/routes/maintenance/work-orders/index/new.js index 7757b0a10..fbe3da463 100644 --- a/addon/routes/maintenance/work-orders/index/new.js +++ b/addon/routes/maintenance/work-orders/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceWorkOrdersIndexNewRoute extends Route {} +export default class MaintenanceWorkOrdersIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + } +} diff --git a/addon/routes/management/fuel-transactions.js b/addon/routes/management/fuel-transactions.js index 4f0de54b6..c463fb850 100644 --- a/addon/routes/management/fuel-transactions.js +++ b/addon/routes/management/fuel-transactions.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementFuelTransactionsRoute extends Route {} +export default class ManagementFuelTransactionsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list fuel-provider-transaction')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/management/fuel-transactions/index/details.js b/addon/routes/management/fuel-transactions/index/details.js index 05d202d42..b251e2e57 100644 --- a/addon/routes/management/fuel-transactions/index/details.js +++ b/addon/routes/management/fuel-transactions/index/details.js @@ -7,6 +7,15 @@ export default class ManagementFuelTransactionsIndexDetailsRoute extends Route { @service store; @service notifications; @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view fuel-provider-transaction')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.fuel-transactions.index'); + } + } @action error(error) { this.notifications.serverError(error); diff --git a/addon/routes/management/index.js b/addon/routes/management/index.js index 0a4dcd62b..63f8af741 100644 --- a/addon/routes/management/index.js +++ b/addon/routes/management/index.js @@ -1,6 +1,19 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class ManagementIndexRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list driver')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } + queryParams = { view: { refreshModel: false }, status: { refreshModel: false }, diff --git a/addon/routes/management/trailers.js b/addon/routes/management/trailers.js new file mode 100644 index 000000000..0b4992833 --- /dev/null +++ b/addon/routes/management/trailers.js @@ -0,0 +1,16 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class ManagementTrailersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list trailer')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/management/vendors/integrated.js b/addon/routes/management/vendors/integrated.js index 0d90aa4ff..b86b8ac6d 100644 --- a/addon/routes/management/vendors/integrated.js +++ b/addon/routes/management/vendors/integrated.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedRoute extends Route {} +export default class ManagementVendorsIntegratedRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/management/vendors/integrated/details.js b/addon/routes/management/vendors/integrated/details.js index 2c1b8c9b6..e4cb44ae5 100644 --- a/addon/routes/management/vendors/integrated/details.js +++ b/addon/routes/management/vendors/integrated/details.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedDetailsRoute extends Route {} +export default class ManagementVendorsIntegratedDetailsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/management/vendors/integrated/edit.js b/addon/routes/management/vendors/integrated/edit.js index 5167d56f4..5131805c8 100644 --- a/addon/routes/management/vendors/integrated/edit.js +++ b/addon/routes/management/vendors/integrated/edit.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedEditRoute extends Route {} +export default class ManagementVendorsIntegratedEditRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/management/vendors/integrated/new.js b/addon/routes/management/vendors/integrated/new.js index 1a9ea38c9..1c57c5cc8 100644 --- a/addon/routes/management/vendors/integrated/new.js +++ b/addon/routes/management/vendors/integrated/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedNewRoute extends Route {} +export default class ManagementVendorsIntegratedNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/operations/orders.js b/addon/routes/operations/orders.js index 4d5b88ed7..f4cc46d1e 100644 --- a/addon/routes/operations/orders.js +++ b/addon/routes/operations/orders.js @@ -1,3 +1,42 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsOrdersRoute extends Route {} +/** + * Orders are the FleetOps landing route (`console.fleet-ops` resolves here), so a user + * without `fleet-ops list order` cannot be sent back to `console.fleet-ops` - that would + * loop. Instead they are forwarded to the first FleetOps area they are permitted to list, + * falling back to the console when there is none. + */ +const FALLBACK_ROUTES = [ + ['fleet-ops list driver', 'console.fleet-ops.management.drivers'], + ['fleet-ops list vehicle', 'console.fleet-ops.management.vehicles'], + ['fleet-ops list trailer', 'console.fleet-ops.management.trailers'], + ['fleet-ops list fleet', 'console.fleet-ops.management.fleets'], + ['fleet-ops list place', 'console.fleet-ops.management.places'], + ['fleet-ops list contact', 'console.fleet-ops.management.contacts'], + ['fleet-ops list vendor', 'console.fleet-ops.management.vendors'], + ['fleet-ops list fuel-report', 'console.fleet-ops.management.fuel-reports'], + ['fleet-ops list issue', 'console.fleet-ops.management.issues'], + ['fleet-ops list work-order', 'console.fleet-ops.maintenance.work-orders'], + ['fleet-ops list device', 'console.fleet-ops.connectivity.devices'], + ['iam list report', 'console.fleet-ops.analytics.reports'], +]; + +export default class OperationsOrdersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list order')) { + const fallback = FALLBACK_ROUTES.find(([permission]) => this.abilities.can(permission)); + if (fallback) { + return this.hostRouter.transitionTo(fallback[1]); + } + + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console'); + } + } +} diff --git a/addon/routes/operations/routes.js b/addon/routes/operations/routes.js index de8587326..bb6a52e38 100644 --- a/addon/routes/operations/routes.js +++ b/addon/routes/operations/routes.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsRoutesRoute extends Route {} +export default class OperationsRoutesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list route')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/operations/routes/index/details.js b/addon/routes/operations/routes/index/details.js index 9ede890da..bf504a0d2 100644 --- a/addon/routes/operations/routes/index/details.js +++ b/addon/routes/operations/routes/index/details.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsRoutesIndexDetailsRoute extends Route {} +export default class OperationsRoutesIndexDetailsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view route')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.operations.routes.index'); + } + } +} diff --git a/addon/routes/operations/routes/index/new.js b/addon/routes/operations/routes/index/new.js index 3974d9c2c..b2ca429af 100644 --- a/addon/routes/operations/routes/index/new.js +++ b/addon/routes/operations/routes/index/new.js @@ -1,6 +1,19 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class OperationsRoutesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create route')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.operations.routes.index'); + } + } + queryParams = { selectedOrders: { refreshModel: false, diff --git a/addon/routes/operations/service-rates.js b/addon/routes/operations/service-rates.js index 83628deb6..e46ea3081 100644 --- a/addon/routes/operations/service-rates.js +++ b/addon/routes/operations/service-rates.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsServiceRatesRoute extends Route {} +export default class OperationsServiceRatesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list service-rate')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/operations/service-rates/index/new.js b/addon/routes/operations/service-rates/index/new.js index 81ceee92f..0659118ce 100644 --- a/addon/routes/operations/service-rates/index/new.js +++ b/addon/routes/operations/service-rates/index/new.js @@ -3,6 +3,17 @@ import { inject as service } from '@ember/service'; export default class OperationsServiceRatesIndexNewRoute extends Route { @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create service-rate')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.operations.service-rates.index'); + } + } async setupController(controller) { super.setupController(...arguments); diff --git a/addon/routes/settings/avatars.js b/addon/routes/settings/avatars.js index 22582c4e8..a6cef9606 100644 --- a/addon/routes/settings/avatars.js +++ b/addon/routes/settings/avatars.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsAvatarsRoute extends Route {} +export default class SettingsAvatarsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list avatar')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/custom-fields.js b/addon/routes/settings/custom-fields.js index 9563a887e..a2bb3fcbc 100644 --- a/addon/routes/settings/custom-fields.js +++ b/addon/routes/settings/custom-fields.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsCustomFieldsRoute extends Route {} +export default class SettingsCustomFieldsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list custom-field')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/map.js b/addon/routes/settings/map.js index 51f072976..6b1c6caaa 100644 --- a/addon/routes/settings/map.js +++ b/addon/routes/settings/map.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsMapRoute extends Route {} +export default class SettingsMapRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view map-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/notifications.js b/addon/routes/settings/notifications.js index 9258c98f6..665c8a638 100644 --- a/addon/routes/settings/notifications.js +++ b/addon/routes/settings/notifications.js @@ -4,6 +4,17 @@ import { hash } from 'rsvp'; export default class SettingsNotificationsRoute extends Route { @service fetch; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view notification-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } model() { return hash({ diff --git a/addon/routes/settings/orchestrator.js b/addon/routes/settings/orchestrator.js index f5109bd46..b6a474dd9 100644 --- a/addon/routes/settings/orchestrator.js +++ b/addon/routes/settings/orchestrator.js @@ -14,7 +14,7 @@ export default class SettingsOrchestratorRoute extends Route { @service hostRouter; beforeModel() { - if (this.abilities.cannot('fleet-ops list order')) { + if (this.abilities.cannot('fleet-ops view routing-settings')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops'); } diff --git a/addon/routes/settings/payments/index.js b/addon/routes/settings/payments/index.js index bab0e134c..6925e7aa4 100644 --- a/addon/routes/settings/payments/index.js +++ b/addon/routes/settings/payments/index.js @@ -16,7 +16,7 @@ export default class SettingsPaymentsIndexRoute extends Route { }; beforeModel() { - if (this.abilities.cannot('fleet-ops list purchase-rate')) { + if (this.abilities.cannot('fleet-ops view payments')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops'); } diff --git a/addon/routes/settings/routing.js b/addon/routes/settings/routing.js index cd75821fb..bc5a75e59 100644 --- a/addon/routes/settings/routing.js +++ b/addon/routes/settings/routing.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsRoutingRoute extends Route {} +export default class SettingsRoutingRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view routing-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/scheduling.js b/addon/routes/settings/scheduling.js index c6411b241..c64c6b68b 100644 --- a/addon/routes/settings/scheduling.js +++ b/addon/routes/settings/scheduling.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsSchedulingRoute extends Route {} +export default class SettingsSchedulingRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view scheduling-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/virtual.js b/addon/routes/virtual.js index b79b4e462..bbd17220e 100644 --- a/addon/routes/virtual.js +++ b/addon/routes/virtual.js @@ -4,6 +4,10 @@ import { inject as service } from '@ember/service'; export default class VirtualRoute extends Route { @service universe; @service('universe/menu-service') menuService; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; queryParams = { view: { @@ -15,4 +19,11 @@ export default class VirtualRoute extends Route { const view = this.universe.getViewFromTransition(transition); return this.menuService.lookupMenuItem('engine:fleet-ops', slug, view, section); } + + afterModel(menuItem) { + if (menuItem?.permission && this.abilities.cannot(menuItem.permission)) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } } diff --git a/server/src/Auth/Schemas/FleetOps.php b/server/src/Auth/Schemas/FleetOps.php index ac2cdda86..95cd81d73 100644 --- a/server/src/Auth/Schemas/FleetOps.php +++ b/server/src/Auth/Schemas/FleetOps.php @@ -43,7 +43,7 @@ class FleetOps ], [ 'name' => 'service-rate', - 'actions' => ['import'], + 'actions' => ['import', 'export'], ], [ 'name' => 'zone', @@ -141,6 +141,51 @@ class FleetOps 'name' => 'part', 'actions' => ['export', 'import'], ], + [ + 'name' => 'maintenance-schedule', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'warranty', + 'actions' => [], + ], + [ + 'name' => 'device', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'sensor', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'device-event', + 'actions' => ['export'], + ], + [ + 'name' => 'telematic', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'fuel-provider-connection', + 'actions' => ['sync'], + ], + [ + 'name' => 'fuel-provider-transaction', + 'actions' => ['review'], + ], + [ + 'name' => 'fuel-provider-sync-run', + 'actions' => [], + ], + [ + 'name' => 'purchase-rate', + 'actions' => [], + ], + [ + 'name' => 'analytics', // dashboards, KPI metrics and the live fleet summary + 'actions' => [], + 'remove_actions' => ['create', 'update', 'delete', 'export'], + ], [ 'name' => 'custom-field', 'actions' => [], @@ -151,29 +196,39 @@ class FleetOps ], [ 'name' => 'navigator-settings', // the navigator mobile app used by drivers - 'action' => [], + 'actions' => [], 'remove_actions' => ['delete', 'export', 'list', 'create'], ], [ 'name' => 'payments', - 'action' => ['onboard'], + 'actions' => ['onboard'], 'remove_actions' => ['delete', 'export', 'list', 'create'], ], [ 'name' => 'notification-settings', - 'action' => [], + 'actions' => [], 'remove_actions' => ['export', 'create'], ], [ 'name' => 'routing-settings', - 'action' => [], + 'actions' => [], 'remove_actions' => ['export', 'create'], ], [ 'name' => 'map-settings', - 'action' => [], + 'actions' => [], 'remove_actions' => ['export', 'create'], ], + [ + 'name' => 'scheduling-settings', + 'actions' => [], + 'remove_actions' => ['delete', 'export', 'create'], + ], + [ + 'name' => 'tracking-settings', + 'actions' => [], + 'remove_actions' => ['delete', 'export', 'create'], + ], [ 'name' => 'telematics-settings', // data retention and storage for telematics ingestion 'action' => [], @@ -192,6 +247,9 @@ class FleetOps 'see extension', '* order', '* route', + 'see analytics', + 'list analytics', + 'view analytics', 'see order-config', 'list order-config', 'view order-config', @@ -235,6 +293,12 @@ class FleetOps '* driver', '* vehicle', '* trailer', + '* device', + '* sensor', + '* telematic', + 'see device-event', + 'list device-event', + 'view device-event', ], ], [ @@ -282,6 +346,7 @@ class FleetOps 'permissions' => [ 'see extension', '* service-rate', + '* purchase-rate', 'see order', 'list order', 'see service-area', @@ -314,6 +379,8 @@ class FleetOps '* inspection-submission', '* equipment', '* part', + '* maintenance-schedule', + '* warranty', '* trailer', 'see vehicle', 'list vehicle', @@ -341,6 +408,18 @@ class FleetOps '* inspection-submission', '* equipment', '* part', + '* maintenance-schedule', + '* warranty', + '* device', + '* sensor', + '* device-event', + '* telematic', + '* fuel-provider-connection', + '* fuel-provider-transaction', + '* fuel-provider-sync-run', + 'see analytics', + 'list analytics', + 'view analytics', '* trailer', ], ], diff --git a/server/src/Http/Controllers/FleetOpsController.php b/server/src/Http/Controllers/FleetOpsController.php index fadafc391..dbad90ef7 100644 --- a/server/src/Http/Controllers/FleetOpsController.php +++ b/server/src/Http/Controllers/FleetOpsController.php @@ -2,12 +2,33 @@ namespace Fleetbase\FleetOps\Http\Controllers; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\FleetbaseController; +use Illuminate\Database\Eloquent\Model; class FleetOpsController extends FleetbaseController { + use AuthorizesMethods; + /** * The package namespace used to resolve from. */ public string $namespace = '\\Fleetbase\\FleetOps'; + + /** + * Explicit permissions for custom actions, keyed by controller method, e.g. + * `['bulkDispatch' => 'dispatch order']`. Those methods also carry + * `#[SkipAuthorizationCheck]` so AuthorizationGuard does not demand the + * permission it would otherwise infer from the HTTP verb. + * + * @var array + */ + protected array $methodPermissions = []; + + public function __construct(?Model $model = null, ?string $resource = null) + { + parent::__construct($model, $resource); + + $this->authorizeMethods($this->methodPermissions); + } } diff --git a/server/src/Http/Controllers/Internal/v1/AnalyticsController.php b/server/src/Http/Controllers/Internal/v1/AnalyticsController.php index 654be49ab..e88f7f6cd 100644 --- a/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +++ b/server/src/Http/Controllers/Internal/v1/AnalyticsController.php @@ -14,6 +14,7 @@ use Fleetbase\FleetOps\Support\Analytics\OrdersByStatus; use Fleetbase\FleetOps\Support\Analytics\RevenueTrend; use Fleetbase\FleetOps\Support\Analytics\TopDrivers; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Controllers\Controller; use Illuminate\Http\Request; @@ -28,6 +29,16 @@ */ class AnalyticsController extends Controller { + public function __construct() + { + // Not a resource controller, so AuthorizationGuard cannot resolve a permission for it. + $this->middleware(function ($request, $next) { + Authorization::authorize('view analytics'); + + return $next($request); + }); + } + public function operationsPulse(Request $request) { return $this->run($request, OperationsPulse::class); diff --git a/server/src/Http/Controllers/Internal/v1/ContactController.php b/server/src/Http/Controllers/Internal/v1/ContactController.php index 402fd4eb9..f4b3346f2 100644 --- a/server/src/Http/Controllers/Internal/v1/ContactController.php +++ b/server/src/Http/Controllers/Internal/v1/ContactController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exports\ContactExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Http\Resources\v1\Vendor as VendorResource; @@ -26,6 +27,15 @@ class ContactController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'convertToVendor' => 'create vendor', + ]; + /** * The resource to query. * @@ -108,6 +118,7 @@ public function getAsCustomer($id) ]); } + #[SkipAuthorizationCheck] public function convertToVendor(Request $request, string $id) { $contact = $this->contactForVendorConversion($id); diff --git a/server/src/Http/Controllers/Internal/v1/CustomerController.php b/server/src/Http/Controllers/Internal/v1/CustomerController.php index e17f4c4fd..b20ed00b4 100644 --- a/server/src/Http/Controllers/Internal/v1/CustomerController.php +++ b/server/src/Http/Controllers/Internal/v1/CustomerController.php @@ -5,6 +5,7 @@ use Fleetbase\FleetOps\Mail\CustomerCredentialsMail; use Fleetbase\FleetOps\Models\Contact; use Fleetbase\FleetOps\Support\ProfileAccountManager; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\User; use Illuminate\Http\Request; @@ -13,6 +14,19 @@ class CustomerController extends Controller { + use AuthorizesMethods; + + public function __construct() + { + $this->authorizeMethods([ + 'createPortalLogin' => 'reset-credentials-for customer', + 'sendCredentials' => 'reset-credentials-for customer', + 'deactivatePortalLogin' => 'reset-credentials-for customer', + 'reactivatePortalLogin' => 'reset-credentials-for customer', + 'resetCredentials' => 'reset-credentials-for customer', + ]); + } + public function createPortalLogin(Request $request) { $customer = $this->resolveCustomer($request); diff --git a/server/src/Http/Controllers/Internal/v1/DeviceController.php b/server/src/Http/Controllers/Internal/v1/DeviceController.php index 09efcd960..ce969fbc2 100644 --- a/server/src/Http/Controllers/Internal/v1/DeviceController.php +++ b/server/src/Http/Controllers/Internal/v1/DeviceController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException; use Fleetbase\FleetOps\Exports\DeviceExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; @@ -17,6 +18,16 @@ class DeviceController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'attach' => 'update device', + 'detach' => 'update device', + ]; + /** * The resource to query. * @@ -110,6 +121,7 @@ public static function onFindRecord($query, $request): void /** * Attach a device to a supported FleetOps resource. */ + #[SkipAuthorizationCheck] public function attach(Request $request, string $id): JsonResponse { $request->validate([ @@ -152,6 +164,7 @@ public function attach(Request $request, string $id): JsonResponse /** * Detach a device from its current FleetOps resource. */ + #[SkipAuthorizationCheck] public function detach(string $id): JsonResponse { $device = $this->resolveDevice($id); diff --git a/server/src/Http/Controllers/Internal/v1/DeviceEventController.php b/server/src/Http/Controllers/Internal/v1/DeviceEventController.php index 80b8d8467..8735cc042 100644 --- a/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +++ b/server/src/Http/Controllers/Internal/v1/DeviceEventController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\DeviceEvent; use Fleetbase\FleetOps\Support\Utils; @@ -9,6 +10,15 @@ class DeviceEventController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'markProcessed' => 'update device-event', + ]; + /** * The resource to query. * @@ -51,6 +61,7 @@ public static function onQueryRecord($query, $request): void } } + #[SkipAuthorizationCheck] public function markProcessed(string $id): JsonResponse { $deviceEvent = DeviceEvent::where('company_uuid', session('company')) diff --git a/server/src/Http/Controllers/Internal/v1/DriverController.php b/server/src/Http/Controllers/Internal/v1/DriverController.php index 26203e962..87d591cee 100644 --- a/server/src/Http/Controllers/Internal/v1/DriverController.php +++ b/server/src/Http/Controllers/Internal/v1/DriverController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\Exceptions\FleetbaseRequestValidationException; use Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException; use Fleetbase\FleetOps\Exports\DriverExport; @@ -33,6 +34,20 @@ class DriverController extends FleetOpsController { use Traits\DriverSchedulingTrait; + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'sendCredentials' => 'update-user-for driver', + 'resetCredentials' => 'update-user-for driver', + 'deactivateLogin' => 'update-user-for driver', + 'reactivateLogin' => 'update-user-for driver', + 'unassignOrders' => 'assign-order-for driver', + 'unassignOrder' => 'assign-order-for driver', + 'unassignVehicle' => 'assign-vehicle-for driver', + ]; /** * The resource to query. * @@ -200,6 +215,7 @@ function ($request, &$driver) { * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function sendCredentials(string $id) { [$driver, $user, $error] = $this->resolveDriverLogin($id); @@ -221,6 +237,7 @@ public function sendCredentials(string $id) * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function resetCredentials(Request $request, string $id) { $password = $request->input('password'); @@ -255,6 +272,7 @@ public function resetCredentials(Request $request, string $id) * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function deactivateLogin(string $id) { [$driver, $user, $error] = $this->resolveDriverLogin($id); @@ -275,6 +293,7 @@ public function deactivateLogin(string $id) * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function reactivateLogin(string $id) { [$driver, $user, $error] = $this->resolveDriverLogin($id); @@ -430,6 +449,7 @@ public function assignedOrders(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignOrders(Request $request, string $id): JsonResponse { $request->validate([ @@ -462,6 +482,7 @@ public function unassignOrders(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignOrder(string $id): JsonResponse { $driver = $this->findDriver($id); @@ -498,6 +519,7 @@ public function assignVehicle(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignVehicle(string $id): JsonResponse { $driver = $this->findDriver($id); diff --git a/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php b/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php index af7684887..df99e25e3 100644 --- a/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +++ b/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Jobs\SyncFuelProviderTransactionsJob; use Fleetbase\FleetOps\Models\FuelProviderConnection; @@ -13,6 +14,16 @@ class FuelProviderConnectionController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'testCredentials' => 'create fuel-provider-connection', + 'testConnection' => 'update fuel-provider-connection', + ]; + public $resource = 'fuel_provider_connection'; public function __construct(protected FuelProviderService $fuelProviderService) @@ -38,6 +49,7 @@ public function onBeforeUpdate(Request $request, FuelProviderConnection $connect $this->normalizeConnectionInput($input, $connection); } + #[SkipAuthorizationCheck] public function testCredentials(Request $request, string $provider): JsonResponse { $request->validate([ @@ -54,6 +66,7 @@ public function testCredentials(Request $request, string $provider): JsonRespons return response()->json($result, data_get($result, 'success') ? 200 : 422); } + #[SkipAuthorizationCheck] public function testConnection(Request $request, string $id): JsonResponse { $connection = $this->findConnection($id); diff --git a/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php b/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php index 926bd00f5..b219793ff 100644 --- a/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +++ b/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\FuelProviderTransaction; use Fleetbase\FleetOps\Support\FuelProviders\FuelProviderService; @@ -10,6 +11,17 @@ class FuelProviderTransactionController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'matchVehicle' => 'update fuel-provider-transaction', + 'matchOrder' => 'update fuel-provider-transaction', + 'reprocess' => 'update fuel-provider-transaction', + ]; + public $resource = 'fuel_provider_transaction'; public function __construct(protected FuelProviderService $fuelProviderService) @@ -22,6 +34,7 @@ public static function onQueryRecord($query, $request): void $query->with(['vehicle', 'driver', 'fuelReport']); } + #[SkipAuthorizationCheck] public function matchVehicle(Request $request, string $id): JsonResponse { $request->validate(['vehicle' => 'required|string']); @@ -33,6 +46,7 @@ public function matchVehicle(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function matchOrder(Request $request, string $id): JsonResponse { $request->validate(['order' => 'required|string']); @@ -44,6 +58,7 @@ public function matchOrder(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function reprocess(Request $request, string $id): JsonResponse { $transaction = $this->findTransaction($id); diff --git a/server/src/Http/Controllers/Internal/v1/InspectionFormController.php b/server/src/Http/Controllers/Internal/v1/InspectionFormController.php index b77373a82..f7766cbbd 100644 --- a/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +++ b/server/src/Http/Controllers/Internal/v1/InspectionFormController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Http\Resources\v1\InspectionLink as InspectionLinkResource; use Fleetbase\FleetOps\Models\Driver; @@ -16,6 +17,17 @@ class InspectionFormController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'generateLink' => 'publish inspection-form', + 'revokeLink' => 'publish inspection-form', + 'sendPin' => 'publish inspection-form', + ]; + /** * The resource to query. * @@ -107,6 +119,7 @@ public function archive(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function generateLink(Request $request, string $id): JsonResponse { $form = $this->resolveForm($id) @@ -215,6 +228,7 @@ public function links(Request $request, string $id): JsonResponse } /** Take a link out of use, leaving the record of it in the list. */ + #[SkipAuthorizationCheck] public function revokeLink(Request $request, string $id, string $linkId): JsonResponse { $form = $this->resolveForm($id)->firstOrFail(); @@ -243,6 +257,7 @@ public function revokeLink(Request $request, string $id, string $linkId): JsonRe * A failed delivery answers 200 with `pin_delivery.sent` false and why, so * the console shows it as a warning rather than an error. */ + #[SkipAuthorizationCheck] public function sendPin(Request $request, string $id, string $linkId): JsonResponse { $form = $this->resolveForm($id)->firstOrFail(); diff --git a/server/src/Http/Controllers/Internal/v1/LiveController.php b/server/src/Http/Controllers/Internal/v1/LiveController.php index 1d8cdf7ac..690960baf 100644 --- a/server/src/Http/Controllers/Internal/v1/LiveController.php +++ b/server/src/Http/Controllers/Internal/v1/LiveController.php @@ -18,6 +18,7 @@ use Fleetbase\FleetOps\Support\LiveCacheService; use Fleetbase\FleetOps\Support\LiveOrderQuery; use Fleetbase\FleetOps\Support\Utils; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Illuminate\Http\Request; @@ -26,6 +27,21 @@ */ class LiveController extends Controller { + use AuthorizesMethods; + + public function __construct() + { + $this->authorizeMethods([ + 'coordinates' => 'list order', + 'routes' => 'list order', + 'orders' => 'list order', + 'drivers' => 'list driver', + 'vehicles' => 'list vehicle', + 'operationsMonitor' => 'list order', + 'places' => 'list place', + ]); + } + protected const DEFAULT_VIEWPORT_LIMIT = 500; protected const MAX_VIEWPORT_LIMIT = 1000; protected const VIEWPORT_BOUNDS_PRECISION = 4; diff --git a/server/src/Http/Controllers/Internal/v1/MaintenanceController.php b/server/src/Http/Controllers/Internal/v1/MaintenanceController.php index e3153d45c..4205c2fe9 100644 --- a/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +++ b/server/src/Http/Controllers/Internal/v1/MaintenanceController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exports\MaintenanceExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Imports\MaintenanceImport; @@ -15,6 +16,17 @@ class MaintenanceController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'addLineItem' => 'update maintenance', + 'updateLineItem' => 'update maintenance', + 'removeLineItem' => 'update maintenance', + ]; + /** * The resource to query. * @@ -67,6 +79,7 @@ public function onFindRecord($builder, $request): void * Add a cost line item to a maintenance record. * POST /maintenances/{id}/line-items. */ + #[SkipAuthorizationCheck] public function addLineItem(string $id, Request $request): JsonResponse { $maintenance = $this->findMaintenanceForLineItem($id); @@ -89,6 +102,7 @@ public function addLineItem(string $id, Request $request): JsonResponse * Update a cost line item on a maintenance record. * PUT /maintenances/{id}/line-items/{index}. */ + #[SkipAuthorizationCheck] public function updateLineItem(string $id, int $index, Request $request): JsonResponse { $maintenance = $this->findMaintenanceForLineItem($id); @@ -121,6 +135,7 @@ public function updateLineItem(string $id, int $index, Request $request): JsonRe * Remove a cost line item from a maintenance record. * DELETE /maintenances/{id}/line-items/{index}. */ + #[SkipAuthorizationCheck] public function removeLineItem(string $id, int $index): JsonResponse { $maintenance = $this->findMaintenanceForLineItem($id); diff --git a/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php b/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php index 15cfa292b..083ef8aad 100644 --- a/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +++ b/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exports\MaintenanceScheduleExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Imports\MaintenanceScheduleImport; @@ -22,6 +23,17 @@ class MaintenanceScheduleController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'pause' => 'update maintenance-schedule', + 'resume' => 'update maintenance-schedule', + 'trigger' => 'update maintenance-schedule', + ]; + /** * The resource to query. * @@ -71,6 +83,7 @@ public function import(ImportRequest $request) * Pause a maintenance schedule. * POST /maintenance-schedules/{id}/pause. */ + #[SkipAuthorizationCheck] public function pause(string $id): JsonResponse { $schedule = $this->findSchedule($id); @@ -88,6 +101,7 @@ public function pause(string $id): JsonResponse * Resume a paused maintenance schedule. * POST /maintenance-schedules/{id}/resume. */ + #[SkipAuthorizationCheck] public function resume(string $id): JsonResponse { $schedule = $this->findSchedule($id); @@ -105,6 +119,7 @@ public function resume(string $id): JsonResponse * Manually trigger a work order from a schedule. * POST /maintenance-schedules/{id}/trigger. */ + #[SkipAuthorizationCheck] public function trigger(string $id, Request $request): JsonResponse { $schedule = $this->findSchedule($id); diff --git a/server/src/Http/Controllers/Internal/v1/ManifestController.php b/server/src/Http/Controllers/Internal/v1/ManifestController.php index 80f003605..42329337d 100644 --- a/server/src/Http/Controllers/Internal/v1/ManifestController.php +++ b/server/src/Http/Controllers/Internal/v1/ManifestController.php @@ -4,6 +4,7 @@ use Fleetbase\FleetOps\Models\Manifest; use Fleetbase\FleetOps\Models\ManifestStop; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -23,6 +24,20 @@ */ class ManifestController extends Controller { + use AuthorizesMethods; + + public function __construct() + { + $this->authorizeMethods([ + 'index' => 'list order', + 'show' => 'view order', + 'cancel' => 'cancel order', + 'destroy' => 'delete order', + 'showStop' => 'view order', + 'updateStop' => 'update order', + ]); + } + /** * List manifests for the current company. * Supports filtering by status, driver_id, vehicle_id, and scheduled_date. diff --git a/server/src/Http/Controllers/Internal/v1/MetricsController.php b/server/src/Http/Controllers/Internal/v1/MetricsController.php index f61a5e637..4d3b374fd 100644 --- a/server/src/Http/Controllers/Internal/v1/MetricsController.php +++ b/server/src/Http/Controllers/Internal/v1/MetricsController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Metrics; use Fleetbase\FleetOps\Support\Metrics\Registry; use Fleetbase\Http\Controllers\Controller; @@ -10,6 +11,16 @@ class MetricsController extends Controller { + public function __construct() + { + // Not a resource controller, so AuthorizationGuard cannot resolve a permission for it. + $this->middleware(function ($request, $next) { + Authorization::authorize('view analytics'); + + return $next($request); + }); + } + /** * Legacy bulk endpoint. Returns a flat map of slug → scalar value for the * requested period. Preserved for backward compat; the dashboard widgets diff --git a/server/src/Http/Controllers/Internal/v1/NavigatorController.php b/server/src/Http/Controllers/Internal/v1/NavigatorController.php index 868f7b56a..2da2fdc5e 100644 --- a/server/src/Http/Controllers/Internal/v1/NavigatorController.php +++ b/server/src/Http/Controllers/Internal/v1/NavigatorController.php @@ -3,6 +3,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; use Fleetbase\FleetOps\Support\Utils; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Resources\Organization; use Fleetbase\Models\ApiCredential; @@ -15,6 +16,19 @@ class NavigatorController extends Controller { + use AuthorizesMethods; + /** + * How long a Navigator link issued from the console stays valid. + */ + public const LINK_TTL_MINUTES = 30; + + public function __construct() + { + $this->authorizeMethods([ + 'getLinkAppUrl' => 'admin', + ]); + } + /** * Redirects to the Fleetbase Navigator app using a deep link. * Automatically detects the platform (iOS or Android) and uses the correct URI scheme. @@ -23,6 +37,12 @@ class NavigatorController extends Controller */ public function linkApp(Request $request) { + // This route is opened from a phone without a console session, so it is + // authorised by the short-lived signature issued by getLinkAppUrl(). + if (!$this->hasValidLinkSignature($request)) { + return response()->error('This Navigator link is invalid or has expired. Generate a new one from the console.', 403); + } + $adminUser = $this->findAdminUser(); if (!$adminUser || !$adminUser->company) { @@ -67,8 +87,13 @@ public function linkApp(Request $request) */ public function getLinkAppUrl() { + $expires = now()->addMinutes(static::LINK_TTL_MINUTES)->getTimestamp(); + return response()->json([ - 'linkUrl' => url('int/v1/fleet-ops/navigator/link-app'), + 'linkUrl' => url('int/v1/fleet-ops/navigator/link-app') . '?' . http_build_query([ + 'expires' => $expires, + 'signature' => static::linkSignature($expires), + ]), ]); } @@ -189,4 +214,20 @@ protected function driverOnboardSettings(): mixed { return Setting::where('key', 'fleet-ops.driver-onboard')->value('value'); } + + /** + * HMAC signature for a Navigator link expiring at the given unix timestamp. + */ + public static function linkSignature(int $expires): string + { + return hash_hmac('sha256', 'fleet-ops.navigator.link-app|' . $expires, (string) config('app.key')); + } + + protected function hasValidLinkSignature(Request $request): bool + { + $expires = (int) $request->query('expires'); + $signature = (string) $request->query('signature'); + + return $signature !== '' && $expires >= time() && hash_equals(static::linkSignature($expires), $signature); + } } diff --git a/server/src/Http/Controllers/Internal/v1/OrchestrationController.php b/server/src/Http/Controllers/Internal/v1/OrchestrationController.php index a4e011d47..965960a58 100644 --- a/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +++ b/server/src/Http/Controllers/Internal/v1/OrchestrationController.php @@ -16,6 +16,7 @@ use Fleetbase\FleetOps\Orchestration\Engines\DriverAssignmentEngine; use Fleetbase\FleetOps\Orchestration\Engines\RouteSequencingEngine; use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; use Illuminate\Http\JsonResponse; @@ -39,8 +40,19 @@ */ class OrchestrationController extends Controller { + use AuthorizesMethods; + public function __construct(protected OrchestrationEngineRegistry $registry) { + $this->authorizeMethods([ + 'orders' => 'list order', + 'run' => 'optimize order', + 'preview' => 'list order', + 'commit' => 'assign-driver-for order', + 'engines' => 'list order', + 'orderConfigFields' => 'list order', + 'importOrders' => 'import order', + ]); } /** diff --git a/server/src/Http/Controllers/Internal/v1/OrderController.php b/server/src/Http/Controllers/Internal/v1/OrderController.php index 2751c2114..32b762c42 100644 --- a/server/src/Http/Controllers/Internal/v1/OrderController.php +++ b/server/src/Http/Controllers/Internal/v1/OrderController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\Exceptions\FleetbaseRequestValidationException; use Fleetbase\FleetOps\Events\EntityActivityChanged; use Fleetbase\FleetOps\Events\EntityCompleted; @@ -53,6 +54,20 @@ class OrderController extends FleetOpsController { use ResolvesOrderServiceStops; + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'editOrderRoute' => 'update-route-for order', + 'importFromFiles' => 'import order', + 'bulkCancel' => 'cancel order', + 'bulkDispatch' => 'dispatch order', + 'bulkAssignDriver' => 'assign-driver-for order', + 'dispatchOrder' => 'dispatch order', + 'scheduleOrder' => 'schedule order', + ]; /** * The resource to query. @@ -267,6 +282,7 @@ protected function normalizeCustomerType(array &$input): void * * @return Response */ + #[SkipAuthorizationCheck] public function editOrderRoute(string $id, Request $request) { $pickup = $request->input('pickup'); @@ -337,6 +353,7 @@ public function editOrderRoute(string $id, Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function importFromFiles(Request $request) { $info = Utils::lookupIp(); @@ -409,6 +426,7 @@ public function importFromFiles(Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function bulkCancel(Request $request) { $request->validate([ @@ -454,6 +472,7 @@ public function bulkCancel(Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function bulkDispatch(BulkDispatchRequest $request) { /** @var Order */ @@ -502,6 +521,7 @@ public function bulkDispatch(BulkDispatchRequest $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function bulkAssignDriver(Request $request) { // Validate Inputs @@ -578,6 +598,7 @@ public function cancel(CancelOrderRequest $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function dispatchOrder(Request $request) { /** @@ -1906,6 +1927,7 @@ function ($query) use ($trackingNumber) { * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function scheduleOrder(Request $request) { $orderId = $request->input('order'); diff --git a/server/src/Http/Controllers/Internal/v1/PaymentController.php b/server/src/Http/Controllers/Internal/v1/PaymentController.php index 703bbb283..03ac407f3 100644 --- a/server/src/Http/Controllers/Internal/v1/PaymentController.php +++ b/server/src/Http/Controllers/Internal/v1/PaymentController.php @@ -4,6 +4,7 @@ use Fleetbase\FleetOps\Models\PurchaseRate; use Fleetbase\FleetOps\Support\Payment; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Resources\FleetbaseResource; use Fleetbase\Support\Auth; @@ -12,6 +13,18 @@ class PaymentController extends Controller { + use AuthorizesMethods; + + public function __construct() + { + $this->authorizeMethods([ + 'hasStripeConnectAccount' => 'view payments', + 'getStripeAccount' => 'onboard payments', + 'getStripeAccountSession' => 'onboard payments', + 'getCompanyReceivedPayments' => 'view payments', + ]); + } + /** * Checks if the currently authenticated company has an associated Stripe Connect account. * diff --git a/server/src/Http/Controllers/Internal/v1/RadarController.php b/server/src/Http/Controllers/Internal/v1/RadarController.php index a43e2d401..9cd1e7961 100644 --- a/server/src/Http/Controllers/Internal/v1/RadarController.php +++ b/server/src/Http/Controllers/Internal/v1/RadarController.php @@ -19,6 +19,7 @@ use Fleetbase\FleetOps\Support\Radar\RadarBriefing; use Fleetbase\FleetOps\Support\Radar\RadarItemState; use Fleetbase\FleetOps\Support\Radar\RadarRules; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Alert; use Fleetbase\Models\CompanyUser; @@ -42,6 +43,24 @@ */ class RadarController extends Controller { + use AuthorizesMethods; + + public function __construct() + { + $this->authorizeMethods([ + 'extendShift' => ['update driver', 'update vehicle'], + 'acknowledge' => ['update driver', 'update vehicle'], + 'snooze' => ['update driver', 'update vehicle'], + 'wake' => ['update driver', 'update vehicle'], + 'assign' => ['update driver', 'update vehicle'], + 'plan' => ['update driver', 'update vehicle'], + 'resolve' => ['update driver', 'update vehicle'], + 'bulk' => ['update driver', 'update vehicle'], + 'storeNotice' => ['update driver', 'update vehicle'], + 'destroyNotice' => ['update driver', 'update vehicle'], + ]); + } + public const RESOLVED_WINDOW_DAYS = 7; public const SNOOZE_MAX_DAYS = 90; diff --git a/server/src/Http/Controllers/Internal/v1/SettingController.php b/server/src/Http/Controllers/Internal/v1/SettingController.php index 50d8050c5..f1b6cf078 100644 --- a/server/src/Http/Controllers/Internal/v1/SettingController.php +++ b/server/src/Http/Controllers/Internal/v1/SettingController.php @@ -5,6 +5,7 @@ use Fleetbase\FleetOps\Jobs\PruneTelematicsDataJob; use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy; use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; use Fleetbase\Support\Auth; @@ -17,6 +18,29 @@ */ class SettingController extends Controller { + use AuthorizesMethods; + + public function __construct() + { + $this->authorizeMethods([ + 'saveEntityEditingSettings' => 'update navigator-settings', + 'savedDriverOnboardSettings' => 'update navigator-settings', + 'saveCustomerEnabledOrderConfigs' => 'update order-config', + 'saveCustomerPortalPaymentConfig' => 'update payments', + 'saveNotificationSettings' => 'update notification-settings', + 'saveRoutingSettings' => 'update routing-settings', + 'saveTrackingSettings' => 'update tracking-settings', + 'getAdminTrackingSettings' => 'admin', + 'saveAdminTrackingSettings' => 'admin', + 'saveMapSettings' => 'update map-settings', + 'getAdminMapSettings' => 'admin', + 'saveAdminMapSettings' => 'admin', + 'saveSchedulingSettings' => 'update scheduling-settings', + 'saveOrchestratorSettings' => 'update routing-settings', + 'saveOrchestratorCardFields' => 'update routing-settings', + ]); + } + /** * Save entity editing settings. * @@ -24,37 +48,54 @@ class SettingController extends Controller */ public function saveEntityEditingSettings(Request $request) { - $entityEditingSettings = $request->input('entityEditingSettings', []); + // The setting is one platform-wide map keyed by order config id. Only this + // company's order configs may be written, and other companies' entries are kept. + $ownKeys = $this->companyOrderConfigKeys(); + $incoming = array_intersect_key((array) $request->input('entityEditingSettings', []), array_flip($ownKeys)); + $existing = (array) ($this->settingValue('fleet-ops.entity-editing-settings') ?? []); + $merged = array_merge(array_diff_key($existing, array_flip($ownKeys)), $incoming); - // Save entity editing settings - $this->configureSetting('fleet-ops.entity-editing-settings', $entityEditingSettings); + $this->configureSetting('fleet-ops.entity-editing-settings', $merged); - return response()->json(['entityEditingSettings' => $entityEditingSettings]); + return response()->json(['entityEditingSettings' => $incoming]); } /** - * Retrieve entity editing settings. + * Retrieve entity editing settings for this company's order configs. * * @return \Illuminate\Http\JsonResponse */ public function getEntityEditingSettings() { - $entityEditingSettings = $this->settingValue('fleet-ops.entity-editing-settings'); - if (!$entityEditingSettings) { - $entityEditingSettings = []; - } + $entityEditingSettings = (array) ($this->settingValue('fleet-ops.entity-editing-settings') ?? []); + $entityEditingSettings = array_intersect_key($entityEditingSettings, array_flip($this->companyOrderConfigKeys())); return response()->json(['entityEditingSettings' => $entityEditingSettings]); } /** - * Retrieve driver onboard settings. + * Ids (uuid and public id) of the session company's order configs. + */ + protected function companyOrderConfigKeys(): array + { + return \Fleetbase\FleetOps\Models\OrderConfig::where('company_uuid', session('company')) + ->get(['uuid', 'public_id']) + ->flatMap(fn ($config) => array_filter([$config->uuid, $config->public_id])) + ->values() + ->all(); + } + + /** + * Retrieve driver onboard settings for the session company. + * + * The route still carries a company id for backwards compatibility, but only + * the session company's settings are ever returned. * * @return \Illuminate\Http\JsonResponse */ - public function getDriverOnboardSettings($companyId) + public function getDriverOnboardSettings($companyId = null) { - $driverOnboardSettings = $this->settingValue('fleet-ops.driver-onboard-settings.' . $companyId); + $driverOnboardSettings = $this->settingValue('fleet-ops.driver-onboard-settings.' . session('company')); if (!$driverOnboardSettings) { $driverOnboardSettings = []; } @@ -69,9 +110,10 @@ public function getDriverOnboardSettings($companyId) */ public function savedDriverOnboardSettings(Request $request) { - $driverOnboardSettings = $request->array('driverOnboardSettings', []); + $driverOnboardSettings = $request->array('driverOnboardSettings', []); + $driverOnboardSettings['companyId'] = session('company'); - if ($driverOnboardSettings['enableDriverOnboardFromApp'] == false) { + if (empty($driverOnboardSettings['enableDriverOnboardFromApp'])) { $driverOnboardSettings['driverMustProvideOnboardDoucments'] = false; $driverOnboardSettings['requiredOnboardDocuments'] = []; $driverOnboardSettings['driverOnboardAppMethod'] = ''; diff --git a/server/src/Http/Controllers/Internal/v1/TelematicController.php b/server/src/Http/Controllers/Internal/v1/TelematicController.php index acb8dc5fa..4f056da5b 100644 --- a/server/src/Http/Controllers/Internal/v1/TelematicController.php +++ b/server/src/Http/Controllers/Internal/v1/TelematicController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exports\TelematicExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\Telematic; @@ -16,6 +17,20 @@ class TelematicController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'testConnection' => 'update telematic', + 'testCredentials' => 'create telematic', + 'discover' => 'update telematic', + 'linkDevice' => 'update telematic', + 'telemetryWebhook' => 'update telematic', + 'replayTelemetryDelivery' => 'update telematic', + ]; + /** * The resource to query. * @@ -71,6 +86,7 @@ public function providers(): JsonResponse /** * Test connection to provider. */ + #[SkipAuthorizationCheck] public function testConnection(Request $request, string $id): JsonResponse { $telematic = $this->findTelematic($id); @@ -89,6 +105,7 @@ public function testConnection(Request $request, string $id): JsonResponse /** * Test connection to provider. */ + #[SkipAuthorizationCheck] public function testCredentials(Request $request, string $key): JsonResponse { $credentials = $request->array('credentials', []); @@ -120,6 +137,7 @@ public function testCredentials(Request $request, string $key): JsonResponse /** * Discover devices from provider. */ + #[SkipAuthorizationCheck] public function discover(Request $request, string $id): JsonResponse { $telematic = $this->findTelematic($id); @@ -181,6 +199,7 @@ public function logs(Request $request, string $id): JsonResponse /** * Link a device to a telematic. */ + #[SkipAuthorizationCheck] public function linkDevice(Request $request, string $id): JsonResponse { $telematic = $this->findTelematic($id); @@ -199,6 +218,7 @@ public function linkDevice(Request $request, string $id): JsonResponse ], 201); } + #[SkipAuthorizationCheck] public function telemetryWebhook(Request $request, string $id): JsonResponse { $telematic = $this->findTelematic($id); @@ -248,6 +268,7 @@ public function telemetryDiagnostics(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function replayTelemetryDelivery(string $id, string $delivery): JsonResponse { $telematic = $this->findTelematic($id); diff --git a/server/src/Http/Controllers/Internal/v1/TrailerController.php b/server/src/Http/Controllers/Internal/v1/TrailerController.php index 728cd1cc8..841ab10e8 100644 --- a/server/src/Http/Controllers/Internal/v1/TrailerController.php +++ b/server/src/Http/Controllers/Internal/v1/TrailerController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException; use Fleetbase\FleetOps\Exports\TrailerExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; @@ -23,6 +24,20 @@ class TrailerController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'attach' => 'attach-vehicle-for trailer', + 'detach' => 'detach-vehicle-for trailer', + 'attachDevice' => 'attach-device-for trailer', + 'detachDevice' => 'detach-device-for trailer', + 'attachEquipment' => 'attach-equipment-for trailer', + 'detachEquipment' => 'detach-equipment-for trailer', + ]; + /** * The resource to query. * @@ -69,6 +84,7 @@ protected function deleteTrailerRecord($id, Request $request) /** * Attach the trailer to a vehicle. Re-attaching to the same vehicle is idempotent. */ + #[SkipAuthorizationCheck] public function attach(Request $request, string $id) { $request->validate(['vehicle' => ['required', 'string'], 'position' => ['nullable', 'integer', 'min:1']]); @@ -134,6 +150,7 @@ public function attach(Request $request, string $id) /** * End the active towing connection. Detaching an unattached trailer is a no-op. */ + #[SkipAuthorizationCheck] public function detach(string $id) { $trailer = $this->resolveTrailer($id); @@ -157,6 +174,7 @@ public function detach(string $id) ]); } + #[SkipAuthorizationCheck] public function attachDevice(Request $request, string $id) { $request->validate(['device' => ['required', 'string']]); @@ -186,6 +204,7 @@ public function attachDevice(Request $request, string $id) ]); } + #[SkipAuthorizationCheck] public function detachDevice(Request $request, string $id) { $request->validate(['device' => ['required', 'string']]); @@ -215,6 +234,7 @@ public function detachDevice(Request $request, string $id) ]); } + #[SkipAuthorizationCheck] public function attachEquipment(Request $request, string $id) { $request->validate(['equipment' => ['required', 'string']]); @@ -242,6 +262,7 @@ public function attachEquipment(Request $request, string $id) ]); } + #[SkipAuthorizationCheck] public function detachEquipment(Request $request, string $id) { $request->validate(['equipment' => ['required', 'string']]); diff --git a/server/src/Http/Controllers/Internal/v1/VehicleController.php b/server/src/Http/Controllers/Internal/v1/VehicleController.php index 9678a9b3b..b8f113d93 100644 --- a/server/src/Http/Controllers/Internal/v1/VehicleController.php +++ b/server/src/Http/Controllers/Internal/v1/VehicleController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException; use Fleetbase\FleetOps\Exports\VehicleExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; @@ -25,6 +26,20 @@ class VehicleController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'unassignDriver' => 'assign-driver-for vehicle', + 'unassignOrders' => 'update vehicle', + 'attachDevice' => 'update vehicle', + 'detachDevice' => 'update vehicle', + 'attachEquipment' => 'update vehicle', + 'detachEquipment' => 'update vehicle', + ]; + /** * The resource to query. * @@ -111,6 +126,7 @@ public function assignDriver(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignDriver(string $id): JsonResponse { $vehicle = $this->findVehicle($id); @@ -143,6 +159,7 @@ public function assignedOrders(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignOrders(Request $request, string $id): JsonResponse { $request->validate([ @@ -179,6 +196,7 @@ public function unassignOrders(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function attachDevice(Request $request, string $id): JsonResponse { $request->validate(['device' => 'required|string']); @@ -219,6 +237,7 @@ public function attachDevice(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function detachDevice(Request $request, string $id): JsonResponse { $request->validate(['device' => 'required|string']); @@ -265,6 +284,7 @@ public function detachDevice(Request $request, string $id): JsonResponse * Attach equipment to the vehicle. Equipment can only be equipped to one asset at a * time, so attaching moves it from any previous vehicle or trailer. */ + #[SkipAuthorizationCheck] public function attachEquipment(Request $request, string $id): JsonResponse { $request->validate(['equipment' => 'required|string']); @@ -292,6 +312,7 @@ public function attachEquipment(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function detachEquipment(Request $request, string $id): JsonResponse { $request->validate(['equipment' => 'required|string']); diff --git a/server/src/Http/Controllers/Internal/v1/VendorController.php b/server/src/Http/Controllers/Internal/v1/VendorController.php index a4e02a2ea..69c100da2 100644 --- a/server/src/Http/Controllers/Internal/v1/VendorController.php +++ b/server/src/Http/Controllers/Internal/v1/VendorController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exports\VendorExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Http\Resources\v1\Contact as ContactResource; @@ -19,6 +20,18 @@ class VendorController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'assignDriver' => 'update vendor', + 'removeDriver' => 'update vendor', + 'addVendorPersonnel' => 'update vendor', + 'removeVendorPersonnel' => 'update vendor', + ]; + /** * The resource to query. * @@ -128,6 +141,7 @@ public function import(ImportRequest $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function assignDriver(string $id, Request $request) { // Validate only param @@ -160,6 +174,7 @@ public function assignDriver(string $id, Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function removeDriver(string $id, Request $request) { // Validate only param @@ -197,6 +212,7 @@ public function vendorPersonnels(string $vendorId) return response()->json(['personnels' => $personnels->values()]); } + #[SkipAuthorizationCheck] public function addVendorPersonnel(Request $request, string $vendorId) { $vendor = $this->findVendorByIdOrFail($vendorId); @@ -220,6 +236,7 @@ public function addVendorPersonnel(Request $request, string $vendorId) ]); } + #[SkipAuthorizationCheck] public function removeVendorPersonnel(string $vendorId, string $contactId) { $vendor = $this->findVendorByIdOrFail($vendorId); diff --git a/server/src/Http/Controllers/Internal/v1/WorkOrderController.php b/server/src/Http/Controllers/Internal/v1/WorkOrderController.php index 51d917245..28d37e315 100644 --- a/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +++ b/server/src/Http/Controllers/Internal/v1/WorkOrderController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Exports\WorkOrderExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Imports\WorkOrderImport; @@ -16,6 +17,15 @@ class WorkOrderController extends FleetOpsController { + /** + * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController). + * + * @var array + */ + protected array $methodPermissions = [ + 'sendEmail' => 'update work-order', + ]; + /** * The resource to query. * @@ -80,6 +90,7 @@ protected function importFile(WorkOrderImport $import, string $path, string $dis * Send a work order email to the assigned vendor. * POST /work-orders/{id}/send. */ + #[SkipAuthorizationCheck] public function sendEmail(string $id): JsonResponse { $workOrder = $this->workOrderForEmail($id); diff --git a/server/src/Support/Authorization.php b/server/src/Support/Authorization.php new file mode 100644 index 000000000..9b8f22d9d --- /dev/null +++ b/server/src/Support/Authorization.php @@ -0,0 +1,103 @@ +json(['errors' => ['User is not authorized to ' . $permissions[0]]], 401)); + } + + /** + * Abort with a 401 JSON response unless the current user is a platform administrator. + * + * @throws HttpResponseException + */ + public static function authorizeAdmin(): void + { + $user = static::user(); + if ($user && $user->isAdmin()) { + return; + } + + throw new HttpResponseException(response()->json(['errors' => ['This action requires a system administrator']], 401)); + } + + public static function canAny(string ...$permissions): bool + { + $user = static::user(); + if (!$user) { + return false; + } + + if ($user->isAdmin()) { + return true; + } + + foreach ($permissions as $permission) { + if (static::can($permission)) { + return true; + } + } + + return false; + } + + public static function can(string $permission): bool + { + [$action, $resource] = explode(' ', $permission, 2); + + // Same test as Auth::isResourceGuarded(): no permission rows for the resource yet. + if (!Permission::where('name', 'like', static::SERVICE . ' % ' . $resource)->exists()) { + return true; + } + + try { + return Auth::can(static::SERVICE . " {$action} {$resource}"); + } catch (\Throwable $e) { + return false; + } + } + + protected static function user() + { + try { + return Auth::getUserFromSession(); + } catch (\Throwable $e) { + return null; + } + } +} diff --git a/server/src/Traits/AuthorizesMethods.php b/server/src/Traits/AuthorizesMethods.php new file mode 100644 index 000000000..ee4c24181 --- /dev/null +++ b/server/src/Traits/AuthorizesMethods.php @@ -0,0 +1,37 @@ + $map method name => permission(s) + */ + protected function authorizeMethods(array $map): void + { + foreach ($map as $method => $permissions) { + $this->middleware(function ($request, $next) use ($permissions) { + if ($permissions === 'admin') { + Authorization::authorizeAdmin(); + } else { + Authorization::authorize(...(array) $permissions); + } + + return $next($request); + })->only($method); + } + } +} diff --git a/server/src/routes.php b/server/src/routes.php index 9789b37f8..dd757f07c 100644 --- a/server/src/routes.php +++ b/server/src/routes.php @@ -405,8 +405,10 @@ function ($router) { $router->group( ['prefix' => 'v1/fleet-ops/navigator', 'namespace' => 'v1'], function ($router) { - $router->get('get-link-app', 'NavigatorController@getLinkAppUrl'); + // Opened from the Navigator app's QR scan without a session; + // NavigatorController::linkApp() requires a valid signature. $router->get('link-app', 'NavigatorController@linkApp'); + $router->get('get-link-app', 'NavigatorController@getLinkAppUrl')->middleware(['fleetbase.protected']); } ); diff --git a/server/tests/NavigatorControllerContractsTest.php b/server/tests/NavigatorControllerContractsTest.php index 0b2060cf0..216804e17 100644 --- a/server/tests/NavigatorControllerContractsTest.php +++ b/server/tests/NavigatorControllerContractsTest.php @@ -137,17 +137,24 @@ function fleetopsNavigatorCredential(string $key = 'flb_live_key'): ApiCredentia return $credential; } +function fleetopsNavigatorSignedQuery(): array +{ + $expires = time() + 600; + + return ['expires' => $expires, 'signature' => NavigatorController::linkSignature($expires)]; +} + test('navigator controller builds android and ios app link redirects', function () { $company = fleetopsNavigatorCompany(); $controller = new FleetOpsNavigatorControllerProbe(); $controller->adminUser = fleetopsNavigatorUser($company); $controller->apiCredential = fleetopsNavigatorCredential('flb_live_navigator'); - $android = $controller->linkApp(Request::create('/navigator/link-app', 'GET', [], [], [], [ + $android = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery(), [], [], [ 'HTTP_USER_AGENT' => 'Mozilla/5.0 Android', ]))->getData(true); - $ios = $controller->linkApp(Request::create('/navigator/link-app', 'GET', [], [], [], [ + $ios = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery(), [], [], [ 'HTTP_USER_AGENT' => 'Mozilla/5.0 iPhone', ]))->getData(true); @@ -169,11 +176,32 @@ function fleetopsNavigatorCredential(string $key = 'flb_live_key'): ApiCredentia $controller = new FleetOpsNavigatorControllerProbe(); $controller->adminUser = fleetopsNavigatorUser(null); - $response = $controller->linkApp(new Request()); + $response = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery())); expect($response->getData(true))->toBe(['error' => 'Organization for linking not found.']); }); +test('navigator controller refuses unsigned, tampered and expired app links before touching credentials', function () { + $company = fleetopsNavigatorCompany(); + $controller = new FleetOpsNavigatorControllerProbe(); + $controller->adminUser = fleetopsNavigatorUser($company); + $controller->apiCredential = fleetopsNavigatorCredential('flb_live_navigator'); + + $expired = time() - 60; + $requests = [ + 'unsigned' => Request::create('/navigator/link-app', 'GET'), + 'tampered' => Request::create('/navigator/link-app', 'GET', ['signature' => 'forged'] + fleetopsNavigatorSignedQuery()), + 'expired' => Request::create('/navigator/link-app', 'GET', ['expires' => $expired, 'signature' => NavigatorController::linkSignature($expired)]), + ]; + + foreach ($requests as $request) { + $response = $controller->linkApp($request); + expect($response->getData(true))->toBe(['error' => 'This Navigator link is invalid or has expired. Generate a new one from the console.']); + } + + expect($controller->credentialLookups)->toBe([]); +}); + test('navigator controller exposes link url settings and current organization token lookup branches', function () { $controller = new FleetOpsNavigatorControllerProbe(); $controller->apiCredential = fleetopsNavigatorCredential('flb_test_key'); @@ -194,7 +222,11 @@ function fleetopsNavigatorCredential(string $key = 'flb_live_key'): ApiCredentia ]); $missing = $controller->getCurrentOrganization($secretRequest); - expect($linkUrl)->toBe(['linkUrl' => 'http://localhost/int/v1/fleet-ops/navigator/link-app']) + parse_str((string) parse_url($linkUrl['linkUrl'], PHP_URL_QUERY), $linkQuery); + + expect($linkUrl['linkUrl'])->toStartWith('http://localhost/int/v1/fleet-ops/navigator/link-app?') + ->and($linkQuery['signature'])->toBe(NavigatorController::linkSignature((int) $linkQuery['expires'])) + ->and((int) $linkQuery['expires'])->toBeGreaterThan(time()) ->and($settings)->toBe(['enabled' => true, 'invite_code_required' => false]) ->and($organization)->toBeInstanceOf(Organization::class) ->and($controller->credentialLookups)->toContain( diff --git a/server/tests/SettingControllerContractsTest.php b/server/tests/SettingControllerContractsTest.php index f1d7e7770..e24b75ac3 100644 --- a/server/tests/SettingControllerContractsTest.php +++ b/server/tests/SettingControllerContractsTest.php @@ -8,6 +8,7 @@ class FleetOpsSettingControllerProbe extends SettingController public array $configured = []; public array $configuredCompany = []; public array $settingValues = []; + public array $orderConfigKeys = []; public array $settings = []; public array $companySettings = []; public array $lookupCompanySettings = []; @@ -34,6 +35,11 @@ protected function configureCompanySetting(string $key, mixed $value): mixed return null; } + protected function companyOrderConfigKeys(): array + { + return $this->orderConfigKeys; + } + protected function settingValue(string $key): mixed { return $this->settingValues[$key] ?? null; @@ -141,16 +147,22 @@ function fleetopsJsonPayload(mixed $response): array } test('setting controller persists and returns basic company settings through configured keys', function () { - $controller = new FleetOpsSettingControllerProbe(); - - $entityPayload = fleetopsJsonPayload($controller->saveEntityEditingSettings(new Request([ - 'entityEditingSettings' => ['orders' => ['editable' => true]], + session(['company' => 'company-1']); + $controller = new FleetOpsSettingControllerProbe(); + $controller->orderConfigKeys = ['orders']; + + // The setting is one platform-wide map keyed by order config: another company's entry must survive, + // and this company cannot write keys for order configs it does not own. + $controller->settingValues['fleet-ops.entity-editing-settings'] = ['foreign-config' => ['editable' => true]]; + $entityPayload = fleetopsJsonPayload($controller->saveEntityEditingSettings(new Request([ + 'entityEditingSettings' => ['orders' => ['editable' => true], 'foreign-config' => ['editable' => false]], ]))); - $controller->settingValues['fleet-ops.entity-editing-settings'] = ['orders' => ['editable' => false]]; + $controller->settingValues['fleet-ops.entity-editing-settings'] = ['orders' => ['editable' => false], 'foreign-config' => ['editable' => true]]; + // A request-supplied company id is ignored in favour of the session company. $disabledDriverPayload = fleetopsJsonPayload($controller->savedDriverOnboardSettings(new Request([ 'driverOnboardSettings' => [ - 'companyId' => 'company-1', + 'companyId' => 'company-2', 'enableDriverOnboardFromApp' => false, 'driverMustProvideOnboardDoucments' => true, 'requiredOnboardDocuments' => ['license'], @@ -171,7 +183,7 @@ function fleetopsJsonPayload(mixed $response): array $controller->company = (object) ['stripe_connect_id' => 'acct_123']; expect($entityPayload)->toBe(['entityEditingSettings' => ['orders' => ['editable' => true]]]) - ->and($controller->configured['fleet-ops.entity-editing-settings'])->toBe(['orders' => ['editable' => true]]) + ->and($controller->configured['fleet-ops.entity-editing-settings'])->toBe(['foreign-config' => ['editable' => true], 'orders' => ['editable' => true]]) ->and(fleetopsJsonPayload($controller->getEntityEditingSettings()))->toBe(['entityEditingSettings' => ['orders' => ['editable' => false]]]) ->and(fleetopsJsonPayload((new FleetOpsSettingControllerProbe())->getEntityEditingSettings()))->toBe(['entityEditingSettings' => []]) ->and($disabledDriverPayload['driverOnboardSettings'])->toMatchArray([ @@ -182,6 +194,7 @@ function fleetopsJsonPayload(mixed $response): array 'driverOnboardAppMethod' => '', ]) ->and($controller->configured['fleet-ops.driver-onboard-settings.company-1'])->toBe($disabledDriverPayload['driverOnboardSettings']) + ->and($controller->configured)->not->toHaveKey('fleet-ops.driver-onboard-settings.company-2') ->and(fleetopsJsonPayload($controller->getDriverOnboardSettings('company-1')))->toBe(['driverOnboardSettings' => ['enableDriverOnboardFromApp' => true]]) ->and(fleetopsJsonPayload((new FleetOpsSettingControllerProbe())->getDriverOnboardSettings('missing')))->toBe(['driverOnboardSettings' => []]) ->and($enabledConfigs)->toBe(['order-express', 'order-freight'])