@fleetbase/fleetops-engine 0.6.59 → 0.6.61
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 +25 -0
- package/addon/components/ai/route-preview-map.js +1 -5
- package/addon/components/customer/orders.js +2 -1
- package/addon/components/driver/card.hbs +49 -0
- package/addon/components/driver/card.js +30 -0
- package/addon/components/map/leaflet-live-map.js +4 -19
- package/addon/components/modals/place-details.hbs +1 -1
- package/addon/components/modals/point-map.hbs +1 -1
- package/addon/components/orchestrator-workbench.js +2 -1
- package/addon/components/order/form/route.js +3 -0
- package/addon/components/order-tracking-lookup.js +2 -1
- package/addon/components/place/details.hbs +1 -1
- package/addon/components/positions-replay.js +4 -1
- package/addon/components/service-area/details.hbs +1 -1
- package/addon/components/vehicle/details/schedules.hbs +2 -2
- package/addon/components/vehicle/details/schedules.js +7 -1
- package/addon/components/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -1
- package/addon/controllers/maintenance/schedules/index/edit.js +10 -1
- package/addon/controllers/management/drivers/index.js +22 -1
- package/addon/controllers/settings/map.js +16 -0
- package/addon/helpers/leaflet-tile-url.js +19 -0
- package/addon/services/map-settings.js +22 -0
- package/addon/services/vehicle-actions.js +4 -2
- package/addon/templates/management/drivers/index.hbs +67 -33
- package/addon/templates/settings/map.hbs +10 -0
- package/addon/utils/leaflet-tile-url.js +31 -0
- package/addon/utils/prepare-place-for-save.js +13 -0
- package/app/components/driver/card.js +1 -0
- package/app/helpers/leaflet-tile-url.js +1 -0
- package/app/utils/leaflet-tile-url.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/index.js +11 -0
- package/package.json +2 -1
- package/server/config/fleetops.php +44 -0
- package/server/config/geocoder.php +1 -1
- package/server/migrations/2026_08_08_000001_make_sku_and_provider_transaction_unique_indexes_soft_delete_aware.php +152 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +50 -4
- package/server/src/Http/Controllers/Api/v1/DriverController.php +277 -5
- package/server/src/Http/Controllers/Api/v1/GeofenceController.php +34 -2
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/NavigatorController.php +14 -13
- package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +10 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +6 -0
- package/server/src/Http/Controllers/Concerns/ResolvesReviewAccountBypass.php +62 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
- package/server/src/Http/Filter/FuelReportFilter.php +29 -0
- package/server/src/Http/Filter/IssueFilter.php +29 -0
- package/server/src/Http/Filter/MaintenanceScheduleFilter.php +40 -0
- package/server/src/Http/Requests/CreateCustomerRequest.php +9 -1
- package/server/src/Http/Requests/CreateFuelReportRequest.php +14 -3
- package/server/src/Http/Requests/CreateFuelTransactionRequest.php +64 -1
- package/server/src/Http/Requests/CreatePartRequest.php +39 -1
- package/server/src/Http/Requests/CreateVehicleRequest.php +10 -5
- package/server/src/Http/Resources/v1/Manifest.php +52 -0
- package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
- package/server/src/Http/Resources/v1/TrackingNumber.php +33 -0
- package/server/src/Models/FuelReport.php +21 -0
- package/server/src/Models/Place.php +1 -1
- package/server/src/Models/Sensor.php +24 -0
- package/server/src/Support/FuelProviders/FuelProviderService.php +10 -1
- package/server/src/routes.php +22 -1
- package/server/tests/ApiCustomerControllerContractsTest.php +168 -0
- package/server/tests/ApiDriverControllerContractsTest.php +65 -0
- package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
- package/server/tests/ApiManifestControllerContractsTest.php +599 -0
- package/server/tests/ApiVehicleControllerContractsTest.php +35 -0
- package/server/tests/CompactResourceSerializationTest.php +99 -0
- package/server/tests/CustomerEndpointTest.php +4 -1
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +81 -2
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Api/GeofenceControllerContractsTest.php +99 -2
- package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +12 -6
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +58 -0
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/FuelProviderTransactionControllerContractsTest.php +17 -0
- package/server/tests/RequestContractsTest.php +102 -5
- package/server/tests/SettingControllerContractsTest.php +33 -0
- package/server/tests/SmallControllerContractsTest.php +28 -4
- package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
- package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
- package/server/tests/Unit/Http/Filter/MaintenanceScheduleFilterTest.php +69 -0
- package/server/tests/Unit/Http/Requests/CreateFuelReportRequestTest.php +21 -0
- package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/server/tests/Unit/Jobs/GeofenceDwellAndBulkNotifyTest.php +7 -0
- package/server/tests/Unit/Models/SensorLastPositionDefaultTest.php +130 -0
- package/translations/en-us.yaml +4 -0
|
@@ -30,6 +30,17 @@ class TrackingNumber extends FleetbaseResource
|
|
|
30
30
|
'status' => $this->last_status,
|
|
31
31
|
'status_code' => $this->last_status_code,
|
|
32
32
|
'qr_code' => $this->qr_code,
|
|
33
|
+
// The raw text encoded inside qr_code, exposed ONLY in debug mode.
|
|
34
|
+
//
|
|
35
|
+
// qr_code is a base64 PNG generated from owner_uuid (TrackingNumber::newBarcode),
|
|
36
|
+
// and the endpoints that consume a scanned code match on that uuid. A client
|
|
37
|
+
// scans the image to obtain it; an automated contract run cannot, so debug
|
|
38
|
+
// builds publish the value beside the image rather than the API growing a
|
|
39
|
+
// decode endpoint or handing out uuids generally.
|
|
40
|
+
//
|
|
41
|
+
// `when()` omits the key entirely when false — it is absent, not null — so a
|
|
42
|
+
// production response is byte-identical to before.
|
|
43
|
+
'qr_code_content' => $this->when(static::exposesQrCodeContent(), fn () => $this->owner_uuid),
|
|
33
44
|
'barcode' => $this->barcode,
|
|
34
45
|
'url' => Utils::consoleUrl('track-order', ['order' => $this->tracking_number]),
|
|
35
46
|
'type' => Utils::getTypeFromClassName($this->owner_type),
|
|
@@ -38,6 +49,28 @@ class TrackingNumber extends FleetbaseResource
|
|
|
38
49
|
];
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Whether the QR code's decoded content may be published.
|
|
54
|
+
*
|
|
55
|
+
* Debug mode only, and fails closed: any failure to determine the debug state — no
|
|
56
|
+
* container, an application without the accessor — answers false rather than
|
|
57
|
+
* defaulting to exposure.
|
|
58
|
+
*/
|
|
59
|
+
protected static function exposesQrCodeContent(): bool
|
|
60
|
+
{
|
|
61
|
+
try {
|
|
62
|
+
$app = app();
|
|
63
|
+
|
|
64
|
+
if (!is_object($app) || !method_exists($app, 'hasDebugModeEnabled')) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (bool) $app->hasDebugModeEnabled();
|
|
69
|
+
} catch (\Throwable $e) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
/**
|
|
42
75
|
* Transform the resource into an webhook payload.
|
|
43
76
|
*
|
|
@@ -6,6 +6,7 @@ use Fleetbase\Casts\Json;
|
|
|
6
6
|
use Fleetbase\FleetOps\Casts\Point;
|
|
7
7
|
use Fleetbase\FleetOps\Support\Utils;
|
|
8
8
|
use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
|
|
9
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
|
|
9
10
|
use Fleetbase\Models\Model;
|
|
10
11
|
use Fleetbase\Models\User;
|
|
11
12
|
use Fleetbase\Traits\HasApiModelBehavior;
|
|
@@ -149,6 +150,26 @@ class FuelReport extends Model
|
|
|
149
150
|
return $this->belongsTo(User::class, 'reported_by_uuid');
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Bootstrap the model.
|
|
155
|
+
*
|
|
156
|
+
* `location` is declared nullable in CreateFuelReportRequest but the column is
|
|
157
|
+
* POINT NOT NULL, so a report created without one failed outright with
|
|
158
|
+
* SQLSTATE[HY000] 1364 "Field 'location' doesn't have a default value" — a 500 for
|
|
159
|
+
* a request the validator had just accepted. Mirrors Device and Sensor, which
|
|
160
|
+
* default their spatial columns the same way.
|
|
161
|
+
*/
|
|
162
|
+
protected static function boot()
|
|
163
|
+
{
|
|
164
|
+
parent::boot();
|
|
165
|
+
|
|
166
|
+
static::creating(function (self $fuelReport) {
|
|
167
|
+
if (empty($fuelReport->location)) {
|
|
168
|
+
$fuelReport->location = new SpatialPoint(0, 0);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
/**
|
|
153
174
|
* Get the driver's name assigned to vehicle.
|
|
154
175
|
*
|
|
@@ -129,7 +129,7 @@ class Place extends Model
|
|
|
129
129
|
*
|
|
130
130
|
* @var array
|
|
131
131
|
*/
|
|
132
|
-
protected $filterParams = ['vendor', 'contact', 'vendor_uuid', 'vendor_name'];
|
|
132
|
+
protected $filterParams = ['vendor', 'contact', 'vendor_uuid', 'vendor_name', 'avatar_value', 'eta'];
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
|
@@ -147,6 +147,30 @@ class Sensor extends Model
|
|
|
147
147
|
*/
|
|
148
148
|
protected $spatialFields = ['last_position'];
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Bootstrap the model.
|
|
152
|
+
*
|
|
153
|
+
* The same migration that made devices.last_position NOT NULL did it to
|
|
154
|
+
* sensors.last_position too, but only Device got a default. Creating a sensor
|
|
155
|
+
* without a position therefore failed outright:
|
|
156
|
+
*
|
|
157
|
+
* SQLSTATE[HY000]: General error: 1364 Field 'last_position' doesn't have a
|
|
158
|
+
* default value
|
|
159
|
+
*
|
|
160
|
+
* Position is not something a caller has when registering a sensor, so default
|
|
161
|
+
* it to POINT(0,0) on create, exactly as Device does.
|
|
162
|
+
*/
|
|
163
|
+
protected static function boot()
|
|
164
|
+
{
|
|
165
|
+
parent::boot();
|
|
166
|
+
|
|
167
|
+
static::creating(function (self $sensor) {
|
|
168
|
+
if (empty($sensor->last_position)) {
|
|
169
|
+
$sensor->last_position = new SpatialPoint(0, 0);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
/**
|
|
151
175
|
* The attributes that should be cast to native types.
|
|
152
176
|
*
|
|
@@ -173,8 +173,17 @@ class FuelProviderService
|
|
|
173
173
|
return DB::transaction(function () use ($connection, $payload) {
|
|
174
174
|
$provider = $payload['provider'] ?? $connection->provider;
|
|
175
175
|
$providerTransactionId = $payload['provider_transaction_id'];
|
|
176
|
+
// The match must include company_uuid. Provider transaction ids are only
|
|
177
|
+
// unique within the account they were issued for, so keying on
|
|
178
|
+
// (provider, provider_transaction_id) alone let one company's sync find
|
|
179
|
+
// and overwrite another company's transaction — reassigning its
|
|
180
|
+
// company_uuid. Mirrors fuel_provider_txn_company_provider_unique.
|
|
176
181
|
$transaction = FuelProviderTransaction::updateOrCreate(
|
|
177
|
-
[
|
|
182
|
+
[
|
|
183
|
+
'company_uuid' => $connection->company_uuid,
|
|
184
|
+
'provider' => $provider,
|
|
185
|
+
'provider_transaction_id' => $providerTransactionId,
|
|
186
|
+
],
|
|
178
187
|
array_merge($payload, [
|
|
179
188
|
'company_uuid' => $connection->company_uuid,
|
|
180
189
|
'fuel_provider_connection_uuid' => $connection->uuid,
|
package/server/src/routes.php
CHANGED
|
@@ -28,6 +28,13 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
28
28
|
$router->post('login-with-sms', 'DriverController@loginWithPhone');
|
|
29
29
|
$router->post('verify-code', 'DriverController@verifyCode');
|
|
30
30
|
$router->post('login', 'DriverController@login');
|
|
31
|
+
// Password management. Deliberately separate from the driver
|
|
32
|
+
// update: changing a password requires proving the old one, and
|
|
33
|
+
// resetting it requires a code, neither of which a general PUT
|
|
34
|
+
// can express.
|
|
35
|
+
$router->post('forgot-password', 'DriverController@forgotPassword');
|
|
36
|
+
$router->post('reset-password', 'DriverController@resetPassword');
|
|
37
|
+
$router->post('{id}/change-password', 'DriverController@changePassword');
|
|
31
38
|
$router->post('{id}/simulate', 'DriverController@simulate');
|
|
32
39
|
$router->match(['put', 'patch', 'post'], '{id}/track', 'DriverController@track');
|
|
33
40
|
$router->post('{id}/register-device', 'DriverController@registerDevice');
|
|
@@ -37,6 +44,8 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
37
44
|
$router->get('/', 'DriverController@query');
|
|
38
45
|
$router->get('{id}', 'DriverController@find');
|
|
39
46
|
$router->get('{id}/organizations', 'DriverController@listOrganizations');
|
|
47
|
+
// A driver's own routes for the day.
|
|
48
|
+
$router->get('{id}/manifests', 'ManifestController@forDriver');
|
|
40
49
|
$router->get('{id}/current-organization', 'DriverController@currentOrganization');
|
|
41
50
|
$router->put('{id}', 'DriverController@update');
|
|
42
51
|
$router->delete('{id}', 'DriverController@delete');
|
|
@@ -185,6 +194,18 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
185
194
|
$router->delete('{id}', 'OrderController@delete');
|
|
186
195
|
$router->get('{id}/editable-entity-fields', 'OrderController@getEditableEntityFields');
|
|
187
196
|
});
|
|
197
|
+
// manifests — a driver's route. Read and run only: creating,
|
|
198
|
+
// cancelling and deleting a manifest is dispatch work and stays on
|
|
199
|
+
// the internal namespace.
|
|
200
|
+
$router->group(['prefix' => 'manifests'], function () use ($router) {
|
|
201
|
+
$router->get('{id}', 'ManifestController@show');
|
|
202
|
+
$router->post('{id}/optimize', 'ManifestController@optimize');
|
|
203
|
+
});
|
|
204
|
+
$router->group(['prefix' => 'manifest-stops'], function () use ($router) {
|
|
205
|
+
$router->patch('{id}', 'ManifestController@updateStop');
|
|
206
|
+
$router->post('{id}', 'ManifestController@updateStop');
|
|
207
|
+
});
|
|
208
|
+
|
|
188
209
|
// entities routes
|
|
189
210
|
$router->group(['prefix' => 'entities'], function () use ($router) {
|
|
190
211
|
$router->post('/', 'EntityController@create');
|
|
@@ -237,7 +258,7 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
237
258
|
$router->get('events', 'GeofenceController@events');
|
|
238
259
|
$router->get('inventory', 'GeofenceController@inventory');
|
|
239
260
|
$router->get('dwell-report', 'GeofenceController@dwellReport');
|
|
240
|
-
$router->get('driver/{
|
|
261
|
+
$router->get('driver/{driverId}/history', 'GeofenceController@driverHistory');
|
|
241
262
|
});
|
|
242
263
|
// service-rates routes
|
|
243
264
|
$router->group(['prefix' => 'service-rates'], function () use ($router) {
|
|
@@ -960,3 +960,171 @@ test('api customer controller handles verification delivery fallbacks and profil
|
|
|
960
960
|
'name' => 'explode',
|
|
961
961
|
]))))->toBe(['error' => 'update failed']);
|
|
962
962
|
});
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Run a callback with an app container that supports environment().
|
|
966
|
+
*
|
|
967
|
+
* The harness binds a bare Illuminate\Container\Container, which has no
|
|
968
|
+
* environment() — so CustomerController::verificationBypassMatches fatals with
|
|
969
|
+
* "Call to undefined method" without this. Mirrors the swap in
|
|
970
|
+
* NotificationAndMailContractsTest, but carries every existing binding across so
|
|
971
|
+
* the controller can still resolve config/request/db.
|
|
972
|
+
*/
|
|
973
|
+
function fleetopsApiCustomerWithEnvironment(string $environment, callable $callback): mixed
|
|
974
|
+
{
|
|
975
|
+
$previousApp = Illuminate\Container\Container::getInstance();
|
|
976
|
+
$app = new class extends Illuminate\Container\Container {
|
|
977
|
+
public string $fleetopsEnvironment = 'testing';
|
|
978
|
+
|
|
979
|
+
public function environment(...$environments)
|
|
980
|
+
{
|
|
981
|
+
if (empty($environments)) {
|
|
982
|
+
return $this->fleetopsEnvironment;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
$environments = is_array($environments[0]) ? $environments[0] : $environments;
|
|
986
|
+
|
|
987
|
+
return in_array($this->fleetopsEnvironment, $environments, true);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
public function hasDebugModeEnabled()
|
|
991
|
+
{
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
$app->fleetopsEnvironment = $environment;
|
|
996
|
+
|
|
997
|
+
$reflection = new ReflectionClass(Illuminate\Container\Container::class);
|
|
998
|
+
foreach (['bindings', 'instances', 'aliases', 'abstractAliases', 'resolved', 'scopedInstances'] as $property) {
|
|
999
|
+
if (!$reflection->hasProperty($property)) {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
$handle = $reflection->getProperty($property);
|
|
1003
|
+
$handle->setAccessible(true);
|
|
1004
|
+
$handle->setValue($app, $handle->getValue($previousApp));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
Illuminate\Container\Container::setInstance($app);
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
return $callback();
|
|
1011
|
+
} finally {
|
|
1012
|
+
Illuminate\Container\Container::setInstance($previousApp);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
test('api customer controller ignores the verification bypass unless it is configured', function () {
|
|
1017
|
+
// Unset and empty-string are both inert. Without this the bypass would be a
|
|
1018
|
+
// standing hole in every default install, which is the entire risk of shipping one.
|
|
1019
|
+
foreach ([null, ''] as $bypassCode) {
|
|
1020
|
+
config(['fleetops.customers.verification_bypass_code' => $bypassCode]);
|
|
1021
|
+
|
|
1022
|
+
fleetopsApiCustomerWithEnvironment('local', function () {
|
|
1023
|
+
$create = fleetopsApiCustomerController();
|
|
1024
|
+
$create->verificationExists = false;
|
|
1025
|
+
$verify = fleetopsApiCustomerController();
|
|
1026
|
+
$verify->verificationExists = false;
|
|
1027
|
+
$reset = fleetopsApiCustomerController();
|
|
1028
|
+
$reset->verificationCode = null;
|
|
1029
|
+
|
|
1030
|
+
expect(fleetopsApiCustomerJson($create->create(new CreateCustomerRequest([
|
|
1031
|
+
'code' => '000000',
|
|
1032
|
+
'identity' => 'jane@example.test',
|
|
1033
|
+
]))))->toBe(['error' => 'Invalid verification code provided.'])
|
|
1034
|
+
->and(fleetopsApiCustomerJson($verify->verifyCode(Request::create('/v1/customers/verify-code', 'POST', [
|
|
1035
|
+
'identity' => 'jane@example.test',
|
|
1036
|
+
'code' => '000000',
|
|
1037
|
+
]))))->toBe(['error' => 'Invalid verification code.'])
|
|
1038
|
+
->and(fleetopsApiCustomerJson($reset->resetPassword(Request::create('/v1/customers/reset-password', 'POST', [
|
|
1039
|
+
'identity' => 'jane@example.test',
|
|
1040
|
+
'code' => '000000',
|
|
1041
|
+
'password' => 'password-secret',
|
|
1042
|
+
]))))->toBe(['error' => 'Invalid reset code.']);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
config(['fleetops.customers.verification_bypass_code' => null]);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test('api customer controller accepts a configured verification bypass for a listed review account', function () {
|
|
1050
|
+
config([
|
|
1051
|
+
'fleetops.customers.verification_bypass_code' => '000000',
|
|
1052
|
+
'fleetops.customers.review_accounts' => ['jane@example.test'],
|
|
1053
|
+
]);
|
|
1054
|
+
|
|
1055
|
+
// Deliberately production. App store reviewers test a release build against
|
|
1056
|
+
// production, so refusing the bypass there made review impossible — that is the
|
|
1057
|
+
// behaviour this replaces. Safety now comes from the allowlist, not the environment.
|
|
1058
|
+
fleetopsApiCustomerWithEnvironment('production', function () {
|
|
1059
|
+
$create = fleetopsApiCustomerController();
|
|
1060
|
+
$create->verificationExists = false;
|
|
1061
|
+
$verify = fleetopsApiCustomerController();
|
|
1062
|
+
$verify->verificationExists = false;
|
|
1063
|
+
// No VerificationCode row on the bypass path — resetPassword must not fatal
|
|
1064
|
+
// calling ->delete() on null, and must still revoke existing sessions.
|
|
1065
|
+
$reset = fleetopsApiCustomerController();
|
|
1066
|
+
$reset->verificationCode = null;
|
|
1067
|
+
// A non-matching code is still rejected while the bypass is live.
|
|
1068
|
+
$wrong = fleetopsApiCustomerController();
|
|
1069
|
+
$wrong->verificationExists = false;
|
|
1070
|
+
|
|
1071
|
+
expect($create->create(new CreateCustomerRequest([
|
|
1072
|
+
'code' => '000000',
|
|
1073
|
+
'identity' => 'jane@example.test',
|
|
1074
|
+
'name' => 'Jane',
|
|
1075
|
+
'password' => 'password-secret',
|
|
1076
|
+
])))->toMatchArray(['resource' => 'customer', 'token' => 'plain-token'])
|
|
1077
|
+
->and($verify->verifyCode(Request::create('/v1/customers/verify-code', 'POST', [
|
|
1078
|
+
'identity' => 'jane@example.test',
|
|
1079
|
+
'code' => '000000',
|
|
1080
|
+
])))->toMatchArray(['resource' => 'customer', 'token' => 'plain-token'])
|
|
1081
|
+
->and(fleetopsApiCustomerJson($reset->resetPassword(Request::create('/v1/customers/reset-password', 'POST', [
|
|
1082
|
+
'identity' => 'jane@example.test',
|
|
1083
|
+
'code' => '000000',
|
|
1084
|
+
'password' => 'password-secret',
|
|
1085
|
+
]))))->toBe(['status' => 'ok'])
|
|
1086
|
+
->and($reset->genericUser->tokensDeleted)->toBeTrue()
|
|
1087
|
+
->and(fleetopsApiCustomerJson($wrong->create(new CreateCustomerRequest([
|
|
1088
|
+
'code' => '999999',
|
|
1089
|
+
'identity' => 'jane@example.test',
|
|
1090
|
+
]))))->toBe(['error' => 'Invalid verification code provided.']);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
config(['fleetops.customers.verification_bypass_code' => null, 'fleetops.customers.review_accounts' => []]);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
test('api customer controller refuses the verification bypass for an identity that is not listed', function () {
|
|
1097
|
+
// The point of the allowlist: holding the code is not sufficient. Previously
|
|
1098
|
+
// anyone who learned it could authenticate as any customer.
|
|
1099
|
+
foreach ([['someone-else@example.test'], []] as $reviewAccounts) {
|
|
1100
|
+
config([
|
|
1101
|
+
'fleetops.customers.verification_bypass_code' => '000000',
|
|
1102
|
+
'fleetops.customers.review_accounts' => $reviewAccounts,
|
|
1103
|
+
]);
|
|
1104
|
+
|
|
1105
|
+
fleetopsApiCustomerWithEnvironment('local', function () {
|
|
1106
|
+
$create = fleetopsApiCustomerController();
|
|
1107
|
+
$create->verificationExists = false;
|
|
1108
|
+
$verify = fleetopsApiCustomerController();
|
|
1109
|
+
$verify->verificationExists = false;
|
|
1110
|
+
$reset = fleetopsApiCustomerController();
|
|
1111
|
+
$reset->verificationCode = null;
|
|
1112
|
+
|
|
1113
|
+
expect(fleetopsApiCustomerJson($create->create(new CreateCustomerRequest([
|
|
1114
|
+
'code' => '000000',
|
|
1115
|
+
'identity' => 'jane@example.test',
|
|
1116
|
+
]))))->toBe(['error' => 'Invalid verification code provided.'])
|
|
1117
|
+
->and(fleetopsApiCustomerJson($verify->verifyCode(Request::create('/v1/customers/verify-code', 'POST', [
|
|
1118
|
+
'identity' => 'jane@example.test',
|
|
1119
|
+
'code' => '000000',
|
|
1120
|
+
]))))->toBe(['error' => 'Invalid verification code.'])
|
|
1121
|
+
->and(fleetopsApiCustomerJson($reset->resetPassword(Request::create('/v1/customers/reset-password', 'POST', [
|
|
1122
|
+
'identity' => 'jane@example.test',
|
|
1123
|
+
'code' => '000000',
|
|
1124
|
+
'password' => 'password-secret',
|
|
1125
|
+
]))))->toBe(['error' => 'Invalid reset code.']);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
config(['fleetops.customers.verification_bypass_code' => null, 'fleetops.customers.review_accounts' => []]);
|
|
1130
|
+
});
|
|
@@ -550,6 +550,38 @@ test('api driver controller registers a device for the authenticated driver with
|
|
|
550
550
|
]);
|
|
551
551
|
});
|
|
552
552
|
|
|
553
|
+
test('api driver controller falls back to the container request when the router injects null', function () {
|
|
554
|
+
// Laravel's ResolvesRouteDependencies::transformDependency() returns null — it does
|
|
555
|
+
// NOT resolve from the container — for any class-typed parameter that declares a
|
|
556
|
+
// default value. registerDevice() has to declare one, because the internal controller
|
|
557
|
+
// delegates with an id only, so every routed call arrived with $request === null.
|
|
558
|
+
// Every other test here passes a request explicitly, which is why they stayed green
|
|
559
|
+
// while POST /v1/drivers/{id}/register-device answered 500.
|
|
560
|
+
$driver = new FleetOpsApiDriverFake();
|
|
561
|
+
$driver->setRawAttributes([
|
|
562
|
+
'uuid' => 'driver-uuid',
|
|
563
|
+
'public_id' => 'driver_public',
|
|
564
|
+
'user_uuid' => 'user-uuid',
|
|
565
|
+
], true);
|
|
566
|
+
|
|
567
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
568
|
+
$controller->driver = $driver;
|
|
569
|
+
|
|
570
|
+
$bound = new FleetOpsApiDriverRegisterDeviceRequest(['token' => 'push-token', 'platform' => 'android']);
|
|
571
|
+
app()->instance('request', $bound);
|
|
572
|
+
|
|
573
|
+
expect($controller->registerDevice('driver_public', null))->toBe([
|
|
574
|
+
'json' => ['device' => 'device_public'],
|
|
575
|
+
'status' => 200,
|
|
576
|
+
])
|
|
577
|
+
->and($controller->deviceCreates)->toBe([
|
|
578
|
+
[
|
|
579
|
+
['token' => 'push-token', 'platform' => 'android'],
|
|
580
|
+
['user_uuid' => 'user-uuid', 'platform' => 'android', 'token' => 'push-token', 'status' => 'active'],
|
|
581
|
+
],
|
|
582
|
+
]);
|
|
583
|
+
});
|
|
584
|
+
|
|
553
585
|
test('api driver controller reports missing driver branches', function () {
|
|
554
586
|
$controller = new FleetOpsApiDriverControllerProbe();
|
|
555
587
|
$controller->driverNotFound = true;
|
|
@@ -569,3 +601,36 @@ test('api driver controller reports missing driver branches', function () {
|
|
|
569
601
|
'status' => 404,
|
|
570
602
|
]);
|
|
571
603
|
});
|
|
604
|
+
|
|
605
|
+
test('api driver controller refuses to set a password through a general update', function () {
|
|
606
|
+
/*
|
|
607
|
+
* Regression for a takeover path: `update()` passed `password` straight
|
|
608
|
+
* through to the user, so anyone holding a driver's token — or an unlocked
|
|
609
|
+
* handset — could set a new one without proving they knew the old one, and
|
|
610
|
+
* the account was theirs. A password change is an authorisation decision
|
|
611
|
+
* and now has its own endpoint that demands the current password.
|
|
612
|
+
*/
|
|
613
|
+
$user = new FleetOpsApiDriverUserFake();
|
|
614
|
+
$user->setRawAttributes(['uuid' => 'user-uuid'], true);
|
|
615
|
+
$driver = new FleetOpsApiDriverFake();
|
|
616
|
+
$driver->setRawAttributes([
|
|
617
|
+
'uuid' => 'driver-uuid',
|
|
618
|
+
'public_id' => 'driver_public',
|
|
619
|
+
'user_uuid' => 'user-uuid',
|
|
620
|
+
], true);
|
|
621
|
+
$driver->userForTest = $user;
|
|
622
|
+
$driver->setRelation('user', $user);
|
|
623
|
+
|
|
624
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
625
|
+
$controller->driver = $driver;
|
|
626
|
+
$controller->sessionCompanyUuid = 'session-company-uuid';
|
|
627
|
+
|
|
628
|
+
$controller->update('driver_public', new UpdateDriverRequest([
|
|
629
|
+
'name' => 'Driver Updated',
|
|
630
|
+
'password' => 'attacker-chosen-password',
|
|
631
|
+
]));
|
|
632
|
+
|
|
633
|
+
expect($user->updates)->not->toBeEmpty()
|
|
634
|
+
->and($user->updates[0])->not->toHaveKey('password')
|
|
635
|
+
->and($user->updates[0])->toHaveKey('name');
|
|
636
|
+
});
|