@fleetbase/fleetops-engine 0.6.68 → 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 -9
- 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/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/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/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -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/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/ProfileAccountManager.php +405 -0
- 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/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/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
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
|
|
|
@@ -265,25 +265,13 @@ class ContactController extends FleetOpsController
|
|
|
265
265
|
->get();
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* The login account is managed by the contact profile, so a user can't be
|
|
270
|
+
* picked or swapped from the console.
|
|
271
|
+
*/
|
|
268
272
|
private function resolveUserInput(Request $request, array &$input): void
|
|
269
273
|
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (is_array($user)) {
|
|
273
|
-
$user = data_get($user, 'uuid') ?? data_get($user, 'id');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (!$user) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
$input['user_uuid'] = $this->resolveUserUuid($user);
|
|
281
|
-
unset($input['user']);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
protected function resolveUserUuid(string $user): string
|
|
285
|
-
{
|
|
286
|
-
return User::where('uuid', $user)->orWhere('public_id', $user)->value('uuid') ?? $user;
|
|
274
|
+
unset($input['user_uuid'], $input['user']);
|
|
287
275
|
}
|
|
288
276
|
|
|
289
277
|
protected function assertCustomerPortalCanSendWelcomeEmail(array $input): void
|
|
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Mail\CustomerCredentialsMail;
|
|
6
6
|
use Fleetbase\FleetOps\Models\Contact;
|
|
7
|
+
use Fleetbase\FleetOps\Support\ProfileAccountManager;
|
|
7
8
|
use Fleetbase\Http\Controllers\Controller;
|
|
8
9
|
use Fleetbase\Models\User;
|
|
9
10
|
use Illuminate\Http\Request;
|
|
@@ -21,6 +22,10 @@ class CustomerController extends Controller
|
|
|
21
22
|
return response()->error('Unable to create customer portal login.');
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
if ($error = $this->staffLinkedError($user)) {
|
|
26
|
+
return $error;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
if ($user->status !== 'active') {
|
|
25
30
|
$user->activate();
|
|
26
31
|
}
|
|
@@ -39,6 +44,10 @@ class CustomerController extends Controller
|
|
|
39
44
|
return response()->error('Unable to send customer portal credentials.');
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
if ($error = $this->staffLinkedError($user)) {
|
|
48
|
+
return $error;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
$password = $this->randomPassword();
|
|
43
52
|
$user->changePassword($password);
|
|
44
53
|
$this->sendCustomerCredentials($user, $password, $customer);
|
|
@@ -57,6 +66,10 @@ class CustomerController extends Controller
|
|
|
57
66
|
return response()->error('Customer portal login not found.');
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
if ($error = $this->staffLinkedError($user)) {
|
|
70
|
+
return $error;
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
$user->deactivate();
|
|
61
74
|
|
|
62
75
|
return response()->json([
|
|
@@ -73,6 +86,10 @@ class CustomerController extends Controller
|
|
|
73
86
|
return response()->error('Customer portal login not found.');
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
if ($error = $this->staffLinkedError($user)) {
|
|
90
|
+
return $error;
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
$user->activate();
|
|
77
94
|
|
|
78
95
|
return response()->json([
|
|
@@ -149,6 +166,10 @@ class CustomerController extends Controller
|
|
|
149
166
|
return response()->error('Unable to reset customer credentials');
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
if ($error = $this->staffLinkedError($user)) {
|
|
170
|
+
return $error;
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
// Change password
|
|
153
174
|
$user->changePassword($password);
|
|
154
175
|
|
|
@@ -163,6 +184,19 @@ class CustomerController extends Controller
|
|
|
163
184
|
]);
|
|
164
185
|
}
|
|
165
186
|
|
|
187
|
+
/**
|
|
188
|
+
* A customer linked to a team member's account signs in with that account,
|
|
189
|
+
* which is managed in IAM.
|
|
190
|
+
*/
|
|
191
|
+
protected function staffLinkedError(User $user): ?\Illuminate\Http\JsonResponse
|
|
192
|
+
{
|
|
193
|
+
if (ProfileAccountManager::isStaffAccount($user)) {
|
|
194
|
+
return response()->error('This customer signs in with a team member account. Manage this login in IAM.', 422);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
166
200
|
protected function resolveCustomer(Request $request): Contact
|
|
167
201
|
{
|
|
168
202
|
$customerId = $request->input('customer');
|
|
@@ -213,10 +247,12 @@ class CustomerController extends Controller
|
|
|
213
247
|
$customer->loadMissing('user');
|
|
214
248
|
|
|
215
249
|
return [
|
|
216
|
-
'id'
|
|
217
|
-
'uuid'
|
|
218
|
-
'user_uuid'
|
|
219
|
-
'
|
|
250
|
+
'id' => $customer->public_id,
|
|
251
|
+
'uuid' => $customer->uuid,
|
|
252
|
+
'user_uuid' => $customer->user_uuid,
|
|
253
|
+
'is_staff_linked' => ProfileAccountManager::isStaffAccount($customer->user),
|
|
254
|
+
'login_status' => $customer->user?->status,
|
|
255
|
+
'user' => $customer->user ? [
|
|
220
256
|
'id' => $customer->user->public_id,
|
|
221
257
|
'uuid' => $customer->user->uuid,
|
|
222
258
|
'name' => $customer->user->name,
|