From 08e06cc9db12b0581416d4ae86eb938fa5db8b0a Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sun, 27 Sep 2026 14:32:59 +0800 Subject: [PATCH 1/3] fix(permissions): enforce Fleet-Ops permissions on unguarded and mis-mapped endpoints Schema - Declare resources the UI already checked but the schema never created, which left their API unguarded and their screens unreachable for non-admins: maintenance-schedule, device, sensor, device-event, telematic, warranty, purchase-rate, fuel-provider-connection/-transaction/-sync-run, analytics, scheduling-settings, tracking-settings. service-rate gains export. - Fix the 'action' => 'actions' key on the settings resources, so `fleet-ops onboard payments` is actually created. - Grant the new resources in the built-in policies (FleetManager, MaintenanceManager, ServiceRateManager, OperationsAdmin, DispatchManager). Custom actions (Support\Authorization + #[SkipAuthorizationCheck]) - AuthorizationGuard maps unmatched method names by HTTP verb, so e.g. bulk-dispatch required `create order`, unassign-vehicle `create driver` and trailer attach `create trailer`. Map them to the schema actions: dispatch, cancel, schedule, import, assign-driver-for, update-route-for order; assign-order-for / assign-vehicle-for / update-user-for driver; attach-/detach-*-for trailer; and update on vehicle, vendor, device, telematic, maintenance-schedule, maintenance, work-order and fuel-provider-* sub-actions. Controllers outside the guard - Analytics and metrics require `view analytics`; live map feeds require list order/driver/vehicle/place; orchestrator, manifests, radar writes, customer portal logins and Stripe payments now require their permissions. - Settings writes require the matching *-settings permission; the admin tracking/map settings require a system admin. - Entity editing settings are one platform-wide map: a save now only writes the session company's order configs and keeps other companies' entries, and reads return only the company's own. Driver onboard settings are pinned to the session company instead of a request-supplied company id. - Navigator link-app returned an API key to unauthenticated callers. The link is now a 30 minute signed URL issued only to system admins, and link-app rejects unsigned requests. --- server/src/Auth/Schemas/FleetOps.php | 91 +++++++++++++++- .../Internal/v1/AnalyticsController.php | 11 ++ .../Internal/v1/ContactController.php | 5 + .../Internal/v1/CustomerController.php | 11 ++ .../Internal/v1/DeviceController.php | 8 ++ .../Internal/v1/DeviceEventController.php | 5 + .../Internal/v1/DriverController.php | 23 ++++ .../v1/FuelProviderConnectionController.php | 8 ++ .../v1/FuelProviderTransactionController.php | 11 ++ .../Internal/v1/InspectionFormController.php | 11 ++ .../Internal/v1/LiveController.php | 15 +++ .../Internal/v1/MaintenanceController.php | 11 ++ .../v1/MaintenanceScheduleController.php | 11 ++ .../Internal/v1/ManifestController.php | 13 +++ .../Internal/v1/MetricsController.php | 11 ++ .../Internal/v1/NavigatorController.php | 14 ++- .../Internal/v1/OrchestrationController.php | 15 +++ .../Internal/v1/OrderController.php | 23 ++++ .../Internal/v1/PaymentController.php | 9 ++ .../Internal/v1/RadarController.php | 21 ++++ .../Internal/v1/SettingController.php | 77 ++++++++++--- .../Internal/v1/TelematicController.php | 20 ++++ .../Internal/v1/TrailerController.php | 20 ++++ .../Internal/v1/VehicleController.php | 20 ++++ .../Internal/v1/VendorController.php | 14 +++ .../Internal/v1/WorkOrderController.php | 5 + server/src/Support/Authorization.php | 103 ++++++++++++++++++ server/src/routes.php | 6 +- 28 files changed, 569 insertions(+), 23 deletions(-) create mode 100644 server/src/Support/Authorization.php diff --git a/server/src/Auth/Schemas/FleetOps.php b/server/src/Auth/Schemas/FleetOps.php index 74bf863ae..084eca17e 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'], + ], ]; /** @@ -187,6 +242,9 @@ class FleetOps 'see extension', '* order', '* route', + 'see analytics', + 'list analytics', + 'view analytics', 'see order-config', 'list order-config', 'view order-config', @@ -230,6 +288,12 @@ class FleetOps '* driver', '* vehicle', '* trailer', + '* device', + '* sensor', + '* telematic', + 'see device-event', + 'list device-event', + 'view device-event', ], ], [ @@ -277,6 +341,7 @@ class FleetOps 'permissions' => [ 'see extension', '* service-rate', + '* purchase-rate', 'see order', 'list order', 'see service-area', @@ -309,6 +374,8 @@ class FleetOps '* inspection-submission', '* equipment', '* part', + '* maintenance-schedule', + '* warranty', '* trailer', 'see vehicle', 'list vehicle', @@ -336,6 +403,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/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..a17605ec2 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; @@ -14,6 +15,7 @@ use Fleetbase\FleetOps\Models\PurchaseRate; use Fleetbase\FleetOps\Models\Vendor; use Fleetbase\FleetOps\Models\VendorPersonnel; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; @@ -108,8 +110,11 @@ public function getAsCustomer($id) ]); } + #[SkipAuthorizationCheck] public function convertToVendor(Request $request, string $id) { + Authorization::authorize('create vendor'); + $contact = $this->contactForVendorConversion($id); $vendor = $this->runContactConversionTransaction(function () use ($contact, $request) { diff --git a/server/src/Http/Controllers/Internal/v1/CustomerController.php b/server/src/Http/Controllers/Internal/v1/CustomerController.php index e17f4c4fd..8c91a537f 100644 --- a/server/src/Http/Controllers/Internal/v1/CustomerController.php +++ b/server/src/Http/Controllers/Internal/v1/CustomerController.php @@ -4,6 +4,7 @@ use Fleetbase\FleetOps\Mail\CustomerCredentialsMail; use Fleetbase\FleetOps\Models\Contact; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\ProfileAccountManager; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\User; @@ -15,6 +16,8 @@ class CustomerController extends Controller { public function createPortalLogin(Request $request) { + Authorization::authorize('reset-credentials-for customer'); + $customer = $this->resolveCustomer($request); $user = $this->resolveCustomerUser($customer); @@ -37,6 +40,8 @@ public function createPortalLogin(Request $request) public function sendCredentials(Request $request) { + Authorization::authorize('reset-credentials-for customer'); + $customer = $this->resolveCustomer($request); $user = $this->resolveCustomerUser($customer); @@ -59,6 +64,8 @@ public function sendCredentials(Request $request) public function deactivatePortalLogin(Request $request) { + Authorization::authorize('reset-credentials-for customer'); + $customer = $this->resolveCustomer($request); $user = $customer->user_uuid ? $this->findUser($customer->user_uuid) : null; @@ -79,6 +86,8 @@ public function deactivatePortalLogin(Request $request) public function reactivatePortalLogin(Request $request) { + Authorization::authorize('reset-credentials-for customer'); + $customer = $this->resolveCustomer($request); $user = $customer->user_uuid ? $this->findUser($customer->user_uuid) : null; @@ -145,6 +154,8 @@ public function reactivatePortalLogin(Request $request) */ public function resetCredentials(Request $request) { + Authorization::authorize('reset-credentials-for customer'); + $customerId = $request->input('customer'); $password = $request->input('password'); $confirmPassword = $request->input('password_confirmation'); diff --git a/server/src/Http/Controllers/Internal/v1/DeviceController.php b/server/src/Http/Controllers/Internal/v1/DeviceController.php index 09efcd960..6396af206 100644 --- a/server/src/Http/Controllers/Internal/v1/DeviceController.php +++ b/server/src/Http/Controllers/Internal/v1/DeviceController.php @@ -2,11 +2,13 @@ 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; use Fleetbase\FleetOps\Models\Device; use Fleetbase\FleetOps\Models\Vehicle; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; use Illuminate\Http\JsonResponse; @@ -110,8 +112,11 @@ public static function onFindRecord($query, $request): void /** * Attach a device to a supported FleetOps resource. */ + #[SkipAuthorizationCheck] public function attach(Request $request, string $id): JsonResponse { + Authorization::authorize('update device'); + $request->validate([ 'vehicle' => 'required_without:attachable_uuid|nullable|string', ]); @@ -152,8 +157,11 @@ public function attach(Request $request, string $id): JsonResponse /** * Detach a device from its current FleetOps resource. */ + #[SkipAuthorizationCheck] public function detach(string $id): JsonResponse { + Authorization::authorize('update device'); + $device = $this->resolveDevice($id); if (!$device) { diff --git a/server/src/Http/Controllers/Internal/v1/DeviceEventController.php b/server/src/Http/Controllers/Internal/v1/DeviceEventController.php index 80b8d8467..3cc196f07 100644 --- a/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +++ b/server/src/Http/Controllers/Internal/v1/DeviceEventController.php @@ -2,8 +2,10 @@ 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\Authorization; use Fleetbase\FleetOps\Support\Utils; use Illuminate\Http\JsonResponse; @@ -51,8 +53,11 @@ public static function onQueryRecord($query, $request): void } } + #[SkipAuthorizationCheck] public function markProcessed(string $id): JsonResponse { + Authorization::authorize('update device-event'); + $deviceEvent = DeviceEvent::where('company_uuid', session('company')) ->where(function ($query) use ($id) { $query->where('uuid', $id)->orWhere('public_id', $id); diff --git a/server/src/Http/Controllers/Internal/v1/DriverController.php b/server/src/Http/Controllers/Internal/v1/DriverController.php index 26203e962..69d47885b 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; @@ -14,6 +15,7 @@ use Fleetbase\FleetOps\Models\Driver; use Fleetbase\FleetOps\Models\Order; use Fleetbase\FleetOps\Models\Vehicle; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\ProfileAccountManager; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; @@ -200,8 +202,11 @@ function ($request, &$driver) { * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function sendCredentials(string $id) { + Authorization::authorize('update-user-for driver'); + [$driver, $user, $error] = $this->resolveDriverLogin($id); if ($error) { return $error; @@ -221,8 +226,11 @@ public function sendCredentials(string $id) * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function resetCredentials(Request $request, string $id) { + Authorization::authorize('update-user-for driver'); + $password = $request->input('password'); if (!is_string($password) || strlen($password) < 8) { return response()->error('Password must be at least 8 characters.'); @@ -255,8 +263,11 @@ public function resetCredentials(Request $request, string $id) * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function deactivateLogin(string $id) { + Authorization::authorize('update-user-for driver'); + [$driver, $user, $error] = $this->resolveDriverLogin($id); if ($error) { return $error; @@ -275,8 +286,11 @@ public function deactivateLogin(string $id) * * @return JsonResponse */ + #[SkipAuthorizationCheck] public function reactivateLogin(string $id) { + Authorization::authorize('update-user-for driver'); + [$driver, $user, $error] = $this->resolveDriverLogin($id); if ($error) { return $error; @@ -430,8 +444,11 @@ public function assignedOrders(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignOrders(Request $request, string $id): JsonResponse { + Authorization::authorize('assign-order-for driver'); + $request->validate([ 'orders' => 'required|array|min:1', 'orders.*' => 'required|string', @@ -462,8 +479,11 @@ public function unassignOrders(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignOrder(string $id): JsonResponse { + Authorization::authorize('assign-order-for driver'); + $driver = $this->findDriver($id); $order = $this->currentAssignedOrderForDriver($driver) ?? $driver->getCurrentOrder(); @@ -498,8 +518,11 @@ public function assignVehicle(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignVehicle(string $id): JsonResponse { + Authorization::authorize('assign-vehicle-for driver'); + $driver = $this->findDriver($id); $vehicle = $driver->vehicle; diff --git a/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php b/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php index af7684887..9123b346a 100644 --- a/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +++ b/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php @@ -2,9 +2,11 @@ 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; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\FuelProviders\FuelProviderService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -38,8 +40,11 @@ public function onBeforeUpdate(Request $request, FuelProviderConnection $connect $this->normalizeConnectionInput($input, $connection); } + #[SkipAuthorizationCheck] public function testCredentials(Request $request, string $provider): JsonResponse { + Authorization::authorize('create fuel-provider-connection'); + $request->validate([ 'credentials' => 'required|array', 'environment' => 'nullable|string|in:production,sandbox', @@ -54,8 +59,11 @@ 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 { + Authorization::authorize('update fuel-provider-connection'); + $connection = $this->findConnection($id); $result = $this->fuelProviderService->testConnection($connection); diff --git a/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php b/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php index 926bd00f5..74ccd0d41 100644 --- a/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +++ b/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php @@ -2,8 +2,10 @@ 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\Authorization; use Fleetbase\FleetOps\Support\FuelProviders\FuelProviderService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -22,8 +24,11 @@ public static function onQueryRecord($query, $request): void $query->with(['vehicle', 'driver', 'fuelReport']); } + #[SkipAuthorizationCheck] public function matchVehicle(Request $request, string $id): JsonResponse { + Authorization::authorize('update fuel-provider-transaction'); + $request->validate(['vehicle' => 'required|string']); $transaction = $this->findTransaction($id); @@ -33,8 +38,11 @@ public function matchVehicle(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function matchOrder(Request $request, string $id): JsonResponse { + Authorization::authorize('update fuel-provider-transaction'); + $request->validate(['order' => 'required|string']); $transaction = $this->findTransaction($id); @@ -44,8 +52,11 @@ public function matchOrder(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function reprocess(Request $request, string $id): JsonResponse { + Authorization::authorize('update fuel-provider-transaction'); + $transaction = $this->findTransaction($id); return response()->json([ diff --git a/server/src/Http/Controllers/Internal/v1/InspectionFormController.php b/server/src/Http/Controllers/Internal/v1/InspectionFormController.php index b77373a82..3ff7f8c0b 100644 --- a/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +++ b/server/src/Http/Controllers/Internal/v1/InspectionFormController.php @@ -2,12 +2,14 @@ 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; use Fleetbase\FleetOps\Models\InspectionForm; use Fleetbase\FleetOps\Models\InspectionLink; use Fleetbase\FleetOps\Models\Vehicle; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\InspectionFormSync; use Fleetbase\FleetOps\Support\InspectionLinkPin; use Fleetbase\Models\User; @@ -107,8 +109,11 @@ public function archive(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function generateLink(Request $request, string $id): JsonResponse { + Authorization::authorize('publish inspection-form'); + $form = $this->resolveForm($id) ->firstOrFail(); @@ -215,8 +220,11 @@ 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 { + Authorization::authorize('publish inspection-form'); + $form = $this->resolveForm($id)->firstOrFail(); $link = InspectionLink::where('inspection_form_uuid', $form->uuid) @@ -243,8 +251,11 @@ 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 { + Authorization::authorize('publish inspection-form'); + $form = $this->resolveForm($id)->firstOrFail(); $validated = $request->validate(['via' => 'required|in:email,sms']); diff --git a/server/src/Http/Controllers/Internal/v1/LiveController.php b/server/src/Http/Controllers/Internal/v1/LiveController.php index 1d8cdf7ac..5c8e8be74 100644 --- a/server/src/Http/Controllers/Internal/v1/LiveController.php +++ b/server/src/Http/Controllers/Internal/v1/LiveController.php @@ -15,6 +15,7 @@ use Fleetbase\FleetOps\Models\Place; use Fleetbase\FleetOps\Models\Route; use Fleetbase\FleetOps\Models\Vehicle; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\LiveCacheService; use Fleetbase\FleetOps\Support\LiveOrderQuery; use Fleetbase\FleetOps\Support\Utils; @@ -37,6 +38,8 @@ class LiveController extends Controller */ public function coordinates() { + Authorization::authorize('list order'); + return LiveCacheService::remember('coordinates', [], function () { $coordinates = []; @@ -70,6 +73,8 @@ public function coordinates() */ public function routes() { + Authorization::authorize('list order'); + return LiveCacheService::remember('routes', [], function () { // Fetch routes that are not canceled or completed and have an assigned driver $routes = Route::where('company_uuid', session('company')) @@ -107,6 +112,8 @@ function ($q) { */ public function orders(Request $request) { + Authorization::authorize('list order'); + $exclude = $request->array('exclude'); $active = $request->boolean('active'); $unassigned = $request->boolean('unassigned'); @@ -142,6 +149,8 @@ public function orders(Request $request) */ public function drivers(Request $request) { + Authorization::authorize('list driver'); + $bounds = $this->normalizeLiveBounds($request); $limit = $this->normalizeLiveLimit($request); $cacheParams = ['bounds' => $bounds, 'limit' => $limit]; @@ -169,6 +178,8 @@ public function drivers(Request $request) */ public function vehicles(Request $request) { + Authorization::authorize('list vehicle'); + $bounds = $this->normalizeLiveBounds($request); $limit = $this->normalizeLiveLimit($request); $cacheParams = ['bounds' => $bounds, 'limit' => $limit]; @@ -197,6 +208,8 @@ public function vehicles(Request $request) */ public function operationsMonitor() { + Authorization::authorize('list order'); + return LiveCacheService::remember('operations-monitor', [], function () { $drivers = Driver::where(['company_uuid' => session('company')]) ->with(['user', 'vehicle']) @@ -370,6 +383,8 @@ protected function buildOperationsMonitorFleetTree($fleetNodes): array */ public function places(Request $request) { + Authorization::authorize('list place'); + $bounds = $this->normalizeLiveBounds($request); $limit = $this->normalizeLiveLimit($request); diff --git a/server/src/Http/Controllers/Internal/v1/MaintenanceController.php b/server/src/Http/Controllers/Internal/v1/MaintenanceController.php index e3153d45c..0f9344a12 100644 --- a/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +++ b/server/src/Http/Controllers/Internal/v1/MaintenanceController.php @@ -2,10 +2,12 @@ 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; use Fleetbase\FleetOps\Models\Maintenance; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\JsonResponse; @@ -67,8 +69,11 @@ 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 { + Authorization::authorize('update maintenance'); + $maintenance = $this->findMaintenanceForLineItem($id); $validated = $request->validate([ @@ -89,8 +94,11 @@ 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 { + Authorization::authorize('update maintenance'); + $maintenance = $this->findMaintenanceForLineItem($id); $validated = $request->validate([ @@ -121,8 +129,11 @@ 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 { + Authorization::authorize('update maintenance'); + $maintenance = $this->findMaintenanceForLineItem($id); if (!$maintenance->removeLineItem($index)) { diff --git a/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php b/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php index 15cfa292b..fe930c9f8 100644 --- a/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +++ b/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php @@ -2,11 +2,13 @@ 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; use Fleetbase\FleetOps\Models\MaintenanceSchedule; use Fleetbase\FleetOps\Models\WorkOrder; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\JsonResponse; @@ -71,8 +73,11 @@ public function import(ImportRequest $request) * Pause a maintenance schedule. * POST /maintenance-schedules/{id}/pause. */ + #[SkipAuthorizationCheck] public function pause(string $id): JsonResponse { + Authorization::authorize('update maintenance-schedule'); + $schedule = $this->findSchedule($id); $schedule->pause(); @@ -88,8 +93,11 @@ public function pause(string $id): JsonResponse * Resume a paused maintenance schedule. * POST /maintenance-schedules/{id}/resume. */ + #[SkipAuthorizationCheck] public function resume(string $id): JsonResponse { + Authorization::authorize('update maintenance-schedule'); + $schedule = $this->findSchedule($id); $schedule->resume(); @@ -105,8 +113,11 @@ 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 { + Authorization::authorize('update maintenance-schedule'); + $schedule = $this->findSchedule($id); $workOrder = $this->createWorkOrderFromSchedule($schedule); diff --git a/server/src/Http/Controllers/Internal/v1/ManifestController.php b/server/src/Http/Controllers/Internal/v1/ManifestController.php index 80f003605..dbd93c044 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\Support\Authorization; use Fleetbase\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -31,6 +32,8 @@ class ManifestController extends Controller */ public function index(Request $request): JsonResponse { + Authorization::authorize('list order'); + $companyUuid = session('company'); $query = $this->manifestQueryForCompany($companyUuid); @@ -59,6 +62,8 @@ public function index(Request $request): JsonResponse */ public function show(string $id): JsonResponse { + Authorization::authorize('view order'); + $manifest = $this->manifestQueryByPublicId($id) ->with($this->manifestShowRelations()) ->firstOrFail(); @@ -73,6 +78,8 @@ public function show(string $id): JsonResponse */ public function cancel(string $id): JsonResponse { + Authorization::authorize('cancel order'); + $manifest = $this->manifestQueryByPublicId($id)->firstOrFail(); $manifest->cancel(); @@ -86,6 +93,8 @@ public function cancel(string $id): JsonResponse */ public function destroy(string $id): JsonResponse { + Authorization::authorize('delete order'); + $manifest = $this->manifestQueryByPublicId($id)->firstOrFail(); $manifest->delete(); @@ -99,6 +108,8 @@ public function destroy(string $id): JsonResponse */ public function showStop(string $id): JsonResponse { + Authorization::authorize('view order'); + $stop = $this->manifestStopQueryByPublicId($id) ->with(['place', 'order.trackingNumber', 'order.payload.dropoff', 'waypoint']) ->firstOrFail(); @@ -115,6 +126,8 @@ public function showStop(string $id): JsonResponse */ public function updateStop(Request $request, string $id): JsonResponse { + Authorization::authorize('update order'); + $stop = $this->manifestStopQueryByPublicId($id)->firstOrFail(); $allowed = ['status', 'sequence', 'actual_arrival', 'meta']; 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..9cf8fb197 100644 --- a/server/src/Http/Controllers/Internal/v1/NavigatorController.php +++ b/server/src/Http/Controllers/Internal/v1/NavigatorController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Resources\Organization; @@ -11,6 +12,7 @@ use Fleetbase\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; class NavigatorController extends Controller @@ -23,6 +25,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 (!$request->hasValidRelativeSignature()) { + 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 +75,12 @@ public function linkApp(Request $request) */ public function getLinkAppUrl() { + Authorization::authorizeAdmin(); + + $path = URL::temporarySignedRoute('fleetops.navigator.link-app', now()->addMinutes(30), [], false); + return response()->json([ - 'linkUrl' => url('int/v1/fleet-ops/navigator/link-app'), + 'linkUrl' => url($path), ]); } diff --git a/server/src/Http/Controllers/Internal/v1/OrchestrationController.php b/server/src/Http/Controllers/Internal/v1/OrchestrationController.php index a4e011d47..2e795fd65 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\Support\Authorization; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; use Illuminate\Http\JsonResponse; @@ -54,6 +55,8 @@ public function __construct(protected OrchestrationEngineRegistry $registry) */ public function orders(Request $request): JsonResponse { + Authorization::authorize('list order'); + $companyUuid = $this->companyUuid(); $query = $this->orchestratorOrdersQuery($companyUuid); @@ -118,6 +121,8 @@ public function orders(Request $request): JsonResponse */ public function run(Request $request): JsonResponse { + Authorization::authorize('optimize order'); + $companyUuid = $this->companyUuid(); $mode = $request->input('mode', 'assign_vehicles'); $orderIds = $request->input('order_ids', []); @@ -323,6 +328,8 @@ public function run(Request $request): JsonResponse */ public function preview(Request $request): JsonResponse { + Authorization::authorize('list order'); + return $this->run($request); } @@ -395,6 +402,8 @@ protected function routeSequencingEngine(): RouteSequencingEngine */ public function commit(Request $request): JsonResponse { + Authorization::authorize('assign-driver-for order'); + $assignments = $request->input('assignments', []); $scheduledDate = $request->input('scheduled_date', now()->toDateString()); $companyUuid = session('company'); @@ -578,6 +587,8 @@ protected function updateWaypointSequence(string $payloadUuid, string $waypointP */ public function engines(): JsonResponse { + Authorization::authorize('list order'); + return response()->json([ 'engines' => $this->registry->available(), ]); @@ -591,6 +602,8 @@ public function engines(): JsonResponse */ public function orderConfigFields(): JsonResponse { + Authorization::authorize('list order'); + $companyUuid = session('company'); $configs = $this->getOrderConfigFieldConfigs($companyUuid) @@ -648,6 +661,8 @@ protected function getCustomFieldsForOrderConfig(string $orderConfigUuid) */ public function importOrders(Request $request): JsonResponse { + Authorization::authorize('import order'); + $rows = $request->input('rows', []); $companyUuid = session('company'); diff --git a/server/src/Http/Controllers/Internal/v1/OrderController.php b/server/src/Http/Controllers/Internal/v1/OrderController.php index 2751c2114..6f69fc5cd 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; @@ -32,6 +33,7 @@ use Fleetbase\FleetOps\Models\TrackingStatus; use Fleetbase\FleetOps\Models\Waypoint; use Fleetbase\FleetOps\Notifications\OrderPing; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; @@ -267,8 +269,11 @@ protected function normalizeCustomerType(array &$input): void * * @return Response */ + #[SkipAuthorizationCheck] public function editOrderRoute(string $id, Request $request) { + Authorization::authorize('update-route-for order'); + $pickup = $request->input('pickup'); $dropoff = $request->input('dropoff'); $return = $request->input('return'); @@ -337,8 +342,11 @@ public function editOrderRoute(string $id, Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function importFromFiles(Request $request) { + Authorization::authorize('import order'); + $info = Utils::lookupIp(); $disk = $request->input('disk', config('filesystems.default')); $files = $request->input('files'); @@ -409,8 +417,11 @@ public function importFromFiles(Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function bulkCancel(Request $request) { + Authorization::authorize('cancel order'); + $request->validate([ 'ids' => ['required', 'array'], ]); @@ -454,8 +465,11 @@ public function bulkCancel(Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function bulkDispatch(BulkDispatchRequest $request) { + Authorization::authorize('dispatch order'); + /** @var Order */ $orders = $this->ordersByUuid($request->input('ids')); @@ -502,8 +516,11 @@ public function bulkDispatch(BulkDispatchRequest $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function bulkAssignDriver(Request $request) { + Authorization::authorize('assign-driver-for order'); + // Validate Inputs $data = $this->validateBulkAssignDriverRequest($request); @@ -578,8 +595,11 @@ public function cancel(CancelOrderRequest $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function dispatchOrder(Request $request) { + Authorization::authorize('dispatch order'); + /** * @var Order */ @@ -1906,8 +1926,11 @@ function ($query) use ($trackingNumber) { * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function scheduleOrder(Request $request) { + Authorization::authorize('schedule order'); + $orderId = $request->input('order'); $scheduledAt = $request->input('scheduled_at'); $driverId = $request->input('driver_id'); diff --git a/server/src/Http/Controllers/Internal/v1/PaymentController.php b/server/src/Http/Controllers/Internal/v1/PaymentController.php index 703bbb283..d4e668005 100644 --- a/server/src/Http/Controllers/Internal/v1/PaymentController.php +++ b/server/src/Http/Controllers/Internal/v1/PaymentController.php @@ -3,6 +3,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; use Fleetbase\FleetOps\Models\PurchaseRate; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Payment; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Resources\FleetbaseResource; @@ -21,6 +22,8 @@ class PaymentController extends Controller */ public function hasStripeConnectAccount() { + Authorization::authorize('view payments'); + $company = $this->getCompany(); if ($company) { return $this->jsonResponse([ @@ -43,6 +46,8 @@ public function hasStripeConnectAccount() */ public function getStripeAccount() { + Authorization::authorize('onboard payments'); + $stripe = $this->stripeClient(); try { @@ -84,6 +89,8 @@ public function getStripeAccount() */ public function getStripeAccountSession(Request $request) { + Authorization::authorize('onboard payments'); + $stripe = $this->stripeClient(); $company = $this->getCompany(); @@ -112,6 +119,8 @@ public function getStripeAccountSession(Request $request) */ public function getCompanyReceivedPayments(Request $request) { + Authorization::authorize('view payments'); + $limit = $request->input('limit', 30); $query = PurchaseRate::select( [ diff --git a/server/src/Http/Controllers/Internal/v1/RadarController.php b/server/src/Http/Controllers/Internal/v1/RadarController.php index a43e2d401..6251fd684 100644 --- a/server/src/Http/Controllers/Internal/v1/RadarController.php +++ b/server/src/Http/Controllers/Internal/v1/RadarController.php @@ -14,6 +14,7 @@ use Fleetbase\FleetOps\Models\Trailer; use Fleetbase\FleetOps\Models\Vehicle; use Fleetbase\FleetOps\Models\WorkOrder; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\LiveOrderQuery; use Fleetbase\FleetOps\Support\Radar\RadarAgenda; use Fleetbase\FleetOps\Support\Radar\RadarBriefing; @@ -182,6 +183,8 @@ public function handoverSuggest(Request $request, string $key): JsonResponse */ public function extendShift(Request $request, string $id): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $minutes = (int) $request->input('minutes', 60); if ($minutes < 1 || $minutes > 24 * 60) { return response()->json(['error' => 'minutes must be between 1 and 1440.'], 422); @@ -211,6 +214,8 @@ public function extendShift(Request $request, string $id): JsonResponse public function acknowledge(Request $request, string $key): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + return $this->act($request, $key, function (Alert $row) use ($request) { RadarItemState::acknowledge($row, $this->actor($request)); }); @@ -218,6 +223,8 @@ public function acknowledge(Request $request, string $key): JsonResponse public function snooze(Request $request, string $key): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $minutes = $this->snoozeMinutes($request); if ($minutes === null) { return response()->json(['error' => 'Pass minutes (1 to ' . (self::SNOOZE_MAX_DAYS * 1440) . ') or a future until date.'], 422); @@ -230,6 +237,8 @@ public function snooze(Request $request, string $key): JsonResponse public function wake(Request $request, string $key): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + return $this->act($request, $key, function (Alert $row) { RadarItemState::wake($row); }, false); @@ -237,6 +246,8 @@ public function wake(Request $request, string $key): JsonResponse public function assign(Request $request, string $key): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $company = $this->companyUuid($request); $assignee = null; @@ -254,6 +265,8 @@ public function assign(Request $request, string $key): JsonResponse public function plan(Request $request, string $key): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $plannedAt = null; if ($request->filled('planned_at')) { $plannedAt = RadarRules::carbon($request->input('planned_at')); @@ -273,6 +286,8 @@ public function plan(Request $request, string $key): JsonResponse */ public function resolve(Request $request, string $key): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $parsed = RadarRules::parseKey($key); if (!$parsed || $parsed[0] !== 'notice') { return response()->json(['error' => 'Only notices resolve by hand; other items close when their record changes.'], 422); @@ -288,6 +303,8 @@ public function resolve(Request $request, string $key): JsonResponse */ public function bulk(Request $request): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $keys = array_values(array_filter((array) $request->input('keys', []), 'is_string')); $action = (string) $request->input('action'); @@ -350,6 +367,8 @@ public function bulk(Request $request): JsonResponse */ public function storeNotice(Request $request): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $message = trim((string) $request->input('message')); $severity = in_array($request->input('severity'), [RadarRules::SEVERITY_CRITICAL, RadarRules::SEVERITY_WARNING, RadarRules::SEVERITY_INFO], true) ? $request->input('severity') : RadarRules::SEVERITY_INFO; $dueAt = $request->filled('due_at') ? RadarRules::carbon($request->input('due_at')) : null; @@ -392,6 +411,8 @@ public function storeNotice(Request $request): JsonResponse public function destroyNotice(Request $request, string $id): JsonResponse { + Authorization::authorize('update driver', 'update vehicle'); + $company = $this->companyUuid($request); $alert = $this->findNotice($company, $id); diff --git a/server/src/Http/Controllers/Internal/v1/SettingController.php b/server/src/Http/Controllers/Internal/v1/SettingController.php index 3a1c568e5..75d1cb175 100644 --- a/server/src/Http/Controllers/Internal/v1/SettingController.php +++ b/server/src/Http/Controllers/Internal/v1/SettingController.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; @@ -21,37 +22,56 @@ class SettingController extends Controller */ public function saveEntityEditingSettings(Request $request) { - $entityEditingSettings = $request->input('entityEditingSettings', []); + Authorization::authorize('update navigator-settings'); - // Save entity editing settings - $this->configureSetting('fleet-ops.entity-editing-settings', $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); - return response()->json(['entityEditingSettings' => $entityEditingSettings]); + $this->configureSetting('fleet-ops.entity-editing-settings', $merged); + + 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 = []; } @@ -66,9 +86,12 @@ public function getDriverOnboardSettings($companyId) */ public function savedDriverOnboardSettings(Request $request) { - $driverOnboardSettings = $request->array('driverOnboardSettings', []); + Authorization::authorize('update navigator-settings'); - if ($driverOnboardSettings['enableDriverOnboardFromApp'] == false) { + $driverOnboardSettings = $request->array('driverOnboardSettings', []); + $driverOnboardSettings['companyId'] = session('company'); + + if (empty($driverOnboardSettings['enableDriverOnboardFromApp'])) { $driverOnboardSettings['driverMustProvideOnboardDoucments'] = false; $driverOnboardSettings['requiredOnboardDocuments'] = []; $driverOnboardSettings['driverOnboardAppMethod'] = ''; @@ -82,6 +105,8 @@ public function savedDriverOnboardSettings(Request $request) public function saveCustomerEnabledOrderConfigs(Request $request) { + Authorization::authorize('update order-config'); + $enabledOrderConfigs = array_values($request->array('enabledOrderConfigs')); $this->configureCompanySetting('fleet-ops.customer-enabled-order-configs', $enabledOrderConfigs); @@ -97,6 +122,8 @@ public function getCustomerEnabledOrderConfigs() public function saveCustomerPortalPaymentConfig(Request $request) { + Authorization::authorize('update payments'); + $paymentsConfig = $request->array('paymentsConfig'); $this->configureCompanySetting('fleet-ops.customer-payments-configs', $paymentsConfig); @@ -147,6 +174,8 @@ public function getNotificationRegistry() */ public function saveNotificationSettings(Request $request) { + Authorization::authorize('update notification-settings'); + $notificationSettings = $request->input('notificationSettings'); if (!is_array($notificationSettings)) { throw new \Exception('Invalid notification settings data.'); @@ -185,6 +214,8 @@ public function getNotificationSettings() */ public function saveRoutingSettings(Request $request) { + Authorization::authorize('update routing-settings'); + $displayEngine = $request->input('display_engine', $request->input('router', 'osrm')); $optimizationEngine = $request->input('optimization_engine', $displayEngine); $unit = $request->input('unit', 'km'); @@ -237,6 +268,8 @@ public function getRoutingSettings() */ public function saveTrackingSettings(Request $request) { + Authorization::authorize('update tracking-settings'); + $config = $this->trackingDefaults(); $fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated'])); if (is_string($fallbacks)) { @@ -286,6 +319,8 @@ public function getTrackingSettings() public function getAdminTrackingSettings() { + Authorization::authorizeAdmin(); + return response()->json(array_merge($this->trackingDefaults(), [ 'providers' => $this->trackingProviderOptions(), ])); @@ -293,6 +328,8 @@ public function getAdminTrackingSettings() public function saveAdminTrackingSettings(Request $request) { + Authorization::authorizeAdmin(); + $config = config('fleetops.tracking', []); $fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated'])); if (is_string($fallbacks)) { @@ -361,6 +398,8 @@ public function getMapSettings() */ public function saveMapSettings(Request $request) { + Authorization::authorize('update map-settings'); + $settings = $request->input('settings', []); // The API key is managed at the system level via core-api — strip it @@ -377,6 +416,8 @@ public function saveMapSettings(Request $request) public function getAdminMapSettings() { + Authorization::authorizeAdmin(); + $defaults = [ 'mapProvider' => 'leaflet', 'googleMapsMapId' => '', @@ -387,6 +428,8 @@ public function getAdminMapSettings() public function saveAdminMapSettings(Request $request) { + Authorization::authorizeAdmin(); + $allowedProviders = ['leaflet', 'google']; $mapProvider = $request->input('mapProvider', 'leaflet'); if (!in_array($mapProvider, $allowedProviders)) { @@ -473,6 +516,8 @@ public function getSchedulingSettings() */ public function saveSchedulingSettings(Request $request) { + Authorization::authorize('update scheduling-settings'); + $settings = [ 'horizon_days' => (int) $request->input('horizon_days', 60), 'default_shift_duration' => (int) $request->input('default_shift_duration', 8), @@ -512,6 +557,8 @@ public function getOrchestratorSettings() */ public function saveOrchestratorSettings(Request $request) { + Authorization::authorize('update routing-settings'); + $settings = [ 'allocation_engine' => $request->input('allocation_engine', 'vroom'), 'auto_allocate_on_create' => (bool) $request->input('auto_allocate_on_create', false), @@ -548,6 +595,8 @@ public function getOrchestratorCardFields() */ public function saveOrchestratorCardFields(Request $request) { + Authorization::authorize('update routing-settings'); + $settings = $request->input('settings', []); $normalized = [ diff --git a/server/src/Http/Controllers/Internal/v1/TelematicController.php b/server/src/Http/Controllers/Internal/v1/TelematicController.php index acb8dc5fa..7d778a565 100644 --- a/server/src/Http/Controllers/Internal/v1/TelematicController.php +++ b/server/src/Http/Controllers/Internal/v1/TelematicController.php @@ -2,9 +2,11 @@ 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; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry; use Fleetbase\FleetOps\Support\Telematics\TelematicService; use Fleetbase\Http\Requests\ExportRequest; @@ -71,8 +73,11 @@ public function providers(): JsonResponse /** * Test connection to provider. */ + #[SkipAuthorizationCheck] public function testConnection(Request $request, string $id): JsonResponse { + Authorization::authorize('update telematic'); + $telematic = $this->findTelematic($id); $async = $request->input('async', false); @@ -89,8 +94,11 @@ public function testConnection(Request $request, string $id): JsonResponse /** * Test connection to provider. */ + #[SkipAuthorizationCheck] public function testCredentials(Request $request, string $key): JsonResponse { + Authorization::authorize('create telematic'); + $credentials = $request->array('credentials', []); $async = $request->input('async', false); @@ -120,8 +128,11 @@ public function testCredentials(Request $request, string $key): JsonResponse /** * Discover devices from provider. */ + #[SkipAuthorizationCheck] public function discover(Request $request, string $id): JsonResponse { + Authorization::authorize('update telematic'); + $telematic = $this->findTelematic($id); $jobId = $this->telematicService->discoverDevices($telematic, [ @@ -181,8 +192,11 @@ public function logs(Request $request, string $id): JsonResponse /** * Link a device to a telematic. */ + #[SkipAuthorizationCheck] public function linkDevice(Request $request, string $id): JsonResponse { + Authorization::authorize('update telematic'); + $telematic = $this->findTelematic($id); $request->validate([ @@ -199,8 +213,11 @@ public function linkDevice(Request $request, string $id): JsonResponse ], 201); } + #[SkipAuthorizationCheck] public function telemetryWebhook(Request $request, string $id): JsonResponse { + Authorization::authorize('update telematic'); + $telematic = $this->findTelematic($id); $provider = $this->registry->resolve($telematic->provider); abort_unless($provider instanceof \Fleetbase\FleetOps\Contracts\TelemetryProviderInterface, 422); @@ -248,8 +265,11 @@ public function telemetryDiagnostics(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function replayTelemetryDelivery(string $id, string $delivery): JsonResponse { + Authorization::authorize('update telematic'); + $telematic = $this->findTelematic($id); $provider = $this->registry->resolve($telematic->provider); abort_unless($provider instanceof \Fleetbase\FleetOps\Contracts\TelemetryProviderInterface, 422); diff --git a/server/src/Http/Controllers/Internal/v1/TrailerController.php b/server/src/Http/Controllers/Internal/v1/TrailerController.php index 728cd1cc8..029b4a417 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; @@ -13,6 +14,7 @@ use Fleetbase\FleetOps\Models\Equipment; use Fleetbase\FleetOps\Models\Trailer; use Fleetbase\FleetOps\Models\Vehicle; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Fleetbase\Support\Resolve; @@ -69,8 +71,11 @@ 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) { + Authorization::authorize('attach-vehicle-for trailer'); + $request->validate(['vehicle' => ['required', 'string'], 'position' => ['nullable', 'integer', 'min:1']]); $trailer = $this->resolveTrailer($id); @@ -134,8 +139,11 @@ 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) { + Authorization::authorize('detach-vehicle-for trailer'); + $trailer = $this->resolveTrailer($id); if (!$trailer) { @@ -157,8 +165,11 @@ public function detach(string $id) ]); } + #[SkipAuthorizationCheck] public function attachDevice(Request $request, string $id) { + Authorization::authorize('attach-device-for trailer'); + $request->validate(['device' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); @@ -186,8 +197,11 @@ public function attachDevice(Request $request, string $id) ]); } + #[SkipAuthorizationCheck] public function detachDevice(Request $request, string $id) { + Authorization::authorize('detach-device-for trailer'); + $request->validate(['device' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); @@ -215,8 +229,11 @@ public function detachDevice(Request $request, string $id) ]); } + #[SkipAuthorizationCheck] public function attachEquipment(Request $request, string $id) { + Authorization::authorize('attach-equipment-for trailer'); + $request->validate(['equipment' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); @@ -242,8 +259,11 @@ public function attachEquipment(Request $request, string $id) ]); } + #[SkipAuthorizationCheck] public function detachEquipment(Request $request, string $id) { + Authorization::authorize('detach-equipment-for trailer'); + $request->validate(['equipment' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); diff --git a/server/src/Http/Controllers/Internal/v1/VehicleController.php b/server/src/Http/Controllers/Internal/v1/VehicleController.php index 9678a9b3b..ccb861e71 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; @@ -12,6 +13,7 @@ use Fleetbase\FleetOps\Models\Equipment; use Fleetbase\FleetOps\Models\Order; use Fleetbase\FleetOps\Models\Vehicle; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Fleetbase\Support\Resolve; @@ -111,8 +113,11 @@ public function assignDriver(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignDriver(string $id): JsonResponse { + Authorization::authorize('assign-driver-for vehicle'); + $vehicle = $this->findVehicle($id); $vehicle->unassignDriver(); $vehicle->load(['driver', 'devices']); @@ -143,8 +148,11 @@ public function assignedOrders(string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function unassignOrders(Request $request, string $id): JsonResponse { + Authorization::authorize('update vehicle'); + $request->validate([ 'orders' => 'required|array|min:1', 'orders.*' => 'required|string', @@ -179,8 +187,11 @@ public function unassignOrders(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function attachDevice(Request $request, string $id): JsonResponse { + Authorization::authorize('update vehicle'); + $request->validate(['device' => 'required|string']); $deviceId = $request->input('device'); @@ -219,8 +230,11 @@ public function attachDevice(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function detachDevice(Request $request, string $id): JsonResponse { + Authorization::authorize('update vehicle'); + $request->validate(['device' => 'required|string']); $deviceId = $request->input('device'); @@ -265,8 +279,11 @@ 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 { + Authorization::authorize('update vehicle'); + $request->validate(['equipment' => 'required|string']); $vehicle = $this->resolveVehicle($id); @@ -292,8 +309,11 @@ public function attachEquipment(Request $request, string $id): JsonResponse ]); } + #[SkipAuthorizationCheck] public function detachEquipment(Request $request, string $id): JsonResponse { + Authorization::authorize('update vehicle'); + $request->validate(['equipment' => 'required|string']); $vehicle = $this->resolveVehicle($id); diff --git a/server/src/Http/Controllers/Internal/v1/VendorController.php b/server/src/Http/Controllers/Internal/v1/VendorController.php index a4e02a2ea..7cc3d69d5 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; @@ -10,6 +11,7 @@ use Fleetbase\FleetOps\Models\Driver; use Fleetbase\FleetOps\Models\Vendor; use Fleetbase\FleetOps\Models\VendorPersonnel; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\Request; @@ -128,8 +130,11 @@ public function import(ImportRequest $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function assignDriver(string $id, Request $request) { + Authorization::authorize('update vendor'); + // Validate only param if (!$request->isUuid('driver')) { return response()->error('No driver selected to assign to vendor.'); @@ -160,8 +165,11 @@ public function assignDriver(string $id, Request $request) * * @return \Illuminate\Http\Response */ + #[SkipAuthorizationCheck] public function removeDriver(string $id, Request $request) { + Authorization::authorize('update vendor'); + // Validate only param if (!$request->isUuid('driver')) { return response()->error('No driver selected to remove from vendor.'); @@ -197,8 +205,11 @@ public function vendorPersonnels(string $vendorId) return response()->json(['personnels' => $personnels->values()]); } + #[SkipAuthorizationCheck] public function addVendorPersonnel(Request $request, string $vendorId) { + Authorization::authorize('update vendor'); + $vendor = $this->findVendorByIdOrFail($vendorId); $contact = $this->resolveOrCreatePersonnelContact($request); @@ -220,8 +231,11 @@ public function addVendorPersonnel(Request $request, string $vendorId) ]); } + #[SkipAuthorizationCheck] public function removeVendorPersonnel(string $vendorId, string $contactId) { + Authorization::authorize('update vendor'); + $vendor = $this->findVendorByIdOrFail($vendorId); $contact = $this->findContactByIdOrFail($contactId); diff --git a/server/src/Http/Controllers/Internal/v1/WorkOrderController.php b/server/src/Http/Controllers/Internal/v1/WorkOrderController.php index 51d917245..2bb304fa0 100644 --- a/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +++ b/server/src/Http/Controllers/Internal/v1/WorkOrderController.php @@ -2,11 +2,13 @@ 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; use Fleetbase\FleetOps\Mail\WorkOrderDispatched; use Fleetbase\FleetOps\Models\WorkOrder; +use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\JsonResponse; @@ -80,8 +82,11 @@ 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 { + Authorization::authorize('update work-order'); + $workOrder = $this->workOrderForEmail($id); // Resolve recipient email from the assignee (vendor or contact) 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/routes.php b/server/src/routes.php index 631bccc92..7adeec662 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'); - $router->get('link-app', 'NavigatorController@linkApp'); + // Opened from the Navigator app's QR scan without a session; + // NavigatorController::linkApp() requires a valid signature. + $router->get('link-app', 'NavigatorController@linkApp')->name('fleetops.navigator.link-app'); + $router->get('get-link-app', 'NavigatorController@getLinkAppUrl')->middleware(['fleetbase.protected']); } ); From b560a1b77b9cc39d7c5c544cbb2573dd1dd18572 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sun, 27 Sep 2026 14:33:00 +0800 Subject: [PATCH 2/3] fix(permissions): guard Fleet-Ops routes, actions and navigation by permission - List guards on every index route that lacked one (orders, routes, trailers, fuel transactions, integrated vendors, all maintenance and connectivity routes, analytics, settings) and create/view/update guards on new, details and edit routes. Orders is the landing route, so it forwards users without `list order` to the first area they can open. - Index New/Import/Export buttons, bulk actions and row actions pass their permission; the order details menu hides actions the user cannot perform. - Sidebar hub items keep their permissions, branch defaults skip routes the user cannot open, and items use the schema's resource names (analytics, fuel-provider-*, list custom-field/avatar). Reports use the iam report permissions that the core reports API enforces. - Header shortcuts carry their module permission; the virtual route honours menu item permissions. - Live map layers check singular resource names (list vehicle, not vehicles), so non-admin users see their layers. - Maintenance details/edit redirects used route names without the console.fleet-ops prefix and threw; fixed. --- addon/components/fuel-integration/hub.hbs | 2 +- addon/components/layout/fleet-ops-sidebar.js | 48 +++++++++++++------ addon/components/map/leaflet-live-map.js | 3 +- addon/components/telematic/hub.hbs | 2 +- addon/controllers/analytics/reports/index.js | 5 ++ .../controllers/connectivity/devices/index.js | 4 ++ .../connectivity/fuel-providers/index.js | 10 ++-- .../controllers/connectivity/sensors/index.js | 4 ++ .../connectivity/telematics/index.js | 4 ++ .../maintenance/equipment/index.js | 15 ++++-- .../maintenance/inspection-forms/index.js | 4 +- .../inspection-submissions/index.js | 10 +++- .../maintenance/maintenances/index.js | 4 ++ addon/controllers/maintenance/parts/index.js | 15 ++++-- .../maintenance/schedules/index.js | 21 ++++++-- .../maintenance/work-orders/index.js | 15 ++++-- .../controllers/management/contacts/index.js | 4 ++ addon/controllers/management/drivers/index.js | 4 ++ addon/controllers/management/fleets/index.js | 4 ++ .../management/fuel-reports/index.js | 4 ++ .../management/fuel-transactions/index.js | 16 ++++--- addon/controllers/management/issues/index.js | 4 ++ addon/controllers/management/places/index.js | 4 ++ .../controllers/management/vehicles/index.js | 4 ++ addon/controllers/management/vendors/index.js | 4 ++ addon/controllers/operations/orders/index.js | 7 +++ .../operations/orders/index/details.js | 31 +++++++++++- .../operations/service-rates/index.js | 3 ++ addon/extension.js | 11 +++++ addon/routes/analytics/index.js | 15 +++++- addon/routes/analytics/reports.js | 15 +++++- .../routes/analytics/reports/index/details.js | 2 +- addon/routes/analytics/reports/index/edit.js | 2 +- addon/routes/analytics/reports/index/new.js | 15 +++++- addon/routes/connectivity/devices.js | 15 +++++- .../routes/connectivity/devices/index/new.js | 15 +++++- addon/routes/connectivity/events.js | 15 +++++- addon/routes/connectivity/fuel-providers.js | 15 +++++- .../connectivity/fuel-providers/details.js | 9 ++++ .../fuel-providers/details/transactions.js | 12 +++++ .../connectivity/fuel-providers/edit.js | 9 ++++ .../routes/connectivity/fuel-providers/new.js | 15 +++++- addon/routes/connectivity/sensors.js | 15 +++++- .../routes/connectivity/sensors/index/new.js | 15 +++++- addon/routes/connectivity/telematics.js | 15 +++++- addon/routes/connectivity/telematics/new.js | 15 +++++- addon/routes/connectivity/tracking.js | 15 +++++- addon/routes/maintenance/equipment.js | 15 +++++- .../maintenance/equipment/index/details.js | 4 +- .../maintenance/equipment/index/edit.js | 4 +- .../routes/maintenance/equipment/index/new.js | 15 +++++- addon/routes/maintenance/index.js | 15 +++++- addon/routes/maintenance/inspection-forms.js | 15 +++++- .../inspection-forms/index/details.js | 11 ++++- .../inspection-forms/index/edit.js | 11 ++++- .../maintenance/inspection-forms/index/new.js | 15 +++++- .../maintenance/inspection-submissions.js | 15 +++++- .../inspection-submissions/index/details.js | 11 ++++- .../inspection-submissions/index/edit.js | 11 ++++- .../inspection-submissions/index/new.js | 15 +++++- addon/routes/maintenance/maintenances.js | 15 +++++- .../maintenance/maintenances/index/details.js | 4 +- .../maintenance/maintenances/index/edit.js | 4 +- .../maintenance/maintenances/index/new.js | 15 +++++- addon/routes/maintenance/parts.js | 15 +++++- .../routes/maintenance/parts/index/details.js | 4 +- addon/routes/maintenance/parts/index/edit.js | 4 +- addon/routes/maintenance/parts/index/new.js | 15 +++++- addon/routes/maintenance/schedules.js | 16 ++++++- .../maintenance/schedules/index/details.js | 4 +- .../maintenance/schedules/index/edit.js | 4 +- .../routes/maintenance/schedules/index/new.js | 16 ++++++- addon/routes/maintenance/work-orders.js | 15 +++++- .../maintenance/work-orders/index/details.js | 4 +- .../maintenance/work-orders/index/edit.js | 4 +- .../maintenance/work-orders/index/new.js | 15 +++++- addon/routes/management/fuel-transactions.js | 15 +++++- .../fuel-transactions/index/details.js | 9 ++++ addon/routes/management/index.js | 13 +++++ addon/routes/management/trailers.js | 16 +++++++ addon/routes/management/vendors/integrated.js | 15 +++++- .../management/vendors/integrated/details.js | 15 +++++- .../management/vendors/integrated/edit.js | 15 +++++- .../management/vendors/integrated/new.js | 15 +++++- addon/routes/operations/orders.js | 41 +++++++++++++++- addon/routes/operations/routes.js | 15 +++++- .../routes/operations/routes/index/details.js | 15 +++++- addon/routes/operations/routes/index/new.js | 13 +++++ addon/routes/operations/service-rates.js | 15 +++++- .../operations/service-rates/index/new.js | 11 +++++ addon/routes/settings/avatars.js | 15 +++++- addon/routes/settings/custom-fields.js | 15 +++++- addon/routes/settings/map.js | 15 +++++- addon/routes/settings/notifications.js | 11 +++++ addon/routes/settings/orchestrator.js | 2 +- addon/routes/settings/payments/index.js | 2 +- addon/routes/settings/routing.js | 15 +++++- addon/routes/settings/scheduling.js | 15 +++++- addon/routes/virtual.js | 11 +++++ 99 files changed, 1010 insertions(+), 120 deletions(-) create mode 100644 addon/routes/management/trailers.js 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 0abf9056f..faf84a74d 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'), @@ -210,8 +208,8 @@ export default class LayoutFleetOpsSidebarComponent extends Component { this.createItem('menu.routing', 'route', 'settings.routing', 'fleet-ops view routing-settings', 'fleet-ops see routing-settings'), 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.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'), ]); } @@ -265,18 +263,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), @@ -290,7 +306,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), @@ -298,6 +314,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 925f632f9..cd43c6763 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'); + } + } } From 44da70863e3d304125e8612ac49a6d8c4fdcbb3e Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sun, 27 Sep 2026 14:42:23 +0800 Subject: [PATCH 3/3] refactor(permissions): declare method permissions and enforce them as controller middleware - Move the explicit checks out of method bodies into a declarative map: FleetOpsController registers `$methodPermissions` (method => permission) as controller middleware via the AuthorizesMethods trait, and non-resource controllers call authorizeMethods() in their constructor. Behaviour through the router is unchanged; unit tests that call controller methods directly keep exercising the endpoint logic. - Navigator links are signed with an HMAC over the expiry (NavigatorController::linkSignature) instead of Laravel's signed routes, so the link no longer depends on a named route or the URL generator. - Tests: signed/unsigned/tampered/expired Navigator links; entity editing settings keep other companies' entries and ignore keys for foreign order configs; driver onboard settings ignore a request-supplied company id. --- .../Http/Controllers/FleetOpsController.php | 21 +++++++ .../Internal/v1/ContactController.php | 12 +++- .../Internal/v1/CustomerController.php | 25 +++++---- .../Internal/v1/DeviceController.php | 15 +++-- .../Internal/v1/DeviceEventController.php | 12 +++- .../Internal/v1/DriverController.php | 29 +++++----- .../v1/FuelProviderConnectionController.php | 15 +++-- .../v1/FuelProviderTransactionController.php | 18 +++--- .../Internal/v1/InspectionFormController.php | 18 +++--- .../Internal/v1/LiveController.php | 31 ++++++----- .../Internal/v1/MaintenanceController.php | 18 +++--- .../v1/MaintenanceScheduleController.php | 18 +++--- .../Internal/v1/ManifestController.php | 28 +++++----- .../Internal/v1/NavigatorController.php | 43 ++++++++++++--- .../Internal/v1/OrchestrationController.php | 27 ++++----- .../Internal/v1/OrderController.php | 29 +++++----- .../Internal/v1/PaymentController.php | 22 +++++--- .../Internal/v1/RadarController.php | 40 +++++++------- .../Internal/v1/SettingController.php | 55 ++++++++----------- .../Internal/v1/TelematicController.php | 27 ++++----- .../Internal/v1/TrailerController.php | 27 ++++----- .../Internal/v1/VehicleController.php | 27 ++++----- .../Internal/v1/VendorController.php | 21 ++++--- .../Internal/v1/WorkOrderController.php | 12 +++- server/src/Traits/AuthorizesMethods.php | 37 +++++++++++++ server/src/routes.php | 2 +- .../NavigatorControllerContractsTest.php | 40 ++++++++++++-- .../tests/SettingControllerContractsTest.php | 27 ++++++--- 28 files changed, 437 insertions(+), 259 deletions(-) create mode 100644 server/src/Traits/AuthorizesMethods.php 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/ContactController.php b/server/src/Http/Controllers/Internal/v1/ContactController.php index a17605ec2..f4b3346f2 100644 --- a/server/src/Http/Controllers/Internal/v1/ContactController.php +++ b/server/src/Http/Controllers/Internal/v1/ContactController.php @@ -15,7 +15,6 @@ use Fleetbase\FleetOps\Models\PurchaseRate; use Fleetbase\FleetOps\Models\Vendor; use Fleetbase\FleetOps\Models\VendorPersonnel; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; @@ -28,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. * @@ -113,8 +121,6 @@ public function getAsCustomer($id) #[SkipAuthorizationCheck] public function convertToVendor(Request $request, string $id) { - Authorization::authorize('create vendor'); - $contact = $this->contactForVendorConversion($id); $vendor = $this->runContactConversionTransaction(function () use ($contact, $request) { diff --git a/server/src/Http/Controllers/Internal/v1/CustomerController.php b/server/src/Http/Controllers/Internal/v1/CustomerController.php index 8c91a537f..b20ed00b4 100644 --- a/server/src/Http/Controllers/Internal/v1/CustomerController.php +++ b/server/src/Http/Controllers/Internal/v1/CustomerController.php @@ -4,8 +4,8 @@ use Fleetbase\FleetOps\Mail\CustomerCredentialsMail; use Fleetbase\FleetOps\Models\Contact; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\ProfileAccountManager; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\User; use Illuminate\Http\Request; @@ -14,10 +14,21 @@ class CustomerController extends Controller { - public function createPortalLogin(Request $request) + use AuthorizesMethods; + + public function __construct() { - Authorization::authorize('reset-credentials-for customer'); + $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); $user = $this->resolveCustomerUser($customer); @@ -40,8 +51,6 @@ public function createPortalLogin(Request $request) public function sendCredentials(Request $request) { - Authorization::authorize('reset-credentials-for customer'); - $customer = $this->resolveCustomer($request); $user = $this->resolveCustomerUser($customer); @@ -64,8 +73,6 @@ public function sendCredentials(Request $request) public function deactivatePortalLogin(Request $request) { - Authorization::authorize('reset-credentials-for customer'); - $customer = $this->resolveCustomer($request); $user = $customer->user_uuid ? $this->findUser($customer->user_uuid) : null; @@ -86,8 +93,6 @@ public function deactivatePortalLogin(Request $request) public function reactivatePortalLogin(Request $request) { - Authorization::authorize('reset-credentials-for customer'); - $customer = $this->resolveCustomer($request); $user = $customer->user_uuid ? $this->findUser($customer->user_uuid) : null; @@ -154,8 +159,6 @@ public function reactivatePortalLogin(Request $request) */ public function resetCredentials(Request $request) { - Authorization::authorize('reset-credentials-for customer'); - $customerId = $request->input('customer'); $password = $request->input('password'); $confirmPassword = $request->input('password_confirmation'); diff --git a/server/src/Http/Controllers/Internal/v1/DeviceController.php b/server/src/Http/Controllers/Internal/v1/DeviceController.php index 6396af206..ce969fbc2 100644 --- a/server/src/Http/Controllers/Internal/v1/DeviceController.php +++ b/server/src/Http/Controllers/Internal/v1/DeviceController.php @@ -8,7 +8,6 @@ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\Device; use Fleetbase\FleetOps\Models\Vehicle; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; use Illuminate\Http\JsonResponse; @@ -19,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. * @@ -115,8 +124,6 @@ public static function onFindRecord($query, $request): void #[SkipAuthorizationCheck] public function attach(Request $request, string $id): JsonResponse { - Authorization::authorize('update device'); - $request->validate([ 'vehicle' => 'required_without:attachable_uuid|nullable|string', ]); @@ -160,8 +167,6 @@ public function attach(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function detach(string $id): JsonResponse { - Authorization::authorize('update device'); - $device = $this->resolveDevice($id); if (!$device) { diff --git a/server/src/Http/Controllers/Internal/v1/DeviceEventController.php b/server/src/Http/Controllers/Internal/v1/DeviceEventController.php index 3cc196f07..8735cc042 100644 --- a/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +++ b/server/src/Http/Controllers/Internal/v1/DeviceEventController.php @@ -5,12 +5,20 @@ use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\DeviceEvent; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; use Illuminate\Http\JsonResponse; 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. * @@ -56,8 +64,6 @@ public static function onQueryRecord($query, $request): void #[SkipAuthorizationCheck] public function markProcessed(string $id): JsonResponse { - Authorization::authorize('update device-event'); - $deviceEvent = DeviceEvent::where('company_uuid', session('company')) ->where(function ($query) use ($id) { $query->where('uuid', $id)->orWhere('public_id', $id); diff --git a/server/src/Http/Controllers/Internal/v1/DriverController.php b/server/src/Http/Controllers/Internal/v1/DriverController.php index 69d47885b..87d591cee 100644 --- a/server/src/Http/Controllers/Internal/v1/DriverController.php +++ b/server/src/Http/Controllers/Internal/v1/DriverController.php @@ -15,7 +15,6 @@ use Fleetbase\FleetOps\Models\Driver; use Fleetbase\FleetOps\Models\Order; use Fleetbase\FleetOps\Models\Vehicle; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\ProfileAccountManager; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; @@ -35,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. * @@ -205,8 +218,6 @@ function ($request, &$driver) { #[SkipAuthorizationCheck] public function sendCredentials(string $id) { - Authorization::authorize('update-user-for driver'); - [$driver, $user, $error] = $this->resolveDriverLogin($id); if ($error) { return $error; @@ -229,8 +240,6 @@ public function sendCredentials(string $id) #[SkipAuthorizationCheck] public function resetCredentials(Request $request, string $id) { - Authorization::authorize('update-user-for driver'); - $password = $request->input('password'); if (!is_string($password) || strlen($password) < 8) { return response()->error('Password must be at least 8 characters.'); @@ -266,8 +275,6 @@ public function resetCredentials(Request $request, string $id) #[SkipAuthorizationCheck] public function deactivateLogin(string $id) { - Authorization::authorize('update-user-for driver'); - [$driver, $user, $error] = $this->resolveDriverLogin($id); if ($error) { return $error; @@ -289,8 +296,6 @@ public function deactivateLogin(string $id) #[SkipAuthorizationCheck] public function reactivateLogin(string $id) { - Authorization::authorize('update-user-for driver'); - [$driver, $user, $error] = $this->resolveDriverLogin($id); if ($error) { return $error; @@ -447,8 +452,6 @@ public function assignedOrders(string $id): JsonResponse #[SkipAuthorizationCheck] public function unassignOrders(Request $request, string $id): JsonResponse { - Authorization::authorize('assign-order-for driver'); - $request->validate([ 'orders' => 'required|array|min:1', 'orders.*' => 'required|string', @@ -482,8 +485,6 @@ public function unassignOrders(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function unassignOrder(string $id): JsonResponse { - Authorization::authorize('assign-order-for driver'); - $driver = $this->findDriver($id); $order = $this->currentAssignedOrderForDriver($driver) ?? $driver->getCurrentOrder(); @@ -521,8 +522,6 @@ public function assignVehicle(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function unassignVehicle(string $id): JsonResponse { - Authorization::authorize('assign-vehicle-for driver'); - $driver = $this->findDriver($id); $vehicle = $driver->vehicle; diff --git a/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php b/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php index 9123b346a..df99e25e3 100644 --- a/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +++ b/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php @@ -6,7 +6,6 @@ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Jobs\SyncFuelProviderTransactionsJob; use Fleetbase\FleetOps\Models\FuelProviderConnection; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\FuelProviders\FuelProviderService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,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) @@ -43,8 +52,6 @@ public function onBeforeUpdate(Request $request, FuelProviderConnection $connect #[SkipAuthorizationCheck] public function testCredentials(Request $request, string $provider): JsonResponse { - Authorization::authorize('create fuel-provider-connection'); - $request->validate([ 'credentials' => 'required|array', 'environment' => 'nullable|string|in:production,sandbox', @@ -62,8 +69,6 @@ public function testCredentials(Request $request, string $provider): JsonRespons #[SkipAuthorizationCheck] public function testConnection(Request $request, string $id): JsonResponse { - Authorization::authorize('update fuel-provider-connection'); - $connection = $this->findConnection($id); $result = $this->fuelProviderService->testConnection($connection); diff --git a/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php b/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php index 74ccd0d41..b219793ff 100644 --- a/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +++ b/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php @@ -5,13 +5,23 @@ use Fleetbase\Attributes\SkipAuthorizationCheck; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\FuelProviderTransaction; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\FuelProviders\FuelProviderService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; 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) @@ -27,8 +37,6 @@ public static function onQueryRecord($query, $request): void #[SkipAuthorizationCheck] public function matchVehicle(Request $request, string $id): JsonResponse { - Authorization::authorize('update fuel-provider-transaction'); - $request->validate(['vehicle' => 'required|string']); $transaction = $this->findTransaction($id); @@ -41,8 +49,6 @@ public function matchVehicle(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function matchOrder(Request $request, string $id): JsonResponse { - Authorization::authorize('update fuel-provider-transaction'); - $request->validate(['order' => 'required|string']); $transaction = $this->findTransaction($id); @@ -55,8 +61,6 @@ public function matchOrder(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function reprocess(Request $request, string $id): JsonResponse { - Authorization::authorize('update fuel-provider-transaction'); - $transaction = $this->findTransaction($id); return response()->json([ diff --git a/server/src/Http/Controllers/Internal/v1/InspectionFormController.php b/server/src/Http/Controllers/Internal/v1/InspectionFormController.php index 3ff7f8c0b..f7766cbbd 100644 --- a/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +++ b/server/src/Http/Controllers/Internal/v1/InspectionFormController.php @@ -9,7 +9,6 @@ use Fleetbase\FleetOps\Models\InspectionForm; use Fleetbase\FleetOps\Models\InspectionLink; use Fleetbase\FleetOps\Models\Vehicle; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\InspectionFormSync; use Fleetbase\FleetOps\Support\InspectionLinkPin; use Fleetbase\Models\User; @@ -18,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. * @@ -112,8 +122,6 @@ public function archive(string $id): JsonResponse #[SkipAuthorizationCheck] public function generateLink(Request $request, string $id): JsonResponse { - Authorization::authorize('publish inspection-form'); - $form = $this->resolveForm($id) ->firstOrFail(); @@ -223,8 +231,6 @@ public function links(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function revokeLink(Request $request, string $id, string $linkId): JsonResponse { - Authorization::authorize('publish inspection-form'); - $form = $this->resolveForm($id)->firstOrFail(); $link = InspectionLink::where('inspection_form_uuid', $form->uuid) @@ -254,8 +260,6 @@ public function revokeLink(Request $request, string $id, string $linkId): JsonRe #[SkipAuthorizationCheck] public function sendPin(Request $request, string $id, string $linkId): JsonResponse { - Authorization::authorize('publish inspection-form'); - $form = $this->resolveForm($id)->firstOrFail(); $validated = $request->validate(['via' => 'required|in:email,sms']); diff --git a/server/src/Http/Controllers/Internal/v1/LiveController.php b/server/src/Http/Controllers/Internal/v1/LiveController.php index 5c8e8be74..690960baf 100644 --- a/server/src/Http/Controllers/Internal/v1/LiveController.php +++ b/server/src/Http/Controllers/Internal/v1/LiveController.php @@ -15,10 +15,10 @@ use Fleetbase\FleetOps\Models\Place; use Fleetbase\FleetOps\Models\Route; use Fleetbase\FleetOps\Models\Vehicle; -use Fleetbase\FleetOps\Support\Authorization; 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; @@ -27,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; @@ -38,8 +53,6 @@ class LiveController extends Controller */ public function coordinates() { - Authorization::authorize('list order'); - return LiveCacheService::remember('coordinates', [], function () { $coordinates = []; @@ -73,8 +86,6 @@ public function coordinates() */ public function routes() { - Authorization::authorize('list order'); - return LiveCacheService::remember('routes', [], function () { // Fetch routes that are not canceled or completed and have an assigned driver $routes = Route::where('company_uuid', session('company')) @@ -112,8 +123,6 @@ function ($q) { */ public function orders(Request $request) { - Authorization::authorize('list order'); - $exclude = $request->array('exclude'); $active = $request->boolean('active'); $unassigned = $request->boolean('unassigned'); @@ -149,8 +158,6 @@ public function orders(Request $request) */ public function drivers(Request $request) { - Authorization::authorize('list driver'); - $bounds = $this->normalizeLiveBounds($request); $limit = $this->normalizeLiveLimit($request); $cacheParams = ['bounds' => $bounds, 'limit' => $limit]; @@ -178,8 +185,6 @@ public function drivers(Request $request) */ public function vehicles(Request $request) { - Authorization::authorize('list vehicle'); - $bounds = $this->normalizeLiveBounds($request); $limit = $this->normalizeLiveLimit($request); $cacheParams = ['bounds' => $bounds, 'limit' => $limit]; @@ -208,8 +213,6 @@ public function vehicles(Request $request) */ public function operationsMonitor() { - Authorization::authorize('list order'); - return LiveCacheService::remember('operations-monitor', [], function () { $drivers = Driver::where(['company_uuid' => session('company')]) ->with(['user', 'vehicle']) @@ -383,8 +386,6 @@ protected function buildOperationsMonitorFleetTree($fleetNodes): array */ public function places(Request $request) { - Authorization::authorize('list place'); - $bounds = $this->normalizeLiveBounds($request); $limit = $this->normalizeLiveLimit($request); diff --git a/server/src/Http/Controllers/Internal/v1/MaintenanceController.php b/server/src/Http/Controllers/Internal/v1/MaintenanceController.php index 0f9344a12..4205c2fe9 100644 --- a/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +++ b/server/src/Http/Controllers/Internal/v1/MaintenanceController.php @@ -7,7 +7,6 @@ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Imports\MaintenanceImport; use Fleetbase\FleetOps\Models\Maintenance; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\JsonResponse; @@ -17,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. * @@ -72,8 +82,6 @@ public function onFindRecord($builder, $request): void #[SkipAuthorizationCheck] public function addLineItem(string $id, Request $request): JsonResponse { - Authorization::authorize('update maintenance'); - $maintenance = $this->findMaintenanceForLineItem($id); $validated = $request->validate([ @@ -97,8 +105,6 @@ public function addLineItem(string $id, Request $request): JsonResponse #[SkipAuthorizationCheck] public function updateLineItem(string $id, int $index, Request $request): JsonResponse { - Authorization::authorize('update maintenance'); - $maintenance = $this->findMaintenanceForLineItem($id); $validated = $request->validate([ @@ -132,8 +138,6 @@ public function updateLineItem(string $id, int $index, Request $request): JsonRe #[SkipAuthorizationCheck] public function removeLineItem(string $id, int $index): JsonResponse { - Authorization::authorize('update maintenance'); - $maintenance = $this->findMaintenanceForLineItem($id); if (!$maintenance->removeLineItem($index)) { diff --git a/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php b/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php index fe930c9f8..083ef8aad 100644 --- a/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +++ b/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php @@ -8,7 +8,6 @@ use Fleetbase\FleetOps\Imports\MaintenanceScheduleImport; use Fleetbase\FleetOps\Models\MaintenanceSchedule; use Fleetbase\FleetOps\Models\WorkOrder; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\JsonResponse; @@ -24,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. * @@ -76,8 +86,6 @@ public function import(ImportRequest $request) #[SkipAuthorizationCheck] public function pause(string $id): JsonResponse { - Authorization::authorize('update maintenance-schedule'); - $schedule = $this->findSchedule($id); $schedule->pause(); @@ -96,8 +104,6 @@ public function pause(string $id): JsonResponse #[SkipAuthorizationCheck] public function resume(string $id): JsonResponse { - Authorization::authorize('update maintenance-schedule'); - $schedule = $this->findSchedule($id); $schedule->resume(); @@ -116,8 +122,6 @@ public function resume(string $id): JsonResponse #[SkipAuthorizationCheck] public function trigger(string $id, Request $request): JsonResponse { - Authorization::authorize('update maintenance-schedule'); - $schedule = $this->findSchedule($id); $workOrder = $this->createWorkOrderFromSchedule($schedule); diff --git a/server/src/Http/Controllers/Internal/v1/ManifestController.php b/server/src/Http/Controllers/Internal/v1/ManifestController.php index dbd93c044..42329337d 100644 --- a/server/src/Http/Controllers/Internal/v1/ManifestController.php +++ b/server/src/Http/Controllers/Internal/v1/ManifestController.php @@ -4,7 +4,7 @@ use Fleetbase\FleetOps\Models\Manifest; use Fleetbase\FleetOps\Models\ManifestStop; -use Fleetbase\FleetOps\Support\Authorization; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -24,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. @@ -32,8 +46,6 @@ class ManifestController extends Controller */ public function index(Request $request): JsonResponse { - Authorization::authorize('list order'); - $companyUuid = session('company'); $query = $this->manifestQueryForCompany($companyUuid); @@ -62,8 +74,6 @@ public function index(Request $request): JsonResponse */ public function show(string $id): JsonResponse { - Authorization::authorize('view order'); - $manifest = $this->manifestQueryByPublicId($id) ->with($this->manifestShowRelations()) ->firstOrFail(); @@ -78,8 +88,6 @@ public function show(string $id): JsonResponse */ public function cancel(string $id): JsonResponse { - Authorization::authorize('cancel order'); - $manifest = $this->manifestQueryByPublicId($id)->firstOrFail(); $manifest->cancel(); @@ -93,8 +101,6 @@ public function cancel(string $id): JsonResponse */ public function destroy(string $id): JsonResponse { - Authorization::authorize('delete order'); - $manifest = $this->manifestQueryByPublicId($id)->firstOrFail(); $manifest->delete(); @@ -108,8 +114,6 @@ public function destroy(string $id): JsonResponse */ public function showStop(string $id): JsonResponse { - Authorization::authorize('view order'); - $stop = $this->manifestStopQueryByPublicId($id) ->with(['place', 'order.trackingNumber', 'order.payload.dropoff', 'waypoint']) ->firstOrFail(); @@ -126,8 +130,6 @@ public function showStop(string $id): JsonResponse */ public function updateStop(Request $request, string $id): JsonResponse { - Authorization::authorize('update order'); - $stop = $this->manifestStopQueryByPublicId($id)->firstOrFail(); $allowed = ['status', 'sequence', 'actual_arrival', 'meta']; diff --git a/server/src/Http/Controllers/Internal/v1/NavigatorController.php b/server/src/Http/Controllers/Internal/v1/NavigatorController.php index 9cf8fb197..2da2fdc5e 100644 --- a/server/src/Http/Controllers/Internal/v1/NavigatorController.php +++ b/server/src/Http/Controllers/Internal/v1/NavigatorController.php @@ -2,8 +2,8 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Utils; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Resources\Organization; use Fleetbase\Models\ApiCredential; @@ -12,11 +12,23 @@ use Fleetbase\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; -use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; 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. @@ -27,7 +39,7 @@ 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 (!$request->hasValidRelativeSignature()) { + if (!$this->hasValidLinkSignature($request)) { return response()->error('This Navigator link is invalid or has expired. Generate a new one from the console.', 403); } @@ -75,12 +87,13 @@ public function linkApp(Request $request) */ public function getLinkAppUrl() { - Authorization::authorizeAdmin(); - - $path = URL::temporarySignedRoute('fleetops.navigator.link-app', now()->addMinutes(30), [], false); + $expires = now()->addMinutes(static::LINK_TTL_MINUTES)->getTimestamp(); return response()->json([ - 'linkUrl' => url($path), + 'linkUrl' => url('int/v1/fleet-ops/navigator/link-app') . '?' . http_build_query([ + 'expires' => $expires, + 'signature' => static::linkSignature($expires), + ]), ]); } @@ -201,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 2e795fd65..965960a58 100644 --- a/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +++ b/server/src/Http/Controllers/Internal/v1/OrchestrationController.php @@ -16,7 +16,7 @@ use Fleetbase\FleetOps\Orchestration\Engines\DriverAssignmentEngine; use Fleetbase\FleetOps\Orchestration\Engines\RouteSequencingEngine; use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry; -use Fleetbase\FleetOps\Support\Authorization; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; use Illuminate\Http\JsonResponse; @@ -40,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', + ]); } /** @@ -55,8 +66,6 @@ public function __construct(protected OrchestrationEngineRegistry $registry) */ public function orders(Request $request): JsonResponse { - Authorization::authorize('list order'); - $companyUuid = $this->companyUuid(); $query = $this->orchestratorOrdersQuery($companyUuid); @@ -121,8 +130,6 @@ public function orders(Request $request): JsonResponse */ public function run(Request $request): JsonResponse { - Authorization::authorize('optimize order'); - $companyUuid = $this->companyUuid(); $mode = $request->input('mode', 'assign_vehicles'); $orderIds = $request->input('order_ids', []); @@ -328,8 +335,6 @@ public function run(Request $request): JsonResponse */ public function preview(Request $request): JsonResponse { - Authorization::authorize('list order'); - return $this->run($request); } @@ -402,8 +407,6 @@ protected function routeSequencingEngine(): RouteSequencingEngine */ public function commit(Request $request): JsonResponse { - Authorization::authorize('assign-driver-for order'); - $assignments = $request->input('assignments', []); $scheduledDate = $request->input('scheduled_date', now()->toDateString()); $companyUuid = session('company'); @@ -587,8 +590,6 @@ protected function updateWaypointSequence(string $payloadUuid, string $waypointP */ public function engines(): JsonResponse { - Authorization::authorize('list order'); - return response()->json([ 'engines' => $this->registry->available(), ]); @@ -602,8 +603,6 @@ public function engines(): JsonResponse */ public function orderConfigFields(): JsonResponse { - Authorization::authorize('list order'); - $companyUuid = session('company'); $configs = $this->getOrderConfigFieldConfigs($companyUuid) @@ -661,8 +660,6 @@ protected function getCustomFieldsForOrderConfig(string $orderConfigUuid) */ public function importOrders(Request $request): JsonResponse { - Authorization::authorize('import order'); - $rows = $request->input('rows', []); $companyUuid = session('company'); diff --git a/server/src/Http/Controllers/Internal/v1/OrderController.php b/server/src/Http/Controllers/Internal/v1/OrderController.php index 6f69fc5cd..32b762c42 100644 --- a/server/src/Http/Controllers/Internal/v1/OrderController.php +++ b/server/src/Http/Controllers/Internal/v1/OrderController.php @@ -33,7 +33,6 @@ use Fleetbase\FleetOps\Models\TrackingStatus; use Fleetbase\FleetOps\Models\Waypoint; use Fleetbase\FleetOps\Notifications\OrderPing; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; @@ -55,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. @@ -272,8 +285,6 @@ protected function normalizeCustomerType(array &$input): void #[SkipAuthorizationCheck] public function editOrderRoute(string $id, Request $request) { - Authorization::authorize('update-route-for order'); - $pickup = $request->input('pickup'); $dropoff = $request->input('dropoff'); $return = $request->input('return'); @@ -345,8 +356,6 @@ public function editOrderRoute(string $id, Request $request) #[SkipAuthorizationCheck] public function importFromFiles(Request $request) { - Authorization::authorize('import order'); - $info = Utils::lookupIp(); $disk = $request->input('disk', config('filesystems.default')); $files = $request->input('files'); @@ -420,8 +429,6 @@ public function importFromFiles(Request $request) #[SkipAuthorizationCheck] public function bulkCancel(Request $request) { - Authorization::authorize('cancel order'); - $request->validate([ 'ids' => ['required', 'array'], ]); @@ -468,8 +475,6 @@ public function bulkCancel(Request $request) #[SkipAuthorizationCheck] public function bulkDispatch(BulkDispatchRequest $request) { - Authorization::authorize('dispatch order'); - /** @var Order */ $orders = $this->ordersByUuid($request->input('ids')); @@ -519,8 +524,6 @@ public function bulkDispatch(BulkDispatchRequest $request) #[SkipAuthorizationCheck] public function bulkAssignDriver(Request $request) { - Authorization::authorize('assign-driver-for order'); - // Validate Inputs $data = $this->validateBulkAssignDriverRequest($request); @@ -598,8 +601,6 @@ public function cancel(CancelOrderRequest $request) #[SkipAuthorizationCheck] public function dispatchOrder(Request $request) { - Authorization::authorize('dispatch order'); - /** * @var Order */ @@ -1929,8 +1930,6 @@ function ($query) use ($trackingNumber) { #[SkipAuthorizationCheck] public function scheduleOrder(Request $request) { - Authorization::authorize('schedule order'); - $orderId = $request->input('order'); $scheduledAt = $request->input('scheduled_at'); $driverId = $request->input('driver_id'); diff --git a/server/src/Http/Controllers/Internal/v1/PaymentController.php b/server/src/Http/Controllers/Internal/v1/PaymentController.php index d4e668005..03ac407f3 100644 --- a/server/src/Http/Controllers/Internal/v1/PaymentController.php +++ b/server/src/Http/Controllers/Internal/v1/PaymentController.php @@ -3,8 +3,8 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; use Fleetbase\FleetOps\Models\PurchaseRate; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Payment; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Resources\FleetbaseResource; use Fleetbase\Support\Auth; @@ -13,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. * @@ -22,8 +34,6 @@ class PaymentController extends Controller */ public function hasStripeConnectAccount() { - Authorization::authorize('view payments'); - $company = $this->getCompany(); if ($company) { return $this->jsonResponse([ @@ -46,8 +56,6 @@ public function hasStripeConnectAccount() */ public function getStripeAccount() { - Authorization::authorize('onboard payments'); - $stripe = $this->stripeClient(); try { @@ -89,8 +97,6 @@ public function getStripeAccount() */ public function getStripeAccountSession(Request $request) { - Authorization::authorize('onboard payments'); - $stripe = $this->stripeClient(); $company = $this->getCompany(); @@ -119,8 +125,6 @@ public function getStripeAccountSession(Request $request) */ public function getCompanyReceivedPayments(Request $request) { - Authorization::authorize('view payments'); - $limit = $request->input('limit', 30); $query = PurchaseRate::select( [ diff --git a/server/src/Http/Controllers/Internal/v1/RadarController.php b/server/src/Http/Controllers/Internal/v1/RadarController.php index 6251fd684..9cd1e7961 100644 --- a/server/src/Http/Controllers/Internal/v1/RadarController.php +++ b/server/src/Http/Controllers/Internal/v1/RadarController.php @@ -14,12 +14,12 @@ use Fleetbase\FleetOps\Models\Trailer; use Fleetbase\FleetOps\Models\Vehicle; use Fleetbase\FleetOps\Models\WorkOrder; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\LiveOrderQuery; use Fleetbase\FleetOps\Support\Radar\RadarAgenda; 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; @@ -43,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; @@ -183,8 +201,6 @@ public function handoverSuggest(Request $request, string $key): JsonResponse */ public function extendShift(Request $request, string $id): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $minutes = (int) $request->input('minutes', 60); if ($minutes < 1 || $minutes > 24 * 60) { return response()->json(['error' => 'minutes must be between 1 and 1440.'], 422); @@ -214,8 +230,6 @@ public function extendShift(Request $request, string $id): JsonResponse public function acknowledge(Request $request, string $key): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - return $this->act($request, $key, function (Alert $row) use ($request) { RadarItemState::acknowledge($row, $this->actor($request)); }); @@ -223,8 +237,6 @@ public function acknowledge(Request $request, string $key): JsonResponse public function snooze(Request $request, string $key): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $minutes = $this->snoozeMinutes($request); if ($minutes === null) { return response()->json(['error' => 'Pass minutes (1 to ' . (self::SNOOZE_MAX_DAYS * 1440) . ') or a future until date.'], 422); @@ -237,8 +249,6 @@ public function snooze(Request $request, string $key): JsonResponse public function wake(Request $request, string $key): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - return $this->act($request, $key, function (Alert $row) { RadarItemState::wake($row); }, false); @@ -246,8 +256,6 @@ public function wake(Request $request, string $key): JsonResponse public function assign(Request $request, string $key): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $company = $this->companyUuid($request); $assignee = null; @@ -265,8 +273,6 @@ public function assign(Request $request, string $key): JsonResponse public function plan(Request $request, string $key): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $plannedAt = null; if ($request->filled('planned_at')) { $plannedAt = RadarRules::carbon($request->input('planned_at')); @@ -286,8 +292,6 @@ public function plan(Request $request, string $key): JsonResponse */ public function resolve(Request $request, string $key): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $parsed = RadarRules::parseKey($key); if (!$parsed || $parsed[0] !== 'notice') { return response()->json(['error' => 'Only notices resolve by hand; other items close when their record changes.'], 422); @@ -303,8 +307,6 @@ public function resolve(Request $request, string $key): JsonResponse */ public function bulk(Request $request): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $keys = array_values(array_filter((array) $request->input('keys', []), 'is_string')); $action = (string) $request->input('action'); @@ -367,8 +369,6 @@ public function bulk(Request $request): JsonResponse */ public function storeNotice(Request $request): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $message = trim((string) $request->input('message')); $severity = in_array($request->input('severity'), [RadarRules::SEVERITY_CRITICAL, RadarRules::SEVERITY_WARNING, RadarRules::SEVERITY_INFO], true) ? $request->input('severity') : RadarRules::SEVERITY_INFO; $dueAt = $request->filled('due_at') ? RadarRules::carbon($request->input('due_at')) : null; @@ -411,8 +411,6 @@ public function storeNotice(Request $request): JsonResponse public function destroyNotice(Request $request, string $id): JsonResponse { - Authorization::authorize('update driver', 'update vehicle'); - $company = $this->companyUuid($request); $alert = $this->findNotice($company, $id); diff --git a/server/src/Http/Controllers/Internal/v1/SettingController.php b/server/src/Http/Controllers/Internal/v1/SettingController.php index 75d1cb175..fd992f00d 100644 --- a/server/src/Http/Controllers/Internal/v1/SettingController.php +++ b/server/src/Http/Controllers/Internal/v1/SettingController.php @@ -2,8 +2,8 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry; +use Fleetbase\FleetOps\Traits\AuthorizesMethods; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; use Fleetbase\Support\Auth; @@ -15,6 +15,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. * @@ -22,8 +45,6 @@ class SettingController extends Controller */ public function saveEntityEditingSettings(Request $request) { - Authorization::authorize('update navigator-settings'); - // 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(); @@ -86,8 +107,6 @@ public function getDriverOnboardSettings($companyId = null) */ public function savedDriverOnboardSettings(Request $request) { - Authorization::authorize('update navigator-settings'); - $driverOnboardSettings = $request->array('driverOnboardSettings', []); $driverOnboardSettings['companyId'] = session('company'); @@ -105,8 +124,6 @@ public function savedDriverOnboardSettings(Request $request) public function saveCustomerEnabledOrderConfigs(Request $request) { - Authorization::authorize('update order-config'); - $enabledOrderConfigs = array_values($request->array('enabledOrderConfigs')); $this->configureCompanySetting('fleet-ops.customer-enabled-order-configs', $enabledOrderConfigs); @@ -122,8 +139,6 @@ public function getCustomerEnabledOrderConfigs() public function saveCustomerPortalPaymentConfig(Request $request) { - Authorization::authorize('update payments'); - $paymentsConfig = $request->array('paymentsConfig'); $this->configureCompanySetting('fleet-ops.customer-payments-configs', $paymentsConfig); @@ -174,8 +189,6 @@ public function getNotificationRegistry() */ public function saveNotificationSettings(Request $request) { - Authorization::authorize('update notification-settings'); - $notificationSettings = $request->input('notificationSettings'); if (!is_array($notificationSettings)) { throw new \Exception('Invalid notification settings data.'); @@ -214,8 +227,6 @@ public function getNotificationSettings() */ public function saveRoutingSettings(Request $request) { - Authorization::authorize('update routing-settings'); - $displayEngine = $request->input('display_engine', $request->input('router', 'osrm')); $optimizationEngine = $request->input('optimization_engine', $displayEngine); $unit = $request->input('unit', 'km'); @@ -268,8 +279,6 @@ public function getRoutingSettings() */ public function saveTrackingSettings(Request $request) { - Authorization::authorize('update tracking-settings'); - $config = $this->trackingDefaults(); $fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated'])); if (is_string($fallbacks)) { @@ -319,8 +328,6 @@ public function getTrackingSettings() public function getAdminTrackingSettings() { - Authorization::authorizeAdmin(); - return response()->json(array_merge($this->trackingDefaults(), [ 'providers' => $this->trackingProviderOptions(), ])); @@ -328,8 +335,6 @@ public function getAdminTrackingSettings() public function saveAdminTrackingSettings(Request $request) { - Authorization::authorizeAdmin(); - $config = config('fleetops.tracking', []); $fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated'])); if (is_string($fallbacks)) { @@ -398,8 +403,6 @@ public function getMapSettings() */ public function saveMapSettings(Request $request) { - Authorization::authorize('update map-settings'); - $settings = $request->input('settings', []); // The API key is managed at the system level via core-api — strip it @@ -416,8 +419,6 @@ public function saveMapSettings(Request $request) public function getAdminMapSettings() { - Authorization::authorizeAdmin(); - $defaults = [ 'mapProvider' => 'leaflet', 'googleMapsMapId' => '', @@ -428,8 +429,6 @@ public function getAdminMapSettings() public function saveAdminMapSettings(Request $request) { - Authorization::authorizeAdmin(); - $allowedProviders = ['leaflet', 'google']; $mapProvider = $request->input('mapProvider', 'leaflet'); if (!in_array($mapProvider, $allowedProviders)) { @@ -516,8 +515,6 @@ public function getSchedulingSettings() */ public function saveSchedulingSettings(Request $request) { - Authorization::authorize('update scheduling-settings'); - $settings = [ 'horizon_days' => (int) $request->input('horizon_days', 60), 'default_shift_duration' => (int) $request->input('default_shift_duration', 8), @@ -557,8 +554,6 @@ public function getOrchestratorSettings() */ public function saveOrchestratorSettings(Request $request) { - Authorization::authorize('update routing-settings'); - $settings = [ 'allocation_engine' => $request->input('allocation_engine', 'vroom'), 'auto_allocate_on_create' => (bool) $request->input('auto_allocate_on_create', false), @@ -595,8 +590,6 @@ public function getOrchestratorCardFields() */ public function saveOrchestratorCardFields(Request $request) { - Authorization::authorize('update routing-settings'); - $settings = $request->input('settings', []); $normalized = [ diff --git a/server/src/Http/Controllers/Internal/v1/TelematicController.php b/server/src/Http/Controllers/Internal/v1/TelematicController.php index 7d778a565..4f056da5b 100644 --- a/server/src/Http/Controllers/Internal/v1/TelematicController.php +++ b/server/src/Http/Controllers/Internal/v1/TelematicController.php @@ -6,7 +6,6 @@ use Fleetbase\FleetOps\Exports\TelematicExport; use Fleetbase\FleetOps\Http\Controllers\FleetOpsController; use Fleetbase\FleetOps\Models\Telematic; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry; use Fleetbase\FleetOps\Support\Telematics\TelematicService; use Fleetbase\Http\Requests\ExportRequest; @@ -18,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. * @@ -76,8 +89,6 @@ public function providers(): JsonResponse #[SkipAuthorizationCheck] public function testConnection(Request $request, string $id): JsonResponse { - Authorization::authorize('update telematic'); - $telematic = $this->findTelematic($id); $async = $request->input('async', false); @@ -97,8 +108,6 @@ public function testConnection(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function testCredentials(Request $request, string $key): JsonResponse { - Authorization::authorize('create telematic'); - $credentials = $request->array('credentials', []); $async = $request->input('async', false); @@ -131,8 +140,6 @@ public function testCredentials(Request $request, string $key): JsonResponse #[SkipAuthorizationCheck] public function discover(Request $request, string $id): JsonResponse { - Authorization::authorize('update telematic'); - $telematic = $this->findTelematic($id); $jobId = $this->telematicService->discoverDevices($telematic, [ @@ -195,8 +202,6 @@ public function logs(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function linkDevice(Request $request, string $id): JsonResponse { - Authorization::authorize('update telematic'); - $telematic = $this->findTelematic($id); $request->validate([ @@ -216,8 +221,6 @@ public function linkDevice(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function telemetryWebhook(Request $request, string $id): JsonResponse { - Authorization::authorize('update telematic'); - $telematic = $this->findTelematic($id); $provider = $this->registry->resolve($telematic->provider); abort_unless($provider instanceof \Fleetbase\FleetOps\Contracts\TelemetryProviderInterface, 422); @@ -268,8 +271,6 @@ public function telemetryDiagnostics(string $id): JsonResponse #[SkipAuthorizationCheck] public function replayTelemetryDelivery(string $id, string $delivery): JsonResponse { - Authorization::authorize('update telematic'); - $telematic = $this->findTelematic($id); $provider = $this->registry->resolve($telematic->provider); abort_unless($provider instanceof \Fleetbase\FleetOps\Contracts\TelemetryProviderInterface, 422); diff --git a/server/src/Http/Controllers/Internal/v1/TrailerController.php b/server/src/Http/Controllers/Internal/v1/TrailerController.php index 029b4a417..841ab10e8 100644 --- a/server/src/Http/Controllers/Internal/v1/TrailerController.php +++ b/server/src/Http/Controllers/Internal/v1/TrailerController.php @@ -14,7 +14,6 @@ use Fleetbase\FleetOps\Models\Equipment; use Fleetbase\FleetOps\Models\Trailer; use Fleetbase\FleetOps\Models\Vehicle; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Fleetbase\Support\Resolve; @@ -25,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. * @@ -74,8 +87,6 @@ protected function deleteTrailerRecord($id, Request $request) #[SkipAuthorizationCheck] public function attach(Request $request, string $id) { - Authorization::authorize('attach-vehicle-for trailer'); - $request->validate(['vehicle' => ['required', 'string'], 'position' => ['nullable', 'integer', 'min:1']]); $trailer = $this->resolveTrailer($id); @@ -142,8 +153,6 @@ public function attach(Request $request, string $id) #[SkipAuthorizationCheck] public function detach(string $id) { - Authorization::authorize('detach-vehicle-for trailer'); - $trailer = $this->resolveTrailer($id); if (!$trailer) { @@ -168,8 +177,6 @@ public function detach(string $id) #[SkipAuthorizationCheck] public function attachDevice(Request $request, string $id) { - Authorization::authorize('attach-device-for trailer'); - $request->validate(['device' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); @@ -200,8 +207,6 @@ public function attachDevice(Request $request, string $id) #[SkipAuthorizationCheck] public function detachDevice(Request $request, string $id) { - Authorization::authorize('detach-device-for trailer'); - $request->validate(['device' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); @@ -232,8 +237,6 @@ public function detachDevice(Request $request, string $id) #[SkipAuthorizationCheck] public function attachEquipment(Request $request, string $id) { - Authorization::authorize('attach-equipment-for trailer'); - $request->validate(['equipment' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); @@ -262,8 +265,6 @@ public function attachEquipment(Request $request, string $id) #[SkipAuthorizationCheck] public function detachEquipment(Request $request, string $id) { - Authorization::authorize('detach-equipment-for trailer'); - $request->validate(['equipment' => ['required', 'string']]); $trailer = $this->resolveTrailer($id); diff --git a/server/src/Http/Controllers/Internal/v1/VehicleController.php b/server/src/Http/Controllers/Internal/v1/VehicleController.php index ccb861e71..b8f113d93 100644 --- a/server/src/Http/Controllers/Internal/v1/VehicleController.php +++ b/server/src/Http/Controllers/Internal/v1/VehicleController.php @@ -13,7 +13,6 @@ use Fleetbase\FleetOps\Models\Equipment; use Fleetbase\FleetOps\Models\Order; use Fleetbase\FleetOps\Models\Vehicle; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Fleetbase\Support\Resolve; @@ -27,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. * @@ -116,8 +129,6 @@ public function assignDriver(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function unassignDriver(string $id): JsonResponse { - Authorization::authorize('assign-driver-for vehicle'); - $vehicle = $this->findVehicle($id); $vehicle->unassignDriver(); $vehicle->load(['driver', 'devices']); @@ -151,8 +162,6 @@ public function assignedOrders(string $id): JsonResponse #[SkipAuthorizationCheck] public function unassignOrders(Request $request, string $id): JsonResponse { - Authorization::authorize('update vehicle'); - $request->validate([ 'orders' => 'required|array|min:1', 'orders.*' => 'required|string', @@ -190,8 +199,6 @@ public function unassignOrders(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function attachDevice(Request $request, string $id): JsonResponse { - Authorization::authorize('update vehicle'); - $request->validate(['device' => 'required|string']); $deviceId = $request->input('device'); @@ -233,8 +240,6 @@ public function attachDevice(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function detachDevice(Request $request, string $id): JsonResponse { - Authorization::authorize('update vehicle'); - $request->validate(['device' => 'required|string']); $deviceId = $request->input('device'); @@ -282,8 +287,6 @@ public function detachDevice(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function attachEquipment(Request $request, string $id): JsonResponse { - Authorization::authorize('update vehicle'); - $request->validate(['equipment' => 'required|string']); $vehicle = $this->resolveVehicle($id); @@ -312,8 +315,6 @@ public function attachEquipment(Request $request, string $id): JsonResponse #[SkipAuthorizationCheck] public function detachEquipment(Request $request, string $id): JsonResponse { - Authorization::authorize('update vehicle'); - $request->validate(['equipment' => 'required|string']); $vehicle = $this->resolveVehicle($id); diff --git a/server/src/Http/Controllers/Internal/v1/VendorController.php b/server/src/Http/Controllers/Internal/v1/VendorController.php index 7cc3d69d5..69c100da2 100644 --- a/server/src/Http/Controllers/Internal/v1/VendorController.php +++ b/server/src/Http/Controllers/Internal/v1/VendorController.php @@ -11,7 +11,6 @@ use Fleetbase\FleetOps\Models\Driver; use Fleetbase\FleetOps\Models\Vendor; use Fleetbase\FleetOps\Models\VendorPersonnel; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\Request; @@ -21,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. * @@ -133,8 +144,6 @@ public function import(ImportRequest $request) #[SkipAuthorizationCheck] public function assignDriver(string $id, Request $request) { - Authorization::authorize('update vendor'); - // Validate only param if (!$request->isUuid('driver')) { return response()->error('No driver selected to assign to vendor.'); @@ -168,8 +177,6 @@ public function assignDriver(string $id, Request $request) #[SkipAuthorizationCheck] public function removeDriver(string $id, Request $request) { - Authorization::authorize('update vendor'); - // Validate only param if (!$request->isUuid('driver')) { return response()->error('No driver selected to remove from vendor.'); @@ -208,8 +215,6 @@ public function vendorPersonnels(string $vendorId) #[SkipAuthorizationCheck] public function addVendorPersonnel(Request $request, string $vendorId) { - Authorization::authorize('update vendor'); - $vendor = $this->findVendorByIdOrFail($vendorId); $contact = $this->resolveOrCreatePersonnelContact($request); @@ -234,8 +239,6 @@ public function addVendorPersonnel(Request $request, string $vendorId) #[SkipAuthorizationCheck] public function removeVendorPersonnel(string $vendorId, string $contactId) { - Authorization::authorize('update vendor'); - $vendor = $this->findVendorByIdOrFail($vendorId); $contact = $this->findContactByIdOrFail($contactId); diff --git a/server/src/Http/Controllers/Internal/v1/WorkOrderController.php b/server/src/Http/Controllers/Internal/v1/WorkOrderController.php index 2bb304fa0..28d37e315 100644 --- a/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +++ b/server/src/Http/Controllers/Internal/v1/WorkOrderController.php @@ -8,7 +8,6 @@ use Fleetbase\FleetOps\Imports\WorkOrderImport; use Fleetbase\FleetOps\Mail\WorkOrderDispatched; use Fleetbase\FleetOps\Models\WorkOrder; -use Fleetbase\FleetOps\Support\Authorization; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; use Illuminate\Http\JsonResponse; @@ -18,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. * @@ -85,8 +93,6 @@ protected function importFile(WorkOrderImport $import, string $path, string $dis #[SkipAuthorizationCheck] public function sendEmail(string $id): JsonResponse { - Authorization::authorize('update work-order'); - $workOrder = $this->workOrderForEmail($id); // Resolve recipient email from the assignee (vendor or contact) 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 7adeec662..dce00734d 100644 --- a/server/src/routes.php +++ b/server/src/routes.php @@ -407,7 +407,7 @@ function ($router) { function ($router) { // Opened from the Navigator app's QR scan without a session; // NavigatorController::linkApp() requires a valid signature. - $router->get('link-app', 'NavigatorController@linkApp')->name('fleetops.navigator.link-app'); + $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 1163a5f36..7e161c835 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; @@ -107,16 +113,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'], @@ -137,7 +149,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([ @@ -148,6 +160,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'])