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'])