@fleetbase/fleetops-engine 0.6.60 → 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 +9 -20
- 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/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -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/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/src/Http/Controllers/Api/v1/DriverController.php +220 -3
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +6 -0
- 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/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/Models/Place.php +1 -1
- package/server/src/routes.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +33 -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 +13 -7
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/RequestContractsTest.php +5 -0
- package/server/tests/SettingControllerContractsTest.php +33 -0
- 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/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/translations/en-us.yaml +4 -0
|
@@ -328,6 +328,8 @@ class SettingController extends Controller
|
|
|
328
328
|
{
|
|
329
329
|
$defaults = [
|
|
330
330
|
'mapProvider' => 'leaflet',
|
|
331
|
+
'leafletTileUrl' => '',
|
|
332
|
+
'leafletDarkTileUrl' => '',
|
|
331
333
|
'googleMapsMapType' => 'roadmap',
|
|
332
334
|
'showGoogleMapsTrafficLayer' => false,
|
|
333
335
|
'showGoogleMapsTransitLayer' => false,
|
|
@@ -418,12 +420,32 @@ class SettingController extends Controller
|
|
|
418
420
|
return [
|
|
419
421
|
...$settings,
|
|
420
422
|
'mapProvider' => $mapProvider,
|
|
423
|
+
'leafletTileUrl' => $this->sanitizeTileUrl(data_get($settings, 'leafletTileUrl', '')),
|
|
424
|
+
'leafletDarkTileUrl' => $this->sanitizeTileUrl(data_get($settings, 'leafletDarkTileUrl', '')),
|
|
421
425
|
'googleMapsMapType' => $googleMapsMapType,
|
|
422
426
|
'showGoogleMapsTrafficLayer' => filter_var(data_get($settings, 'showGoogleMapsTrafficLayer', false), FILTER_VALIDATE_BOOLEAN),
|
|
423
427
|
'showGoogleMapsTransitLayer' => filter_var(data_get($settings, 'showGoogleMapsTransitLayer', false), FILTER_VALIDATE_BOOLEAN),
|
|
424
428
|
];
|
|
425
429
|
}
|
|
426
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Sanitize a Leaflet XYZ tile URL template — only http(s) URLs are accepted,
|
|
433
|
+
* anything else is discarded so the frontend falls back to the default tiles.
|
|
434
|
+
*/
|
|
435
|
+
protected function sanitizeTileUrl($url): string
|
|
436
|
+
{
|
|
437
|
+
if (!is_string($url)) {
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
$url = trim($url);
|
|
442
|
+
if ($url === '' || !preg_match('/^https?:\/\//i', $url)) {
|
|
443
|
+
return '';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return $url;
|
|
447
|
+
}
|
|
448
|
+
|
|
427
449
|
/**
|
|
428
450
|
* Retrieve driver scheduling settings for the current company.
|
|
429
451
|
*
|
|
@@ -86,6 +86,35 @@ class FuelReportFilter extends Filter
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Aliases for {@see driver()}.
|
|
91
|
+
*
|
|
92
|
+
* The base filter resolves a query parameter to a method of the same name
|
|
93
|
+
* and **silently ignores anything it cannot match**. So a client scoping a
|
|
94
|
+
* list with `driver_uuid` — the column's own name, and the name every other
|
|
95
|
+
* part of the payload uses — matched nothing, the filter was dropped, and
|
|
96
|
+
* the response came back scoped only by company: every driver's records,
|
|
97
|
+
* with a 200 and no indication that the request had been narrowed at all.
|
|
98
|
+
*
|
|
99
|
+
* For a driver app that is a disclosure, not a nuisance: a driver asking
|
|
100
|
+
* for their own fuel reports received the whole company's.
|
|
101
|
+
*/
|
|
102
|
+
public function driverUuid(?string $driver)
|
|
103
|
+
{
|
|
104
|
+
$this->driver($driver);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public function driverAssigned(?string $driver)
|
|
108
|
+
{
|
|
109
|
+
$this->driver($driver);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Alias for {@see vehicle()}, for the same reason. */
|
|
113
|
+
public function vehicleUuid(?string $vehicle)
|
|
114
|
+
{
|
|
115
|
+
$this->vehicle($vehicle);
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
public function vehicle(?string $vehicle)
|
|
90
119
|
{
|
|
91
120
|
$this->builder->whereHas('vehicle', function ($q) use ($vehicle) {
|
|
@@ -88,6 +88,35 @@ class IssueFilter extends Filter
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Aliases for {@see driver()}.
|
|
93
|
+
*
|
|
94
|
+
* The base filter resolves a query parameter to a method of the same name
|
|
95
|
+
* and **silently ignores anything it cannot match**. So a client scoping a
|
|
96
|
+
* list with `driver_uuid` — the column's own name, and the name every other
|
|
97
|
+
* part of the payload uses — matched nothing, the filter was dropped, and
|
|
98
|
+
* the response came back scoped only by company: every driver's records,
|
|
99
|
+
* with a 200 and no indication that the request had been narrowed at all.
|
|
100
|
+
*
|
|
101
|
+
* For a driver app that is a disclosure, not a nuisance: a driver asking
|
|
102
|
+
* for their own fuel reports received the whole company's.
|
|
103
|
+
*/
|
|
104
|
+
public function driverUuid(?string $driver)
|
|
105
|
+
{
|
|
106
|
+
$this->driver($driver);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public function driverAssigned(?string $driver)
|
|
110
|
+
{
|
|
111
|
+
$this->driver($driver);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Alias for {@see vehicle()}, for the same reason. */
|
|
115
|
+
public function vehicleUuid(?string $vehicle)
|
|
116
|
+
{
|
|
117
|
+
$this->vehicle($vehicle);
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
public function vehicle(?string $vehicle)
|
|
92
121
|
{
|
|
93
122
|
$this->builder->whereHas('vehicle', function ($q) use ($vehicle) {
|
|
@@ -46,11 +46,16 @@ class CreateVehicleRequest extends FleetbaseRequest
|
|
|
46
46
|
'decommissioned',
|
|
47
47
|
]),
|
|
48
48
|
],
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
'
|
|
49
|
+
// Validated rather than merely accepted: the model casts odometer
|
|
50
|
+
// to an integer, so an unchecked string would be stored as 0 — a
|
|
51
|
+
// vehicle reporting zero miles rather than an error.
|
|
52
|
+
'odometer' => 'nullable|numeric|min:0',
|
|
53
|
+
'odometer_unit' => 'nullable|string|max:12',
|
|
54
|
+
'vendor' => 'nullable|exists:vendors,public_id',
|
|
55
|
+
'driver' => 'nullable|exists:drivers,public_id',
|
|
56
|
+
'location' => ['nullable', new ResolvablePoint()],
|
|
57
|
+
'latitude' => ['nullable', 'required_with:longitude'],
|
|
58
|
+
'longitude' => ['nullable', 'required_with:latitude'],
|
|
54
59
|
];
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
|
+
use Fleetbase\Support\Http;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public projection of a Manifest — a driver's route for a day.
|
|
10
|
+
*
|
|
11
|
+
* A manifest is order-agnostic: it is a sequence of stops, which may belong to
|
|
12
|
+
* several orders or to none the driver has ever seen as an order. That is why
|
|
13
|
+
* this exposes the stops directly rather than making a consumer assemble them
|
|
14
|
+
* from orders.
|
|
15
|
+
*
|
|
16
|
+
* Totals ride along because a driver wants to know what the day looks like
|
|
17
|
+
* before starting it, and computing them client-side from the stop list gives a
|
|
18
|
+
* different answer than the server's own.
|
|
19
|
+
*/
|
|
20
|
+
class Manifest extends FleetbaseResource
|
|
21
|
+
{
|
|
22
|
+
/**
|
|
23
|
+
* Transform the resource into an array.
|
|
24
|
+
*
|
|
25
|
+
* @param \Illuminate\Http\Request $request
|
|
26
|
+
*/
|
|
27
|
+
public function toArray($request): array
|
|
28
|
+
{
|
|
29
|
+
return [
|
|
30
|
+
'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
|
|
31
|
+
'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
|
|
32
|
+
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
33
|
+
'status' => $this->status,
|
|
34
|
+
'scheduled_date' => $this->scheduled_date,
|
|
35
|
+
'started_at' => $this->started_at,
|
|
36
|
+
'completed_at' => $this->completed_at,
|
|
37
|
+
'total_distance_m' => $this->total_distance_m,
|
|
38
|
+
'total_duration_s' => $this->total_duration_s,
|
|
39
|
+
'stop_count' => $this->stop_count,
|
|
40
|
+
'completed_stops' => $this->completed_stops,
|
|
41
|
+
'pending_stops' => $this->pending_stops,
|
|
42
|
+
'driver_name' => $this->driver_name,
|
|
43
|
+
'vehicle_name' => $this->vehicle_name,
|
|
44
|
+
'notes' => $this->notes,
|
|
45
|
+
// Present only when the manifest was loaded with them, so a list
|
|
46
|
+
// endpoint stays a list.
|
|
47
|
+
'stops' => ManifestStop::collection($this->whenLoaded('stops')),
|
|
48
|
+
'updated_at' => $this->updated_at,
|
|
49
|
+
'created_at' => $this->created_at,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
|
+
use Fleetbase\Support\Http;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public projection of a ManifestStop.
|
|
10
|
+
*
|
|
11
|
+
* Carries the place inline rather than as an id. A driver app rendering a route
|
|
12
|
+
* needs a name and a coordinate for every stop, and making it resolve each one
|
|
13
|
+
* separately turns a route of twenty stops into twenty-one requests.
|
|
14
|
+
*
|
|
15
|
+
* `sequence` is the order to drive them in, and is the field a re-sequence
|
|
16
|
+
* rewrites.
|
|
17
|
+
*/
|
|
18
|
+
class ManifestStop extends FleetbaseResource
|
|
19
|
+
{
|
|
20
|
+
/**
|
|
21
|
+
* Transform the resource into an array.
|
|
22
|
+
*
|
|
23
|
+
* @param \Illuminate\Http\Request $request
|
|
24
|
+
*/
|
|
25
|
+
public function toArray($request): array
|
|
26
|
+
{
|
|
27
|
+
return [
|
|
28
|
+
'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
|
|
29
|
+
'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
|
|
30
|
+
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
31
|
+
'status' => $this->status,
|
|
32
|
+
'sequence' => $this->sequence,
|
|
33
|
+
'estimated_arrival' => $this->estimated_arrival,
|
|
34
|
+
'actual_arrival' => $this->actual_arrival,
|
|
35
|
+
'distance_from_prev_m' => $this->distance_from_prev_m,
|
|
36
|
+
'duration_from_prev_s' => $this->duration_from_prev_s,
|
|
37
|
+
'place' => new Place($this->whenLoaded('place')),
|
|
38
|
+
'order' => $this->when($this->relationLoaded('order') && $this->order, fn () => [
|
|
39
|
+
'id' => data_get($this->order, 'public_id'),
|
|
40
|
+
'tracking_number' => data_get($this->order, 'trackingNumber.tracking_number'),
|
|
41
|
+
'status' => data_get($this->order, 'status'),
|
|
42
|
+
]),
|
|
43
|
+
'updated_at' => $this->updated_at,
|
|
44
|
+
'created_at' => $this->created_at,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -41,10 +41,25 @@ class OrderConfig extends FleetbaseResource
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Project the flow JSON into
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
44
|
+
* Project the flow JSON into a list of activities, keeping the public-safe
|
|
45
|
+
* per-activity fields.
|
|
46
|
+
*
|
|
47
|
+
* `code` and `status` (the label) are the contract; `complete`, `color`,
|
|
48
|
+
* `details`, `pod_method` and `require_pod` are useful UI hints that ride
|
|
49
|
+
* along.
|
|
50
|
+
*
|
|
51
|
+
* `activities`, `sequence` and `logic` describe the flow's *shape*, and are
|
|
52
|
+
* published because without them the list cannot be sequenced. The stored
|
|
53
|
+
* flow is a directed graph — `activities` names the codes an activity can
|
|
54
|
+
* transition to, `sequence` orders activities reachable from the same
|
|
55
|
+
* parent, and `logic` gates availability — which is precisely what
|
|
56
|
+
* `OrderConfig::nextActivity()` walks server-side. Projecting them away
|
|
57
|
+
* left consumers with an unordered set: a client rendering progress from
|
|
58
|
+
* array position marks a dispatched order complete whenever `completed`
|
|
59
|
+
* happens to be declared earlier, and can offer no next step at all.
|
|
60
|
+
*
|
|
61
|
+
* These are descriptions of the configured workflow, not internal state, so
|
|
62
|
+
* there is nothing here a consumer of the config should not see.
|
|
48
63
|
*/
|
|
49
64
|
protected function projectFlow(): array
|
|
50
65
|
{
|
|
@@ -66,9 +81,41 @@ class OrderConfig extends FleetbaseResource
|
|
|
66
81
|
'complete' => (bool) ($activity['complete'] ?? false),
|
|
67
82
|
'pod_method' => $activity['pod_method'] ?? null,
|
|
68
83
|
'require_pod' => (bool) ($activity['require_pod'] ?? false),
|
|
84
|
+
'sequence' => isset($activity['sequence']) ? (int) $activity['sequence'] : null,
|
|
85
|
+
'activities' => static::projectTransitions($activity),
|
|
86
|
+
'logic' => $activity['logic'] ?? null,
|
|
69
87
|
];
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
return $activities;
|
|
73
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The codes an activity can transition to, as a list of strings.
|
|
95
|
+
*
|
|
96
|
+
* Stored flows have been written both ways over time: a bare list of codes,
|
|
97
|
+
* and a list of objects carrying a `code`. Normalising here means consumers
|
|
98
|
+
* do not each have to guess which one they were handed.
|
|
99
|
+
*/
|
|
100
|
+
protected static function projectTransitions(array $activity): array
|
|
101
|
+
{
|
|
102
|
+
$transitions = $activity['activities'] ?? [];
|
|
103
|
+
if (!is_array($transitions)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$codes = [];
|
|
108
|
+
foreach ($transitions as $transition) {
|
|
109
|
+
if (is_string($transition)) {
|
|
110
|
+
$codes[] = $transition;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (is_array($transition) && isset($transition['code']) && is_string($transition['code'])) {
|
|
115
|
+
$codes[] = $transition['code'];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return $codes;
|
|
120
|
+
}
|
|
74
121
|
}
|
|
@@ -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
|
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');
|
|
@@ -601,3 +601,36 @@ test('api driver controller reports missing driver branches', function () {
|
|
|
601
601
|
'status' => 404,
|
|
602
602
|
]);
|
|
603
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
|
+
});
|