@fleetbase/fleetops-engine 0.6.71 → 0.6.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE.md +15 -18
- package/addon/components/admin/tracking-page-settings.hbs +56 -0
- package/addon/components/admin/tracking-page-settings.js +67 -0
- package/addon/components/device/form.hbs +2 -2
- package/addon/components/driver/details.hbs +1 -1
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/equipment/form.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +11 -3
- package/addon/components/maintenance/form.hbs +3 -3
- package/addon/components/map/google-live-map.js +2 -2
- package/addon/components/map/leaflet-live-map.js +45 -32
- package/addon/components/orchestrator/phase-builder.js +2 -1
- package/addon/components/orchestrator-workbench.hbs +7 -1
- package/addon/components/orchestrator-workbench.js +16 -2
- package/addon/components/order/form/route.hbs +1 -1
- package/addon/components/order/form/route.js +13 -1
- package/addon/components/service-rate/details.hbs +1 -1
- package/addon/components/service-rate/form.hbs +1 -1
- package/addon/components/tracking-page/full-view.hbs +276 -0
- package/addon/components/tracking-page/full-view.js +474 -0
- package/addon/components/tracking-page/locale-selector.js +30 -0
- package/addon/components/tracking-page/map.hbs +44 -0
- package/addon/components/tracking-page/map.js +328 -0
- package/addon/components/tracking-page/order-list.hbs +35 -0
- package/addon/components/tracking-page/public-view.hbs +60 -0
- package/addon/components/tracking-page/settings-preview.hbs +21 -0
- package/addon/components/tracking-page/settings-preview.js +23 -0
- package/addon/components/tracking-page/verify-panel.hbs +109 -0
- package/addon/components/tracking-page.hbs +142 -0
- package/addon/components/tracking-page.js +564 -0
- package/addon/components/trailer/form.hbs +2 -2
- package/addon/components/vehicle/form.hbs +3 -3
- package/addon/components/warranty/form.hbs +2 -2
- package/addon/components/widget/live-fleet.js +54 -6
- package/addon/components/widget/proof-of-delivery.hbs +49 -0
- package/addon/components/widget/proof-of-delivery.js +62 -0
- package/addon/components/work-order/form.hbs +3 -3
- package/addon/controllers/operations/orders/index/details.js +19 -3
- package/addon/controllers/operations/scheduler/index.js +45 -14
- package/addon/controllers/settings/index.js +6 -0
- package/addon/controllers/settings/map.js +75 -0
- package/addon/controllers/settings/orchestrator.js +8 -6
- package/addon/controllers/settings/tracking-page.js +212 -0
- package/addon/extension.js +65 -1
- package/addon/helpers/parcel-box-image.js +16 -0
- package/addon/routes/operations/orders/index/new.js +5 -4
- package/addon/routes/operations/scheduler/index.js +1 -1
- package/addon/routes/settings/tracking-page.js +31 -0
- package/addon/routes.js +1 -0
- package/addon/services/location.js +69 -21
- package/addon/services/map-settings.js +28 -0
- package/addon/services/order-allocation.js +1 -1
- package/addon/styles/fleetops-engine.css +1391 -0
- package/addon/templates/settings/map.hbs +46 -0
- package/addon/templates/settings/tracking-page.hbs +318 -0
- package/addon/utils/identity-cell-resource.js +2 -56
- package/addon/utils/map-default-view.js +132 -0
- package/addon/utils/placeholder-images.js +4 -69
- package/addon/utils/resource-descriptors/assets.js +2 -57
- package/addon/utils/resource-descriptors/helpers.js +23 -122
- package/addon/utils/resource-descriptors/operations.js +2 -61
- package/addon/utils/resource-descriptors/people.js +3 -142
- package/addon/utils/resource-descriptors/shared.js +18 -0
- package/addon/utils/tracking-live-source.js +123 -0
- package/addon/utils/tracking-page-theme.js +123 -0
- package/app/components/admin/tracking-page-settings.js +1 -0
- package/app/components/tracking-page/full-view.js +1 -0
- package/app/components/tracking-page/locale-selector.js +1 -0
- package/app/components/tracking-page/map.js +1 -0
- package/app/components/tracking-page/order-list.js +1 -0
- package/app/components/tracking-page/public-view.js +1 -0
- package/app/components/tracking-page/settings-preview.js +1 -0
- package/app/components/tracking-page/verify-panel.js +1 -0
- package/app/components/tracking-page.js +1 -0
- package/app/components/widget/proof-of-delivery.js +1 -0
- package/app/controllers/settings/tracking-page.js +1 -0
- package/app/helpers/parcel-box-image.js +1 -0
- package/app/routes/settings/tracking-page.js +1 -0
- package/app/templates/settings/tracking-page.js +1 -0
- package/app/utils/map-default-view.js +1 -0
- package/app/utils/tracking-live-source.js +1 -0
- package/app/utils/tracking-page-theme.js +1 -0
- package/composer.json +2 -2
- package/extension.json +1 -1
- package/package.json +4 -4
- package/scripts/pest-bootstrap.php +4 -0
- package/server/config/fleetops.php +4 -0
- package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
- package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
- package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
- package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
- package/server/resources/views/labels/default.php +2 -2
- package/server/resources/views/labels/entity-label.php +2 -2
- package/server/resources/views/labels/waypoint-label.php +2 -2
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
- package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
- package/server/src/Events/OrderChanged.php +257 -0
- package/server/src/Events/OrderDriverUnassigned.php +68 -0
- package/server/src/Exceptions/OrderEditLockedException.php +32 -0
- package/server/src/Expansions/CompanyExpansion.php +3 -2
- package/server/src/Expansions/UserExpansion.php +12 -10
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
- package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
- package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
- package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
- package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
- package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
- package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
- package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
- package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
- package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
- package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
- package/server/src/Http/Filter/ContactFilter.php +1 -1
- package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
- package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
- package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
- package/server/src/Http/Filter/OrderFilter.php +39 -4
- package/server/src/Http/Filter/ProofFilter.php +18 -0
- package/server/src/Http/Filter/RouteFilter.php +18 -0
- package/server/src/Http/Filter/TelematicFilter.php +23 -0
- package/server/src/Http/Filter/WarrantyFilter.php +23 -0
- package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
- package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
- package/server/src/Http/Resources/v1/Index/Order.php +11 -2
- package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
- package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
- package/server/src/Http/Resources/v1/Zone.php +31 -23
- package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
- package/server/src/Jobs/ProcessAllocationJob.php +16 -6
- package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
- package/server/src/Listeners/HandleOrderCanceled.php +9 -1
- package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
- package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
- package/server/src/Mail/CustomerCredentialsMail.php +4 -2
- package/server/src/Models/AssetConnection.php +3 -0
- package/server/src/Models/Contact.php +3 -0
- package/server/src/Models/Device.php +28 -0
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +20 -7
- package/server/src/Models/Entity.php +12 -4
- package/server/src/Models/Fleet.php +11 -32
- package/server/src/Models/FleetDriver.php +3 -0
- package/server/src/Models/FleetVehicle.php +3 -0
- package/server/src/Models/FuelReport.php +5 -13
- package/server/src/Models/Issue.php +4 -16
- package/server/src/Models/Order.php +200 -9
- package/server/src/Models/Payload.php +227 -35
- package/server/src/Models/Place.php +7 -1
- package/server/src/Models/ServiceArea.php +7 -0
- package/server/src/Models/ServiceRate.php +7 -17
- package/server/src/Models/TrackingNumber.php +101 -2
- package/server/src/Models/TrackingSession.php +33 -0
- package/server/src/Models/TrackingSessionGrant.php +22 -0
- package/server/src/Models/TrackingStatus.php +6 -0
- package/server/src/Models/Vehicle.php +21 -2
- package/server/src/Models/Vendor.php +7 -0
- package/server/src/Models/Waypoint.php +13 -4
- package/server/src/Notifications/OrderAssigned.php +2 -1
- package/server/src/Notifications/OrderCanceled.php +7 -1
- package/server/src/Notifications/OrderChanged.php +87 -0
- package/server/src/Notifications/OrderCompleted.php +2 -1
- package/server/src/Notifications/OrderDispatchFailed.php +2 -1
- package/server/src/Notifications/OrderDispatched.php +2 -1
- package/server/src/Notifications/OrderFailed.php +2 -1
- package/server/src/Notifications/OrderUnassigned.php +66 -0
- package/server/src/Notifications/WaypointCompleted.php +2 -1
- package/server/src/Observers/CompanyUserObserver.php +8 -7
- package/server/src/Observers/DriverObserver.php +6 -1
- package/server/src/Observers/OrderObserver.php +141 -1
- package/server/src/Observers/TrackingNumberObserver.php +18 -6
- package/server/src/Observers/UserObserver.php +2 -1
- package/server/src/Observers/VehicleObserver.php +8 -3
- package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
- package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
- package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
- package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
- package/server/src/Providers/EventServiceProvider.php +11 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
- package/server/src/Support/ApiCacheThrottle.php +76 -0
- package/server/src/Support/LiveOrderQuery.php +3 -1
- package/server/src/Support/OrderEdit.php +365 -0
- package/server/src/Support/OrderEditPolicy.php +292 -0
- package/server/src/Support/ProfileAccountManager.php +54 -1
- package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
- package/server/src/Support/SocketChannels.php +346 -0
- package/server/src/Support/Telematics/TelematicService.php +7 -2
- package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
- package/server/src/Support/TrackingChannel.php +55 -0
- package/server/src/Support/TrackingCode.php +242 -0
- package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
- package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
- package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
- package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
- package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
- package/server/src/Support/TrackingPage/TrackingView.php +543 -0
- package/server/src/Support/TrackingPublisher.php +261 -0
- package/server/src/Support/TrackingScope.php +182 -0
- package/server/src/Support/TrackingUpdate.php +402 -0
- package/server/src/Support/Utils.php +15 -3
- package/server/src/Traits/HasTrackingNumber.php +2 -0
- package/server/src/routes.php +39 -7
- package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
- package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
- package/server/tests/ApiEntityControllerContractsTest.php +84 -0
- package/server/tests/ApiOrderControllerContractsTest.php +245 -0
- package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
- package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
- package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
- package/server/tests/CompactResourceSerializationTest.php +23 -59
- package/server/tests/ControllerFilterContractsTest.php +1 -1
- package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
- package/server/tests/EventContractsTest.php +26 -60
- package/server/tests/ExpansionContractsTest.php +15 -2
- package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
- package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
- package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
- package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
- package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
- package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
- package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
- package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
- package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
- package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
- package/server/tests/IndexCustomerPhotoTest.php +151 -0
- package/server/tests/ModelAccessorContractsTest.php +4 -3
- package/server/tests/NotificationAndMailContractsTest.php +5 -8
- package/server/tests/ObserverContractsTest.php +72 -10
- package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
- package/server/tests/OrderFilterExecutionTest.php +167 -2
- package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
- package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
- package/server/tests/QueueJobContractsTest.php +2 -1
- package/server/tests/RequestContractsTest.php +1 -1
- package/server/tests/RouteRegistrationExecutionTest.php +26 -0
- package/server/tests/RouteSequencingEngineTest.php +17 -0
- package/server/tests/SettingControllerContractsTest.php +110 -0
- package/server/tests/Support/TrackingPageDatabase.php +200 -0
- package/server/tests/TenantScopingTest.php +206 -0
- package/server/tests/TrackingPageSettingsTest.php +343 -0
- package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
- package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
- package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
- package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
- package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
- package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
- package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
- package/server/tests/Unit/Models/FleetTest.php +4 -3
- package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
- package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
- package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
- package/server/tests/Unit/Models/PayloadTest.php +18 -4
- package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
- package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
- package/server/tests/Unit/Models/VehicleTest.php +16 -2
- package/server/tests/Unit/Models/WaypointTest.php +4 -3
- package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
- package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
- package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
- package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
- package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
- package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
- package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
- package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
- package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
- package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
- package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
- package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
- package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
- package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
- package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
- package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
- package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
- package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
- package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
- package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
- package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
- package/server/tests/VehicleResourceContractsTest.php +46 -0
- package/server/tests/VroomOrchestrationEngineTest.php +34 -1
- package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
- package/translations/ar-ae.yml +274 -0
- package/translations/bg-bg.yaml +274 -0
- package/translations/en-us.yaml +287 -0
- package/translations/es-pa.yaml +274 -0
- package/translations/fr-fr.yaml +274 -0
- package/translations/mn-mn.yaml +274 -0
- package/translations/pt-br.yaml +274 -0
- package/translations/ru-ru.yaml +276 -2
- package/translations/uk-ua.yaml +275 -1
- package/translations/vi-vn.yaml +274 -0
- package/addon/components/cell/contact-identity.hbs +0 -1
- package/addon/components/cell/customer-identity.hbs +0 -1
- package/addon/components/cell/driver-identity.hbs +0 -1
- package/addon/components/cell/fleet-identity.hbs +0 -1
- package/addon/components/cell/order-identity.hbs +0 -1
- package/addon/components/cell/place-identity.hbs +0 -1
- package/addon/components/cell/vehicle-identity.hbs +0 -1
- package/addon/components/cell/vendor-identity.hbs +0 -1
- package/addon/components/contact/pill.hbs +0 -24
- package/addon/components/contact/summary.hbs +0 -1
- package/addon/components/customer/pill.hbs +0 -24
- package/addon/components/customer/summary.hbs +0 -1
- package/addon/components/driver/pill.hbs +0 -24
- package/addon/components/driver/summary.hbs +0 -1
- package/addon/components/fleet/pill.hbs +0 -24
- package/addon/components/fleet/summary.hbs +0 -1
- package/addon/components/order/pill.hbs +0 -24
- package/addon/components/order/summary.hbs +0 -1
- package/addon/components/place/pill.hbs +0 -24
- package/addon/components/place/summary.hbs +0 -1
- package/addon/components/select-option/contact.hbs +0 -12
- package/addon/components/select-option/customer.hbs +0 -12
- package/addon/components/select-option/driver.hbs +0 -12
- package/addon/components/select-option/fleet.hbs +0 -12
- package/addon/components/select-option/order.hbs +0 -12
- package/addon/components/select-option/place.hbs +0 -12
- package/addon/components/select-option/vehicle.hbs +0 -12
- package/addon/components/select-option/vendor.hbs +0 -12
- package/addon/components/vehicle/pill.hbs +0 -24
- package/addon/components/vehicle/summary.hbs +0 -1
- package/addon/components/vendor/pill.hbs +0 -24
- package/addon/components/vendor/summary.hbs +0 -1
- package/app/components/cell/contact-identity.js +0 -1
- package/app/components/cell/customer-identity.js +0 -1
- package/app/components/cell/driver-identity.js +0 -1
- package/app/components/cell/fleet-identity.js +0 -1
- package/app/components/cell/order-identity.js +0 -1
- package/app/components/cell/place-identity.js +0 -1
- package/app/components/cell/vehicle-identity.js +0 -1
- package/app/components/cell/vendor-identity.js +0 -1
- package/app/components/contact/pill.js +0 -1
- package/app/components/contact/summary.js +0 -1
- package/app/components/customer/pill.js +0 -1
- package/app/components/customer/summary.js +0 -1
- package/app/components/driver/pill.js +0 -1
- package/app/components/driver/summary.js +0 -1
- package/app/components/fleet/pill.js +0 -1
- package/app/components/fleet/summary.js +0 -1
- package/app/components/order/pill.js +0 -1
- package/app/components/order/summary.js +0 -1
- package/app/components/place/pill.js +0 -1
- package/app/components/place/summary.js +0 -1
- package/app/components/select-option/contact.js +0 -1
- package/app/components/select-option/customer.js +0 -1
- package/app/components/select-option/driver.js +0 -1
- package/app/components/select-option/fleet.js +0 -1
- package/app/components/select-option/order.js +0 -1
- package/app/components/select-option/place.js +0 -1
- package/app/components/select-option/vehicle.js +0 -1
- package/app/components/select-option/vendor.js +0 -1
- package/app/components/vehicle/pill.js +0 -1
- package/app/components/vehicle/summary.js +0 -1
- package/app/components/vendor/pill.js +0 -1
- package/app/components/vendor/summary.js +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Str;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The content printed into a tracking number's QR code and barcode, and the parser that
|
|
9
|
+
* turns any scanned value back into identifiers.
|
|
10
|
+
*
|
|
11
|
+
* QR (v1) is a URL, so a phone camera opens the customer tracking page while scanners
|
|
12
|
+
* and the navigator app read the parameters:
|
|
13
|
+
*
|
|
14
|
+
* https://<console host>/~/track-order?order=<tracking number>&r=<owner public_id>&v=1
|
|
15
|
+
*
|
|
16
|
+
* Only the tracking number (printed on the label in plain text) and the owner's public
|
|
17
|
+
* id go in: no uuids, no addresses, no names, no company. The owner's type is its
|
|
18
|
+
* public_id prefix (order_, waypoint_, entity_, place_), so it is not repeated. Nothing
|
|
19
|
+
* is signed: a label can be photocopied whatever it carries, so authorization belongs
|
|
20
|
+
* to the endpoint that acts on a scan, not to the code.
|
|
21
|
+
*
|
|
22
|
+
* The linear barcode (Code 128) carries the bare tracking number, matching the text
|
|
23
|
+
* printed beneath it, because warehouse handhelds type what they scan into a field.
|
|
24
|
+
*
|
|
25
|
+
* Codes already on parcels encode the owner's bare uuid, and parse() keeps accepting
|
|
26
|
+
* them, alongside a bare tracking number or a bare public_id.
|
|
27
|
+
*/
|
|
28
|
+
class TrackingCode
|
|
29
|
+
{
|
|
30
|
+
public const VERSION = 1;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Where a phone camera lands. The public tracking page is being rebuilt; when its
|
|
34
|
+
* route changes, change these two and nothing else. parse() never depends on the
|
|
35
|
+
* host or the path, since printed labels outlive both.
|
|
36
|
+
*
|
|
37
|
+
* `~/` is the console's public route prefix: it renders the page whether or not the
|
|
38
|
+
* visitor is signed in. A bare `/track-order` falls into the console's own routes, so
|
|
39
|
+
* a signed-in user landed on an empty console page instead of the tracking page.
|
|
40
|
+
*/
|
|
41
|
+
public const PATH = '~/track-order';
|
|
42
|
+
public const TRACKING_PARAM = 'order';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The tracking page link for emails, notifications and the API: the company's own page
|
|
46
|
+
* (`~/t/{slug}/{number}`) when its settings send links there, otherwise the shared page
|
|
47
|
+
* (`~/track/{number}`). QR codes keep {@see PATH}, which labels already printed carry.
|
|
48
|
+
* Without a tracking number it links the page itself, where the number can be entered.
|
|
49
|
+
*/
|
|
50
|
+
public static function pageUrl(?string $trackingNumber, ?string $companyUuid = null): string
|
|
51
|
+
{
|
|
52
|
+
try {
|
|
53
|
+
$slug = $companyUuid ? app(TrackingPage\TrackingResolver::class)->linkSlugFor($companyUuid) : null;
|
|
54
|
+
} catch (\Throwable) {
|
|
55
|
+
$slug = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$base = $slug ? '~/t/' . rawurlencode($slug) : '~/track';
|
|
59
|
+
|
|
60
|
+
return Utils::consoleUrl(filled($trackingNumber) ? $base . '/' . rawurlencode($trackingNumber) : $base, []);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The QR content for a tracking number.
|
|
65
|
+
*/
|
|
66
|
+
public static function qrContent(string $trackingNumber, ?string $ownerPublicId = null): string
|
|
67
|
+
{
|
|
68
|
+
$query = [static::TRACKING_PARAM => $trackingNumber];
|
|
69
|
+
|
|
70
|
+
if (!empty($ownerPublicId)) {
|
|
71
|
+
$query['r'] = $ownerPublicId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$query['v'] = static::VERSION;
|
|
75
|
+
|
|
76
|
+
return static::consoleBaseUrl() . '/' . static::PATH . '?' . http_build_query($query);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The console origin, from the same config Utils::consoleUrl() reads. It does not
|
|
81
|
+
* call consoleUrl() because that infers http/https from the app environment: a code
|
|
82
|
+
* printed on a parcel should not change scheme with where it was generated, so it
|
|
83
|
+
* is https unless the console is explicitly configured otherwise.
|
|
84
|
+
*/
|
|
85
|
+
protected static function consoleBaseUrl(): string
|
|
86
|
+
{
|
|
87
|
+
$host = rtrim((string) config('fleetbase.console.host', ''), '/');
|
|
88
|
+
|
|
89
|
+
if (Str::startsWith($host, 'http')) {
|
|
90
|
+
return $host;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
$subdomain = config('fleetbase.console.subdomain');
|
|
94
|
+
$scheme = config('fleetbase.console.secure', true) ? 'https://' : 'http://';
|
|
95
|
+
|
|
96
|
+
return $scheme . ($subdomain ? $subdomain . '.' : '') . $host;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The linear barcode content for a tracking number.
|
|
101
|
+
*/
|
|
102
|
+
public static function barcodeContent(string $trackingNumber): string
|
|
103
|
+
{
|
|
104
|
+
return $trackingNumber;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Break a scanned value into the identifiers it carries.
|
|
109
|
+
*
|
|
110
|
+
* - `uuid`: a legacy code, the owner's uuid.
|
|
111
|
+
* - `tracking_number` and `public_id`: from a URL code.
|
|
112
|
+
* - `reference`: a bare value that is either a tracking number (the barcode, or one
|
|
113
|
+
* typed by hand) or a public_id. Consumers try both.
|
|
114
|
+
*
|
|
115
|
+
* @return array{uuid: ?string, tracking_number: ?string, public_id: ?string, reference: ?string, version: ?int}
|
|
116
|
+
*/
|
|
117
|
+
public static function parse(?string $code): array
|
|
118
|
+
{
|
|
119
|
+
$parsed = [
|
|
120
|
+
'uuid' => null,
|
|
121
|
+
'tracking_number' => null,
|
|
122
|
+
'public_id' => null,
|
|
123
|
+
'reference' => null,
|
|
124
|
+
'version' => null,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
$code = trim((string) $code);
|
|
128
|
+
if ($code === '') {
|
|
129
|
+
return $parsed;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (preg_match('#^https?://#i', $code)) {
|
|
133
|
+
return array_merge($parsed, static::parseUrl($code));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Str::isUuid($code)) {
|
|
137
|
+
$parsed['uuid'] = $code;
|
|
138
|
+
|
|
139
|
+
return $parsed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
$parsed['reference'] = $code;
|
|
143
|
+
|
|
144
|
+
return $parsed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether a scanned value identifies the given subject (an order, waypoint, entity or
|
|
149
|
+
* place). The value comes straight from a request, so anything but a string, such as
|
|
150
|
+
* a scanner library's whole result object, matches nothing.
|
|
151
|
+
*/
|
|
152
|
+
public static function matches($code, $subject): bool
|
|
153
|
+
{
|
|
154
|
+
$subjectUuid = data_get($subject, 'uuid');
|
|
155
|
+
if (!is_string($code) || !$subject || empty($subjectUuid)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Labels printed before v1 encode the subject's bare uuid; compare it exactly, as
|
|
160
|
+
// scans always have been.
|
|
161
|
+
if (trim($code) === $subjectUuid) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$parsed = static::parse($code);
|
|
166
|
+
|
|
167
|
+
if ($parsed['uuid']) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
$publicId = data_get($subject, 'public_id');
|
|
172
|
+
$trackingNumber = static::subjectTrackingNumber($subject);
|
|
173
|
+
|
|
174
|
+
if ($parsed['reference']) {
|
|
175
|
+
return static::sameTrackingNumber($parsed['reference'], $trackingNumber) || static::samePublicId($parsed['reference'], $publicId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A url code names its owner by public id, and that decides: an owner can hold more
|
|
179
|
+
// than one tracking number (the API issues extra ones), so the number is compared
|
|
180
|
+
// only when the code names no owner.
|
|
181
|
+
if ($parsed['public_id']) {
|
|
182
|
+
return static::samePublicId($parsed['public_id'], $publicId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return $parsed['tracking_number'] !== null && static::sameTrackingNumber($parsed['tracking_number'], $trackingNumber);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @return array{tracking_number: ?string, public_id: ?string, version: ?int}
|
|
190
|
+
*/
|
|
191
|
+
protected static function parseUrl(string $url): array
|
|
192
|
+
{
|
|
193
|
+
$parts = parse_url($url);
|
|
194
|
+
$query = [];
|
|
195
|
+
parse_str(is_array($parts) ? ($parts['query'] ?? '') : '', $query);
|
|
196
|
+
|
|
197
|
+
$trackingNumber = static::stringParam($query, static::TRACKING_PARAM) ?? static::stringParam($query, 'tn');
|
|
198
|
+
|
|
199
|
+
// A future route may carry the tracking number in the path (/track/<number>). Only
|
|
200
|
+
// a path of two or more segments can, and the tracking page's own path
|
|
201
|
+
// (/track-order, /~/track-order) is never mistaken for one.
|
|
202
|
+
if ($trackingNumber === null && is_array($parts) && isset($parts['path'])) {
|
|
203
|
+
$segments = array_values(array_filter(explode('/', $parts['path']), fn ($segment) => $segment !== ''));
|
|
204
|
+
if (count($segments) >= 2 && end($segments) !== basename(static::PATH)) {
|
|
205
|
+
$trackingNumber = rawurldecode(end($segments));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
$version = static::stringParam($query, 'v');
|
|
210
|
+
|
|
211
|
+
return [
|
|
212
|
+
'tracking_number' => $trackingNumber,
|
|
213
|
+
'public_id' => static::stringParam($query, 'r'),
|
|
214
|
+
'version' => $version !== null && ctype_digit($version) ? (int) $version : null,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
protected static function stringParam(array $query, string $key): ?string
|
|
219
|
+
{
|
|
220
|
+
$value = $query[$key] ?? null;
|
|
221
|
+
|
|
222
|
+
return is_string($value) && trim($value) !== '' ? trim($value) : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
protected static function subjectTrackingNumber($subject): ?string
|
|
226
|
+
{
|
|
227
|
+
$trackingNumber = data_get($subject, 'trackingNumber.tracking_number') ?? data_get($subject, 'tracking_number');
|
|
228
|
+
|
|
229
|
+
return is_string($trackingNumber) && $trackingNumber !== '' ? $trackingNumber : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected static function sameTrackingNumber(string $scanned, ?string $trackingNumber): bool
|
|
233
|
+
{
|
|
234
|
+
// The label prints the number upper-cased and handhelds may not preserve case.
|
|
235
|
+
return $trackingNumber !== null && strcasecmp($scanned, $trackingNumber) === 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
protected static function samePublicId(string $scanned, $publicId): bool
|
|
239
|
+
{
|
|
240
|
+
return is_string($publicId) && $publicId !== '' && $scanned === $publicId;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\TrackingPage;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\TrackingSession;
|
|
6
|
+
use Fleetbase\FleetOps\Models\TrackingSessionGrant;
|
|
7
|
+
use Fleetbase\FleetOps\Support\TrackingScope;
|
|
8
|
+
use Fleetbase\Models\VerificationCode;
|
|
9
|
+
use Fleetbase\Scopes\ExpiryScope;
|
|
10
|
+
use Illuminate\Cache\RateLimiter;
|
|
11
|
+
use Illuminate\Http\Request;
|
|
12
|
+
use Illuminate\Support\Carbon;
|
|
13
|
+
use Illuminate\Support\Str;
|
|
14
|
+
use Symfony\Component\HttpFoundation\Cookie;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One-time codes and device sessions for the public tracking page.
|
|
18
|
+
*
|
|
19
|
+
* A code is sent to a scope's recipient and proves the visitor is that customer. A correct
|
|
20
|
+
* code gives the device a grant for that one customer within that one order. Codes are
|
|
21
|
+
* stored hashed (VerificationCode::issue()), three wrong codes pause the scope for every
|
|
22
|
+
* device, and sends are limited per scope and per address.
|
|
23
|
+
*/
|
|
24
|
+
class TrackingAccess
|
|
25
|
+
{
|
|
26
|
+
public const COOKIE = 'fb_track';
|
|
27
|
+
public const CODE_PURPOSE = 'fleetops_tracking_access';
|
|
28
|
+
public const CODE_MINUTES = 10;
|
|
29
|
+
public const MAX_ATTEMPTS = 3;
|
|
30
|
+
public const PAUSE_SECONDS = 300;
|
|
31
|
+
public const RESEND_SECONDS = 30;
|
|
32
|
+
public const SENDS_PER_SCOPE = 3;
|
|
33
|
+
public const SENDS_PER_IP = 5;
|
|
34
|
+
public const SEND_WINDOW = 900;
|
|
35
|
+
public const LOOKUPS_PER_IP = 30;
|
|
36
|
+
public const LOOKUP_WINDOW = 60;
|
|
37
|
+
public const VERIFIES_PER_IP = 10;
|
|
38
|
+
public const REPORTS_PER_HOUR = 3;
|
|
39
|
+
|
|
40
|
+
public const SEND_SENT = 'sent';
|
|
41
|
+
public const SEND_COOLDOWN = 'cooldown';
|
|
42
|
+
public const SEND_LIMITED = 'limited';
|
|
43
|
+
public const SEND_PAUSED = 'paused';
|
|
44
|
+
|
|
45
|
+
public function __construct(protected ?RateLimiter $limiter = null)
|
|
46
|
+
{
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether this address may make another lookup, counting it.
|
|
51
|
+
*/
|
|
52
|
+
public function allowLookup(Request $request): bool
|
|
53
|
+
{
|
|
54
|
+
return $this->attempt('trk-lookup:' . $request->ip(), self::LOOKUPS_PER_IP, self::LOOKUP_WINDOW);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether this address may check another code, counting it.
|
|
59
|
+
*/
|
|
60
|
+
public function allowVerify(Request $request): bool
|
|
61
|
+
{
|
|
62
|
+
return $this->attempt('trk-verify:' . $request->ip(), self::VERIFIES_PER_IP, self::LOOKUP_WINDOW);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether this address may report another problem with this order, counting it.
|
|
67
|
+
*/
|
|
68
|
+
public function allowReport(Request $request, string $orderUuid): bool
|
|
69
|
+
{
|
|
70
|
+
return $this->attempt('trk-report:' . $orderUuid . ':' . $request->ip(), self::REPORTS_PER_HOUR, 3600);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether a scope's codes are paused after too many wrong ones, and for how long.
|
|
75
|
+
*/
|
|
76
|
+
public function pausedFor(TrackingScope $scope): int
|
|
77
|
+
{
|
|
78
|
+
$key = $this->pauseKey($scope);
|
|
79
|
+
|
|
80
|
+
return $this->limiter()->tooManyAttempts($key, 1) ? max(1, $this->limiter()->availableIn($key)) : 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check the limits for sending a code and, when they allow it, count the send.
|
|
85
|
+
*
|
|
86
|
+
* @return array{status: string, retry_after: int}
|
|
87
|
+
*/
|
|
88
|
+
public function claimSend(TrackingScope $scope, Request $request): array
|
|
89
|
+
{
|
|
90
|
+
if ($paused = $this->pausedFor($scope)) {
|
|
91
|
+
return ['status' => self::SEND_PAUSED, 'retry_after' => $paused];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
$cooldown = 'trk-resend:' . $scope->key();
|
|
95
|
+
if ($this->limiter()->tooManyAttempts($cooldown, 1)) {
|
|
96
|
+
return ['status' => self::SEND_COOLDOWN, 'retry_after' => max(1, $this->limiter()->availableIn($cooldown))];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
foreach (['trk-send-scope:' . $scope->key() => self::SENDS_PER_SCOPE, 'trk-send-ip:' . $request->ip() => self::SENDS_PER_IP] as $key => $max) {
|
|
100
|
+
if ($this->limiter()->tooManyAttempts($key, $max)) {
|
|
101
|
+
return ['status' => self::SEND_LIMITED, 'retry_after' => max(1, $this->limiter()->availableIn($key))];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$this->limiter()->hit($cooldown, self::RESEND_SECONDS);
|
|
106
|
+
$this->limiter()->hit('trk-send-scope:' . $scope->key(), self::SEND_WINDOW);
|
|
107
|
+
$this->limiter()->hit('trk-send-ip:' . $request->ip(), self::SEND_WINDOW);
|
|
108
|
+
|
|
109
|
+
return ['status' => self::SEND_SENT, 'retry_after' => self::RESEND_SECONDS];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A new code for a scope, voiding the scope's earlier unused ones. The plain code is on `plainCode`.
|
|
114
|
+
*/
|
|
115
|
+
public function issueCode(TrackingScope $scope, mixed $recipient, string $channel): VerificationCode
|
|
116
|
+
{
|
|
117
|
+
$this->voidCodes($scope);
|
|
118
|
+
|
|
119
|
+
return $this->createCode($recipient, [
|
|
120
|
+
'expireAfter' => Carbon::now()->addMinutes(self::CODE_MINUTES),
|
|
121
|
+
'meta' => [
|
|
122
|
+
'scope' => $scope->key(),
|
|
123
|
+
'channel' => $channel,
|
|
124
|
+
'company_uuid' => $scope->company_uuid,
|
|
125
|
+
],
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check a code for a scope.
|
|
131
|
+
*
|
|
132
|
+
* @return array{status: string, attempts_left?: int, channel?: string|null}
|
|
133
|
+
*/
|
|
134
|
+
public function checkCode(TrackingScope $scope, string $code): array
|
|
135
|
+
{
|
|
136
|
+
$verificationCode = $this->latestCode($scope);
|
|
137
|
+
if (!$verificationCode) {
|
|
138
|
+
return ['status' => VerificationCode::CHECK_EXPIRED];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$result = $verificationCode->check($code, self::MAX_ATTEMPTS);
|
|
142
|
+
if ($result === VerificationCode::CHECK_VALID) {
|
|
143
|
+
$channel = $verificationCode->getMeta('channel');
|
|
144
|
+
$this->consumeCode($verificationCode);
|
|
145
|
+
|
|
146
|
+
return ['status' => $result, 'channel' => $channel];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if ($result === VerificationCode::CHECK_LOCKED) {
|
|
150
|
+
$this->limiter()->hit($this->pauseKey($scope), self::PAUSE_SECONDS);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return ['status' => $result, 'attempts_left' => $verificationCode->attemptsLeft(self::MAX_ATTEMPTS)];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The device session the request's cookie names, when it is live.
|
|
158
|
+
*/
|
|
159
|
+
public function session(Request $request): ?TrackingSession
|
|
160
|
+
{
|
|
161
|
+
$token = $request->cookie(self::COOKIE);
|
|
162
|
+
if (!is_string($token) || strlen($token) < 32) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
$session = $this->findSession(hash('sha256', $token));
|
|
167
|
+
if (!$session || $session->revoked_at || ($session->expires_at && $session->expires_at->isPast())) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return $session;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Whether the request's device holds a live grant for this scope.
|
|
176
|
+
*/
|
|
177
|
+
public function hasGrant(Request $request, ?TrackingScope $scope): bool
|
|
178
|
+
{
|
|
179
|
+
if (!$scope) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
$session = $this->session($request);
|
|
184
|
+
|
|
185
|
+
return $session !== null && $this->findGrant($session, $scope) !== null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* When the device's grant for this scope ends.
|
|
190
|
+
*/
|
|
191
|
+
public function grantExpiresAt(Request $request, TrackingScope $scope): ?string
|
|
192
|
+
{
|
|
193
|
+
$session = $this->session($request);
|
|
194
|
+
$grant = $session ? $this->findGrant($session, $scope) : null;
|
|
195
|
+
|
|
196
|
+
return $grant?->expires_at?->toISOString();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Give the device a grant for a scope, starting a session when it has none.
|
|
201
|
+
*
|
|
202
|
+
* @return Cookie|null the cookie to set when a new session was started
|
|
203
|
+
*/
|
|
204
|
+
public function grant(Request $request, TrackingScope $scope, ?string $channel, int $hours): ?Cookie
|
|
205
|
+
{
|
|
206
|
+
$expiresAt = Carbon::now()->addHours($hours);
|
|
207
|
+
$session = $this->session($request);
|
|
208
|
+
$cookie = null;
|
|
209
|
+
|
|
210
|
+
if (!$session) {
|
|
211
|
+
$token = Str::random(40);
|
|
212
|
+
$session = $this->createSession([
|
|
213
|
+
'token_hash' => hash('sha256', $token),
|
|
214
|
+
'ip' => $request->ip(),
|
|
215
|
+
'user_agent' => Str::limit((string) $request->userAgent(), 250, ''),
|
|
216
|
+
'last_seen_at' => Carbon::now(),
|
|
217
|
+
'expires_at' => $expiresAt,
|
|
218
|
+
]);
|
|
219
|
+
$cookie = $this->cookie($token, $hours * 60);
|
|
220
|
+
} elseif (!$session->expires_at || $session->expires_at->lt($expiresAt)) {
|
|
221
|
+
$this->saveSession($session, ['expires_at' => $expiresAt, 'last_seen_at' => Carbon::now()]);
|
|
222
|
+
$cookie = $this->cookie((string) $request->cookie(self::COOKIE), $hours * 60);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
$this->saveGrant($session, $scope, $channel, $expiresAt);
|
|
226
|
+
|
|
227
|
+
return $cookie;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sign the device out: of one scope, or of every scope when none is given.
|
|
232
|
+
*
|
|
233
|
+
* @return Cookie|null an expired cookie when the whole session ended
|
|
234
|
+
*/
|
|
235
|
+
public function revoke(Request $request, ?TrackingScope $scope = null): ?Cookie
|
|
236
|
+
{
|
|
237
|
+
$session = $this->session($request);
|
|
238
|
+
if (!$session) {
|
|
239
|
+
return $this->cookie('', -1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ($scope) {
|
|
243
|
+
$this->deleteGrant($session, $scope);
|
|
244
|
+
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
$this->saveSession($session, ['revoked_at' => Carbon::now()]);
|
|
249
|
+
|
|
250
|
+
return $this->cookie('', -1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public function cookie(string $value, int $minutes): Cookie
|
|
254
|
+
{
|
|
255
|
+
$sameSite = strtolower((string) config('fleetops.tracking.cookie_same_site', 'lax'));
|
|
256
|
+
$sameSite = in_array($sameSite, ['lax', 'strict', 'none'], true) ? $sameSite : 'lax';
|
|
257
|
+
$secure = $sameSite === 'none' || (bool) config('session.secure', false);
|
|
258
|
+
|
|
259
|
+
return Cookie::create(self::COOKIE, $value, $minutes < 0 ? 1 : Carbon::now()->addMinutes($minutes)->getTimestamp(), '/', config('session.domain'), $secure, true, false, $sameSite);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
protected function attempt(string $key, int $max, int $decay): bool
|
|
263
|
+
{
|
|
264
|
+
if ($this->limiter()->tooManyAttempts($key, $max)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
$this->limiter()->hit($key, $decay);
|
|
269
|
+
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
protected function pauseKey(TrackingScope $scope): string
|
|
274
|
+
{
|
|
275
|
+
return 'trk-pause:' . $scope->key();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
protected function limiter(): RateLimiter
|
|
279
|
+
{
|
|
280
|
+
return $this->limiter ??= app(RateLimiter::class);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
protected function voidCodes(TrackingScope $scope): void
|
|
284
|
+
{
|
|
285
|
+
VerificationCode::withoutGlobalScope(ExpiryScope::class)->where('for', self::CODE_PURPOSE)->where('meta->scope', $scope->key())->delete();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
protected function createCode(mixed $recipient, array $options): VerificationCode
|
|
289
|
+
{
|
|
290
|
+
return VerificationCode::issue($recipient, self::CODE_PURPOSE, $options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The scope's newest code that has not been used. Read without the expiry scope so an
|
|
295
|
+
* expired code is reported as such rather than as missing; a used code stays on the
|
|
296
|
+
* table for the ledger but never verifies again.
|
|
297
|
+
*/
|
|
298
|
+
protected function latestCode(TrackingScope $scope): ?VerificationCode
|
|
299
|
+
{
|
|
300
|
+
return VerificationCode::withoutGlobalScope(ExpiryScope::class)
|
|
301
|
+
->where('for', self::CODE_PURPOSE)
|
|
302
|
+
->where('meta->scope', $scope->key())
|
|
303
|
+
->where(function ($query) {
|
|
304
|
+
$query->whereNull('status')->orWhere('status', '!=', VerificationCode::STATUS_USED);
|
|
305
|
+
})
|
|
306
|
+
->latest()
|
|
307
|
+
->first();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected function consumeCode(VerificationCode $verificationCode): void
|
|
311
|
+
{
|
|
312
|
+
$verificationCode->consume();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected function findSession(string $tokenHash): ?TrackingSession
|
|
316
|
+
{
|
|
317
|
+
return TrackingSession::where('token_hash', $tokenHash)->first();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
protected function createSession(array $attributes): TrackingSession
|
|
321
|
+
{
|
|
322
|
+
return TrackingSession::create($attributes);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
protected function saveSession(TrackingSession $session, array $attributes): void
|
|
326
|
+
{
|
|
327
|
+
$session->forceFill($attributes)->save();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
protected function findGrant(TrackingSession $session, TrackingScope $scope): ?TrackingSessionGrant
|
|
331
|
+
{
|
|
332
|
+
return TrackingSessionGrant::where('tracking_session_uuid', $session->uuid)
|
|
333
|
+
->where('order_uuid', $scope->order_uuid)
|
|
334
|
+
->where('customer_type', $scope->customer_type)
|
|
335
|
+
->where('customer_uuid', $scope->customer_uuid)
|
|
336
|
+
->where('expires_at', '>', Carbon::now())
|
|
337
|
+
->first();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
protected function saveGrant(TrackingSession $session, TrackingScope $scope, ?string $channel, Carbon $expiresAt): void
|
|
341
|
+
{
|
|
342
|
+
TrackingSessionGrant::updateOrCreate(
|
|
343
|
+
['tracking_session_uuid' => $session->uuid, 'order_uuid' => $scope->order_uuid, 'customer_type' => $scope->customer_type, 'customer_uuid' => $scope->customer_uuid],
|
|
344
|
+
['company_uuid' => $scope->company_uuid, 'channel' => $channel, 'expires_at' => $expiresAt]
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected function deleteGrant(TrackingSession $session, TrackingScope $scope): void
|
|
349
|
+
{
|
|
350
|
+
TrackingSessionGrant::where('tracking_session_uuid', $session->uuid)
|
|
351
|
+
->where('order_uuid', $scope->order_uuid)
|
|
352
|
+
->where('customer_type', $scope->customer_type)
|
|
353
|
+
->where('customer_uuid', $scope->customer_uuid)
|
|
354
|
+
->delete();
|
|
355
|
+
}
|
|
356
|
+
}
|