@fleetbase/fleetops-engine 0.6.67 → 0.6.69
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 +11 -15
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/docs/SAFEE.md +5 -0
- package/docs/TELEMATICS_QUEUES.md +59 -8
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/fuel-providers.php +53 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Telematics/TelematicService.php +6 -4
- package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/translations/en-us.yaml +37 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
|
@@ -51,28 +51,56 @@ no worker wait indefinitely.
|
|
|
51
51
|
|
|
52
52
|
### 1. Add telematics workers
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
Each telematics worker must be an exact copy of the existing `queue` worker: the
|
|
55
|
+
same image and tag, the same `env_file`, `environment`, volumes and `depends_on`,
|
|
56
|
+
including every Compose override file that changes `queue`. Change only the command.
|
|
57
|
+
Workers running a different build, or missing `APP_KEY`, fail every telematics job.
|
|
58
|
+
|
|
59
|
+
Docker Compose example, assuming `queue` is defined in `docker-compose.yml` and any
|
|
60
|
+
image or environment overrides for it live in `docker-compose.override.yml`:
|
|
56
61
|
|
|
57
62
|
```yaml
|
|
63
|
+
# docker-compose.yml: copy the full `queue` service definition, then change the command.
|
|
58
64
|
services:
|
|
59
65
|
telematics-queue:
|
|
60
|
-
image:
|
|
66
|
+
# image, env_file, environment, volumes, depends_on: identical to `queue`
|
|
61
67
|
command: ["php", "artisan", "queue:work", "--queue=telematics", "--sleep=1", "--max-time=3600"]
|
|
62
68
|
restart: unless-stopped
|
|
63
69
|
deploy:
|
|
64
70
|
replicas: 2
|
|
65
|
-
# environment, volumes and depends_on: copy from the existing `queue` service
|
|
66
71
|
|
|
67
72
|
telematics-broadcast-queue:
|
|
68
|
-
image:
|
|
73
|
+
# image, env_file, environment, volumes, depends_on: identical to `queue`
|
|
69
74
|
command: ["php", "artisan", "queue:work", "--queue=telematics-broadcasts", "--sleep=1", "--max-time=3600"]
|
|
70
75
|
restart: unless-stopped
|
|
71
|
-
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
# docker-compose.override.yml: repeat every override that applies to `queue`.
|
|
80
|
+
services:
|
|
81
|
+
telematics-queue:
|
|
82
|
+
image: "fleetbase-api:latest" # the same image as `queue`
|
|
83
|
+
telematics-broadcast-queue:
|
|
84
|
+
image: "fleetbase-api:latest"
|
|
72
85
|
```
|
|
73
86
|
|
|
74
87
|
Notes:
|
|
75
88
|
|
|
89
|
+
- **Use the same image as `queue`, not a separate `fleetbase/fleetbase-api:latest`.**
|
|
90
|
+
Docker does not download a newer `latest` when a copy already exists locally, so a
|
|
91
|
+
new service can start from an old cached image. If you do use a registry image,
|
|
92
|
+
pin a version tag and pull it for every API container together.
|
|
93
|
+
- **Every API container needs the same `APP_KEY`**: application, scheduler, the
|
|
94
|
+
default worker and the telematics workers. Provider credentials, token caches and
|
|
95
|
+
telematics deliveries are encrypted with it. If only the application service
|
|
96
|
+
receives it (for example through a mounted `.env` or an override), put it in a
|
|
97
|
+
shared `env_file` instead. Never generate a new key for a worker.
|
|
98
|
+
- **Check the merged configuration, including when using `extends`.** Run
|
|
99
|
+
`docker compose config` and compare the resolved `image`, `env_file` and
|
|
100
|
+
`environment` of `queue` and each telematics service before deploying.
|
|
101
|
+
- The `--queue` names must match the variables in step 2 exactly. A worker listening
|
|
102
|
+
on `telematics-broadcast` while broadcasts go to `telematics-broadcasts` receives
|
|
103
|
+
nothing.
|
|
76
104
|
- Leave the existing `queue` service unchanged. `queue:work` without `--queue`
|
|
77
105
|
processes the connection's default queue (`REDIS_QUEUE`, normally `default`).
|
|
78
106
|
- Leave `--timeout` at its default (60 seconds) or set any value below the Redis
|
|
@@ -83,8 +111,6 @@ Notes:
|
|
|
83
111
|
memory. `restart: unless-stopped` starts them again.
|
|
84
112
|
- On Kubernetes, ECS, or another platform, create equivalent deployments with the
|
|
85
113
|
same image, environment, and commands.
|
|
86
|
-
- Every worker container needs the same `APP_KEY`, database, Redis, and broadcasting
|
|
87
|
-
settings as the application. Deliveries are encrypted with `APP_KEY`.
|
|
88
114
|
|
|
89
115
|
### 2. Set the queue variables for this instance only
|
|
90
116
|
|
|
@@ -154,6 +180,19 @@ of 50 requests per second per account across all workers.
|
|
|
154
180
|
|
|
155
181
|
## Verification
|
|
156
182
|
|
|
183
|
+
First confirm every API container runs the same build and application key. Each line
|
|
184
|
+
must show the same key fingerprint (a hash, never the key) and Fleet-Ops version:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
for c in $(docker ps --format '{{.Names}}' | grep -E 'application|scheduler|queue'); do echo "== $c $(docker inspect --format '{{.Config.Image}}' $c)"; docker exec $c php artisan tinker --execute="echo config('app.key') ? substr(hash('sha256', config('app.key')), 0, 12) : 'MISSING', ' ', json_decode(file_get_contents('vendor/fleetbase/fleetops-api/composer.json'))->version, PHP_EOL;"; done
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Confirm Laravel loaded the queue names in the application and a telematics worker:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
php artisan config:show telematics.telemetry
|
|
194
|
+
```
|
|
195
|
+
|
|
157
196
|
Queue depth should stay close to zero between minute ticks:
|
|
158
197
|
|
|
159
198
|
```bash
|
|
@@ -168,6 +207,18 @@ Also verify:
|
|
|
168
207
|
on the telematics workers, and broadcast jobs only on the broadcast worker.
|
|
169
208
|
- Other jobs on `default`, such as notifications and order events, start promptly
|
|
170
209
|
again.
|
|
210
|
+
- The connection's status and "Last sync" update after each completed scheduled
|
|
211
|
+
sweep. Clicking "Sync Devices" while a sweep is queued or running records the
|
|
212
|
+
request, and the next sweep completes it.
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
| Symptom | Cause |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| Telematics worker logs show `MissingAppKeyException` | The worker has no `APP_KEY`. Give every API container the same key and recreate the workers. |
|
|
219
|
+
| A worker reports a different Fleet-Ops version | The worker uses a different or stale cached image. Use the `queue` service's image. |
|
|
220
|
+
| Broadcast jobs are never processed | The broadcast worker's `--queue` name does not match `TELEMATICS_BROADCAST_QUEUE`. |
|
|
221
|
+
| Scheduled sync commands report `Queued 0 telematics sync job(s)` for an enabled connection | A poll job failed on a worker that could not load it, leaving its one-hour uniqueness lock. It expires on its own; after fixing the workers you can release it with `Cache::lock('laravel_unique_job:Fleetbase\\FleetOps\\Jobs\\PollTelematicTelemetry' . $telematicUuid)->forceRelease()`. |
|
|
171
222
|
|
|
172
223
|
## Rollback
|
|
173
224
|
|
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/fleetops-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.69",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "fleet-ops"
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@babel/core": "^7.23.2",
|
|
47
47
|
"@fleetbase/ember-core": "^0.3.24",
|
|
48
|
-
"@fleetbase/ember-ui": "^0.4.
|
|
49
|
-
"@fleetbase/fleetops-data": "^0.2.
|
|
48
|
+
"@fleetbase/ember-ui": "^0.4.3",
|
|
49
|
+
"@fleetbase/fleetops-data": "^0.2.2",
|
|
50
50
|
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
|
51
51
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
52
52
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
|
@@ -382,6 +382,18 @@ if (!interface_exists('Fleetbase\Ai\Contracts\AIActionCapabilityInterface')) {
|
|
|
382
382
|
eval('namespace Fleetbase\Ai\Contracts; interface AIActionCapabilityInterface {}');
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
if (!interface_exists('Fleetbase\Ai\Contracts\AIToolCapabilityInterface')) {
|
|
386
|
+
eval('namespace Fleetbase\Ai\Contracts; interface AIToolCapabilityInterface {}');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!class_exists('Fleetbase\Ai\Support\AiAudience')) {
|
|
390
|
+
eval('namespace Fleetbase\Ai\Support; class AiAudience { public function __construct(public bool $isSystemAdmin = false, public array $granted = []) {} public function can(string $permission): bool { return $this->isSystemAdmin || in_array($permission, $this->granted, true); } public function canAll(array $permissions): bool { foreach ($permissions as $permission) { if (!$this->can($permission)) { return false; } } return true; } }');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!class_exists('Fleetbase\Ai\Support\AiToolContext')) {
|
|
394
|
+
eval('namespace Fleetbase\Ai\Support; class AiToolContext { public array $actionPreviews = []; public array $uiActions = []; public function __construct(public $task, public $audience) {} public function addActionPreview($capability, array $preview): array { $preview = array_merge(["preview_id" => "preview-" . (count($this->actionPreviews) + 1), "key" => $capability->key()], $preview); $this->actionPreviews[] = $preview; return $preview; } }');
|
|
395
|
+
}
|
|
396
|
+
|
|
385
397
|
if (!class_exists('Fleetbase\Ai\Models\AiTask')) {
|
|
386
398
|
eval('namespace Fleetbase\Ai\Models; class AiTask { public function __construct(array $attributes = []) { foreach ($attributes as $key => $value) { $this->{$key} = $value; } } }');
|
|
387
399
|
}
|
|
@@ -55,9 +55,62 @@ return [
|
|
|
55
55
|
'auth_type' => 'ws_sk_header',
|
|
56
56
|
'supported_auth_types' => ['ws_sk_header', 'bearer_token'],
|
|
57
57
|
'base_urls' => \Fleetbase\FleetOps\Support\FuelProviders\Providers\PetroAppFuelProvider::BASE_URLS,
|
|
58
|
+
'environment_labels' => ['sandbox' => 'Sandbox (PetroApp staging)', 'production' => 'Production'],
|
|
58
59
|
'pagination' => 'page',
|
|
59
60
|
'default_currency' => 'SAR',
|
|
60
61
|
],
|
|
61
62
|
],
|
|
63
|
+
[
|
|
64
|
+
'key' => 'sasco',
|
|
65
|
+
'label' => 'SASCO',
|
|
66
|
+
'type' => 'native',
|
|
67
|
+
'category' => 'Fuel card integration',
|
|
68
|
+
'icon' => 'gas-pump',
|
|
69
|
+
'driver_class' => \Fleetbase\FleetOps\Support\FuelProviders\Providers\SascoFuelProvider::class,
|
|
70
|
+
'description' => 'SASCO B2B fleet fuel transactions and vehicles.',
|
|
71
|
+
'required_fields' => [
|
|
72
|
+
[
|
|
73
|
+
'name' => 'username',
|
|
74
|
+
'label' => 'B2B admin username',
|
|
75
|
+
'type' => 'text',
|
|
76
|
+
'help_text' => 'The SASCO B2B company admin login, usually an email address.',
|
|
77
|
+
'required' => true,
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'name' => 'password',
|
|
81
|
+
'label' => 'B2B admin password',
|
|
82
|
+
'type' => 'password',
|
|
83
|
+
'help_text' => 'Used to request a short-lived access token from SASCO. Fleetbase re-authenticates automatically when it expires.',
|
|
84
|
+
'required' => true,
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
'name' => 'base_url',
|
|
88
|
+
'label' => 'Base URL override (optional)',
|
|
89
|
+
'type' => 'url',
|
|
90
|
+
'help_text' => 'Leave blank to use the selected environment. A custom URL overrides the environment selection.',
|
|
91
|
+
'required' => false,
|
|
92
|
+
],
|
|
93
|
+
],
|
|
94
|
+
'capabilities' => ['vehicles', 'transactions'],
|
|
95
|
+
'sync_defaults' => [
|
|
96
|
+
'window_days' => 7,
|
|
97
|
+
'matching_order' => ['plate_number', 'internal_id', 'vin', 'serial_number', 'call_sign', 'fuel_card_number', 'trip_number'],
|
|
98
|
+
'auto_create_fuel_reports' => true,
|
|
99
|
+
],
|
|
100
|
+
'setup_instructions' => [
|
|
101
|
+
'Request B2B API access from SASCO and obtain the company admin username and password for QC/UAT and Production.',
|
|
102
|
+
'Select Sandbox for the SASCO QC environment, enter the credentials, and run Test Connection.',
|
|
103
|
+
'SASCO transactions carry the vehicle plate only, so make sure Fleetbase vehicle plate numbers use the letters-then-digits format (for example NXD 1240).',
|
|
104
|
+
'Sync a small date window first to verify imported fuel transactions and vehicle matches.',
|
|
105
|
+
],
|
|
106
|
+
'metadata' => [
|
|
107
|
+
'auth_type' => 'username_password',
|
|
108
|
+
'supported_auth_types' => ['username_password'],
|
|
109
|
+
'base_urls' => \Fleetbase\FleetOps\Support\FuelProviders\Providers\SascoFuelProvider::BASE_URLS,
|
|
110
|
+
'environment_labels' => ['sandbox' => 'Sandbox (SASCO QC)', 'production' => 'Production'],
|
|
111
|
+
'pagination' => 'current_page',
|
|
112
|
+
'default_currency' => 'SAR',
|
|
113
|
+
],
|
|
114
|
+
],
|
|
62
115
|
],
|
|
63
116
|
];
|
package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Support\Facades\DB;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Driver and customer login accounts are now managed by their FleetOps
|
|
9
|
+
* profile (user type `driver` or `customer`) instead of in IAM.
|
|
10
|
+
*
|
|
11
|
+
* Before this change the driver form created plain `user` accounts holding only
|
|
12
|
+
* the Driver role, so these accounts show up in IAM and can sign in to the
|
|
13
|
+
* console. This converts them to managed accounts. An account is converted only
|
|
14
|
+
* when it is clearly a profile login and nothing else: it has a driver or
|
|
15
|
+
* customer profile, every organization role it holds is the profile's role, it
|
|
16
|
+
* has no direct permissions or policies, and it owns no organization. Anything
|
|
17
|
+
* else is left as a team member.
|
|
18
|
+
*
|
|
19
|
+
* The previous type is kept in `meta.previous_type` so down() can revert it.
|
|
20
|
+
*/
|
|
21
|
+
return new class extends Migration {
|
|
22
|
+
private const PROFILE_ROLES = [
|
|
23
|
+
'driver' => 'Driver',
|
|
24
|
+
'customer' => 'Fleet-Ops Customer',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
public function up(): void
|
|
28
|
+
{
|
|
29
|
+
if (!$this->hasRequiredTables()) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DB::table('users')
|
|
34
|
+
->select(['uuid', 'meta'])
|
|
35
|
+
->where('type', 'user')
|
|
36
|
+
->whereNull('deleted_at')
|
|
37
|
+
->where(function ($query) {
|
|
38
|
+
$query->whereExists(function ($query) {
|
|
39
|
+
$query->selectRaw(1)->from('drivers')->whereColumn('drivers.user_uuid', 'users.uuid')->whereNull('drivers.deleted_at');
|
|
40
|
+
})->orWhereExists(function ($query) {
|
|
41
|
+
$query->selectRaw(1)->from('contacts')->whereColumn('contacts.user_uuid', 'users.uuid')->where('contacts.type', 'customer')->whereNull('contacts.deleted_at');
|
|
42
|
+
});
|
|
43
|
+
})
|
|
44
|
+
->whereNotExists(function ($query) {
|
|
45
|
+
$query->selectRaw(1)->from('companies')->whereColumn('companies.owner_uuid', 'users.uuid');
|
|
46
|
+
})
|
|
47
|
+
->orderBy('uuid')
|
|
48
|
+
->chunk(500, function ($users) {
|
|
49
|
+
foreach ($users as $user) {
|
|
50
|
+
$type = $this->managedTypeFor($user->uuid);
|
|
51
|
+
if (!$type) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$meta = json_decode($user->meta ?? '', true);
|
|
56
|
+
$meta = is_array($meta) ? $meta : [];
|
|
57
|
+
$meta['previous_type'] = 'user';
|
|
58
|
+
|
|
59
|
+
DB::table('users')->where('uuid', $user->uuid)->update(['type' => $type, 'meta' => json_encode($meta)]);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public function down(): void
|
|
65
|
+
{
|
|
66
|
+
if (!Schema::hasTable('users')) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
DB::table('users')
|
|
71
|
+
->select(['uuid', 'meta'])
|
|
72
|
+
->whereIn('type', array_keys(self::PROFILE_ROLES))
|
|
73
|
+
->where('meta', 'like', '%"previous_type"%')
|
|
74
|
+
->orderBy('uuid')
|
|
75
|
+
->chunk(500, function ($users) {
|
|
76
|
+
foreach ($users as $user) {
|
|
77
|
+
$meta = json_decode($user->meta ?? '', true);
|
|
78
|
+
if (!is_array($meta) || ($meta['previous_type'] ?? null) !== 'user') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
unset($meta['previous_type']);
|
|
83
|
+
DB::table('users')->where('uuid', $user->uuid)->update(['type' => 'user', 'meta' => json_encode($meta)]);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The managed type for the account, or null when it should stay a team member.
|
|
90
|
+
*/
|
|
91
|
+
private function managedTypeFor(string $userUuid): ?string
|
|
92
|
+
{
|
|
93
|
+
$hasDriver = DB::table('drivers')->where('user_uuid', $userUuid)->whereNull('deleted_at')->exists();
|
|
94
|
+
$hasCustomer = DB::table('contacts')->where('user_uuid', $userUuid)->where('type', 'customer')->whereNull('deleted_at')->exists();
|
|
95
|
+
|
|
96
|
+
// An account holding both kinds of profile is a person the organization
|
|
97
|
+
// relates to in two ways; leave it for an administrator to decide.
|
|
98
|
+
if ($hasDriver === $hasCustomer) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$type = $hasDriver ? 'driver' : 'customer';
|
|
103
|
+
$companyUserUuids = DB::table('company_users')->where('user_uuid', $userUuid)->whereNull('deleted_at')->pluck('uuid');
|
|
104
|
+
$modelUuids = $companyUserUuids->push($userUuid)->all();
|
|
105
|
+
|
|
106
|
+
$roles = DB::table('model_has_roles')
|
|
107
|
+
->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
|
|
108
|
+
->whereIn('model_has_roles.model_uuid', $modelUuids)
|
|
109
|
+
->pluck('roles.name')
|
|
110
|
+
->unique();
|
|
111
|
+
|
|
112
|
+
if ($roles->isEmpty() || $roles->contains(fn ($role) => $role !== self::PROFILE_ROLES[$type])) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
$hasPermissions = DB::table('model_has_permissions')->whereIn('model_uuid', $modelUuids)->exists();
|
|
117
|
+
$hasPolicies = DB::table('model_has_policies')->whereIn('model_uuid', $modelUuids)->exists();
|
|
118
|
+
|
|
119
|
+
return $hasPermissions || $hasPolicies ? null : $type;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private function hasRequiredTables(): bool
|
|
123
|
+
{
|
|
124
|
+
foreach (['users', 'drivers', 'contacts', 'companies', 'company_users', 'roles', 'model_has_roles', 'model_has_permissions', 'model_has_policies'] as $table) {
|
|
125
|
+
if (!Schema::hasTable($table)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<x-mail-layout>
|
|
2
|
+
<h2 style="font-size: 18px; font-weight: 600;">
|
|
3
|
+
Your driver sign-in details
|
|
4
|
+
</h2>
|
|
5
|
+
|
|
6
|
+
Hi {{ $user->name }},
|
|
7
|
+
<br />
|
|
8
|
+
<br />
|
|
9
|
+
{{ $companyName }} has set up your driver account. Use these details to sign in to the driver app.
|
|
10
|
+
<br />
|
|
11
|
+
<br />
|
|
12
|
+
<strong>Your sign-in details</strong>
|
|
13
|
+
<br />
|
|
14
|
+
Login: {{ $identity }}
|
|
15
|
+
<br />
|
|
16
|
+
Temporary password: {{ $plaintextPassword }}
|
|
17
|
+
<br />
|
|
18
|
+
<br />
|
|
19
|
+
You can change your password after signing in.
|
|
20
|
+
|
|
21
|
+
</x-mail-layout>
|
|
@@ -68,7 +68,7 @@ class FixCustomerCompanies extends Command
|
|
|
68
68
|
$this->line('Found user ' . $user->name . ' (' . $user->email . ') which doesnt have correct company assignment.');
|
|
69
69
|
$company = $this->companyByUuid($customer->company_uuid);
|
|
70
70
|
if ($company) {
|
|
71
|
-
$user->assignCompany($company);
|
|
71
|
+
$user->assignCompany($company, 'Fleet-Ops Customer');
|
|
72
72
|
$this->line('User ' . $user->email . ' was assigned to company: ' . $company->name);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -49,7 +49,7 @@ class FixDriverCompanies extends Command
|
|
|
49
49
|
$this->line('Found driver ' . $user->name . ' (' . $user->email . ') which doesnt have correct company assignment.');
|
|
50
50
|
$company = $this->companyByUuid($driver->company_uuid);
|
|
51
51
|
if ($company) {
|
|
52
|
-
$user->assignCompany($company);
|
|
52
|
+
$user->assignCompany($company, 'Driver');
|
|
53
53
|
$this->line('Driver ' . $user->email . ' was assigned to company: ' . $company->name);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Exceptions;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a driver, contact or customer profile is given an email or phone
|
|
7
|
+
* that belongs to an account it cannot be linked to.
|
|
8
|
+
*/
|
|
9
|
+
class ProfileIdentityConflictException extends \Exception
|
|
10
|
+
{
|
|
11
|
+
/**
|
|
12
|
+
* The profile field the conflict was found on (`email` or `phone`).
|
|
13
|
+
*/
|
|
14
|
+
protected string $field;
|
|
15
|
+
|
|
16
|
+
public function __construct(string $message, string $field = 'email')
|
|
17
|
+
{
|
|
18
|
+
parent::__construct($message);
|
|
19
|
+
$this->field = $field;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function getField(): string
|
|
23
|
+
{
|
|
24
|
+
return $this->field;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The conflict keyed by field, in the shape of a validation error bag.
|
|
29
|
+
*/
|
|
30
|
+
public function getErrors(): array
|
|
31
|
+
{
|
|
32
|
+
return [$this->field => [$this->getMessage()]];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -44,6 +44,8 @@ class CustomerController extends Controller
|
|
|
44
44
|
{
|
|
45
45
|
use \Fleetbase\FleetOps\Http\Controllers\Concerns\ResolvesReviewAccountBypass;
|
|
46
46
|
|
|
47
|
+
public const DEACTIVATED_LOGIN_MESSAGE = 'This customer login has been deactivated.';
|
|
48
|
+
|
|
47
49
|
/* ============================================================
|
|
48
50
|
| Public auth flows (API credential only, no Customer-Token)
|
|
49
51
|
* ============================================================ */
|
|
@@ -345,6 +347,10 @@ class CustomerController extends Controller
|
|
|
345
347
|
return response()->apiError('Authentication failed using credentials provided.', 401);
|
|
346
348
|
}
|
|
347
349
|
|
|
350
|
+
if ($user->status === 'inactive') {
|
|
351
|
+
return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
|
|
352
|
+
}
|
|
353
|
+
|
|
348
354
|
$sessionCompany = $this->sessionCompany();
|
|
349
355
|
if (!$sessionCompany) {
|
|
350
356
|
return response()->apiError('No company resolved from API credential.', 500);
|
|
@@ -381,6 +387,10 @@ class CustomerController extends Controller
|
|
|
381
387
|
return response()->apiError('No customer with this phone number found.');
|
|
382
388
|
}
|
|
383
389
|
|
|
390
|
+
if ($user->status === 'inactive') {
|
|
391
|
+
return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
|
|
392
|
+
}
|
|
393
|
+
|
|
384
394
|
try {
|
|
385
395
|
$this->generateSmsVerification($user, 'fleetops_customer_login', [
|
|
386
396
|
'messageCallback' => fn ($verification) => 'Your ' . config('app.name') . ' verification code is ' . $verification->code,
|
|
@@ -423,6 +433,10 @@ class CustomerController extends Controller
|
|
|
423
433
|
return response()->apiError('Unable to verify code.');
|
|
424
434
|
}
|
|
425
435
|
|
|
436
|
+
if ($user->status === 'inactive') {
|
|
437
|
+
return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
|
|
438
|
+
}
|
|
439
|
+
|
|
426
440
|
$verificationCode = $this->verificationCodeExists([
|
|
427
441
|
'subject_uuid' => $user->uuid,
|
|
428
442
|
'code' => $code,
|
|
@@ -6,6 +6,7 @@ use Fleetbase\FleetOps\Events\DriverLocationChanged;
|
|
|
6
6
|
use Fleetbase\FleetOps\Events\GeofenceEntered;
|
|
7
7
|
use Fleetbase\FleetOps\Events\GeofenceExited;
|
|
8
8
|
use Fleetbase\FleetOps\Events\VehicleLocationChanged;
|
|
9
|
+
use Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException;
|
|
9
10
|
use Fleetbase\FleetOps\Exceptions\PublicRelationNotFoundException;
|
|
10
11
|
use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesFleetOpsApiResources;
|
|
11
12
|
use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesPublicExpansions;
|
|
@@ -22,6 +23,7 @@ use Fleetbase\FleetOps\Models\Vehicle;
|
|
|
22
23
|
use Fleetbase\FleetOps\Models\Vendor;
|
|
23
24
|
use Fleetbase\FleetOps\Support\GeofenceIntersectionService;
|
|
24
25
|
use Fleetbase\FleetOps\Support\OSRM;
|
|
26
|
+
use Fleetbase\FleetOps\Support\ProfileAccountManager;
|
|
25
27
|
use Fleetbase\FleetOps\Support\Utils;
|
|
26
28
|
use Fleetbase\Http\Controllers\Controller;
|
|
27
29
|
use Fleetbase\Http\Requests\SwitchOrganizationRequest;
|
|
@@ -47,6 +49,11 @@ class DriverController extends Controller
|
|
|
47
49
|
use ResolvesFleetOpsApiResources;
|
|
48
50
|
use ResolvesPublicExpansions;
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Returned when a driver whose login was deactivated from the console tries to sign in.
|
|
54
|
+
*/
|
|
55
|
+
public const DEACTIVATED_LOGIN_MESSAGE = 'This driver login has been deactivated.';
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* Public expansion name => Eloquent relation name.
|
|
52
59
|
*
|
|
@@ -97,20 +104,14 @@ class DriverController extends Controller
|
|
|
97
104
|
// Apply user infos
|
|
98
105
|
$userDetails = $this->applyUserInfoFromRequest($request, $userDetails);
|
|
99
106
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Set user type
|
|
110
|
-
$user->setUserType('driver');
|
|
111
|
-
|
|
112
|
-
// assign driver role
|
|
113
|
-
$user->assignSingleRole('Driver');
|
|
107
|
+
// Resolve the driver's login account: a team member of the company with
|
|
108
|
+
// the email/phone is linked, otherwise a driver account is created and
|
|
109
|
+
// added to the company without an organization invite.
|
|
110
|
+
try {
|
|
111
|
+
$user = $this->resolveDriverAccount($company->uuid, $userDetails);
|
|
112
|
+
} catch (ProfileIdentityConflictException $exception) {
|
|
113
|
+
return $this->jsonResponse(['error' => $exception->getMessage()], 422);
|
|
114
|
+
}
|
|
114
115
|
|
|
115
116
|
// set user id
|
|
116
117
|
$input['user_uuid'] = $user->uuid;
|
|
@@ -193,10 +194,11 @@ class DriverController extends Controller
|
|
|
193
194
|
// it. The driver's own record has no timezone column; the user's does.
|
|
194
195
|
$userDetails = $request->only(['name', 'email', 'phone', 'timezone']);
|
|
195
196
|
|
|
196
|
-
// update driver
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
// update the driver's login account through its proxy fields
|
|
198
|
+
try {
|
|
199
|
+
ProfileAccountManager::syncProxyFields($driver->getUser(), $userDetails);
|
|
200
|
+
} catch (ProfileIdentityConflictException $exception) {
|
|
201
|
+
return $this->jsonResponse(['error' => $exception->getMessage()], 422);
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
// latitude / longitude
|
|
@@ -519,10 +521,14 @@ class DriverController extends Controller
|
|
|
519
521
|
)->whereHas('driver')->first();
|
|
520
522
|
|
|
521
523
|
// Check password to authenticate driver
|
|
522
|
-
if (!Hash::check($password, $user->password)) {
|
|
524
|
+
if (!$user || !is_string($user->password) || !Hash::check((string) $password, $user->password)) {
|
|
523
525
|
return response()->apiError('Authentication failed using password provided.', 401);
|
|
524
526
|
}
|
|
525
527
|
|
|
528
|
+
if (static::isLoginDeactivated($user)) {
|
|
529
|
+
return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
|
|
530
|
+
}
|
|
531
|
+
|
|
526
532
|
// Get the user's company for this driver profile
|
|
527
533
|
$company = static::getDriverCompanyFromUser($user);
|
|
528
534
|
|
|
@@ -561,6 +567,10 @@ class DriverController extends Controller
|
|
|
561
567
|
return response()->apiError('No driver with this phone # found.');
|
|
562
568
|
}
|
|
563
569
|
|
|
570
|
+
if (static::isLoginDeactivated($user)) {
|
|
571
|
+
return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
|
|
572
|
+
}
|
|
573
|
+
|
|
564
574
|
// Get the user's company for this driver profile
|
|
565
575
|
$company = static::getDriverCompanyFromUser($user);
|
|
566
576
|
|
|
@@ -627,6 +637,10 @@ class DriverController extends Controller
|
|
|
627
637
|
return response()->apiError('Unable to verify code.');
|
|
628
638
|
}
|
|
629
639
|
|
|
640
|
+
if (static::isLoginDeactivated($user)) {
|
|
641
|
+
return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
|
|
642
|
+
}
|
|
643
|
+
|
|
630
644
|
// find and verify code
|
|
631
645
|
$verificationCode = VerificationCode::where(['subject_uuid' => $user->uuid, 'code' => $code, 'for' => $for])->exists();
|
|
632
646
|
if (!$verificationCode && !static::verificationBypassMatches($identity, $code)) {
|
|
@@ -975,26 +989,29 @@ class DriverController extends Controller
|
|
|
975
989
|
return User::applyUserInfoFromRequest($request, $userDetails);
|
|
976
990
|
}
|
|
977
991
|
|
|
978
|
-
|
|
992
|
+
/**
|
|
993
|
+
* Resolve the login account for a driver created through the API.
|
|
994
|
+
*
|
|
995
|
+
* @throws ProfileIdentityConflictException
|
|
996
|
+
*/
|
|
997
|
+
protected function resolveDriverAccount(string $companyUuid, array $userDetails): User
|
|
979
998
|
{
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
$user = User::create($userDetails);
|
|
991
|
-
|
|
992
|
-
if (is_string($password) && strlen($password)) {
|
|
993
|
-
$user->password = $password;
|
|
994
|
-
$user->save();
|
|
995
|
-
}
|
|
999
|
+
return ProfileAccountManager::resolveForProfile(
|
|
1000
|
+
$companyUuid,
|
|
1001
|
+
'driver',
|
|
1002
|
+
$userDetails['name'] ?? null,
|
|
1003
|
+
$userDetails['email'] ?? null,
|
|
1004
|
+
$userDetails['phone'] ?? null,
|
|
1005
|
+
array_merge(Arr::except($userDetails, ['name', 'email', 'phone']), ['status' => 'active'])
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
996
1008
|
|
|
997
|
-
|
|
1009
|
+
/**
|
|
1010
|
+
* Whether the driver's login was deactivated from the console.
|
|
1011
|
+
*/
|
|
1012
|
+
public static function isLoginDeactivated(User $user): bool
|
|
1013
|
+
{
|
|
1014
|
+
return $user->status === 'inactive';
|
|
998
1015
|
}
|
|
999
1016
|
|
|
1000
1017
|
protected function getUuid(array|string $table, array $where, array $options = []): mixed
|
|
@@ -1313,7 +1330,7 @@ class DriverController extends Controller
|
|
|
1313
1330
|
}
|
|
1314
1331
|
|
|
1315
1332
|
$user = static::findDriverUserByIdentity($identity);
|
|
1316
|
-
if (!$user) {
|
|
1333
|
+
if (!$user || static::isLoginDeactivated($user)) {
|
|
1317
1334
|
return response()->json(['status' => 'ok']);
|
|
1318
1335
|
}
|
|
1319
1336
|
|
|
@@ -1344,7 +1361,7 @@ class DriverController extends Controller
|
|
|
1344
1361
|
}
|
|
1345
1362
|
|
|
1346
1363
|
$user = static::findDriverUserByIdentity($identity);
|
|
1347
|
-
if (!$user) {
|
|
1364
|
+
if (!$user || static::isLoginDeactivated($user)) {
|
|
1348
1365
|
return response()->apiError('Invalid or expired reset code.', 422);
|
|
1349
1366
|
}
|
|
1350
1367
|
|