Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8e8ec02
fix(events): publish driver-assigned records on driver.{id}
roncodes Oct 15, 2026
777c68d
fix: key API model caches by database connection so live and sandbox …
roncodes Oct 15, 2026
d367196
chore(release): v1.6.71
roncodes Oct 15, 2026
96adddf
Merge pull request #311 from fleetbase/fix/driver-channel-for-assigne…
roncodes Oct 15, 2026
f4693d4
Merge pull request #312 from fleetbase/fix/api-cache-sandbox-isolation
roncodes Oct 15, 2026
c6cffc4
feat(socket): admin socket settings with signed config for the socket…
roncodes Oct 15, 2026
807096c
style(socket): use the imported socket classes in SettingController
roncodes Oct 15, 2026
249e0fa
test(socket): bind the test container before the admin-guard test bui…
roncodes Oct 15, 2026
593518a
fix(find): key the filter and request caches by the controller's own …
roncodes Oct 15, 2026
aa47cc7
docs(release): link the filter cache fix
roncodes Oct 15, 2026
b0932ed
Merge pull request #314 from fleetbase/feature/socket-admin-settings
roncodes Oct 15, 2026
719b438
Merge pull request #315 from fleetbase/fix/find-filter-cache-per-mode…
roncodes Oct 15, 2026
0d3877c
feat(categories): include parent_uuid in internal category responses
roncodes Oct 15, 2026
c31a3f0
docs(release): link the category parent_uuid change
roncodes Oct 15, 2026
a0e0dcf
Merge pull request #316 from fleetbase/feat/category-parent-uuid
roncodes Oct 16, 2026
8749fea
docs(release): v1.6.71 notes cover every PR in the release branch
roncodes Oct 16, 2026
997a5eb
ci: retrigger the checks that the Actions billing lock refused to start
roncodes Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# v1.6.70 — Text a verification code to the number being verified
# v1.6.71 — Socket settings from the console; live and test-mode API caches kept apart; driver-assigned events on driver channels

## Changed
## Added

- **`VerificationCode::generateSmsVerificationFor()` takes a `phone` option.** The code is texted to that number instead of the subject's own, and the SMS guard counts and traces the send against it. A customer changing their phone number now gets the code on the new number, including a customer with no number on file, who previously got nothing while the request reported success. Without the option nothing changes: the subject's phone is used, and a subject with no phone still gets a saved code and no text.
- **Socket settings from the console.** System administrators manage realtime channel authentication without touching the environment: `GET`/`POST int/v1/settings/socket` read and save the switch, the mode (`off`, `log`, `enforce`), the token TTL, the allowed origins and the publisher origin. Each value falls back to the environment when unset, so nothing changes until an administrator saves something. A signed `GET int/v1/socket/config` lets the socket server (fleetbase/fleetbase v0.7.70) poll the effective config and apply it at runtime, no restart. `SOCKETCLUSTER_AUTH_KEY` stays in the environment and is never stored or returned; turning the switch on without a key is refused. Every save is written to the activity log. (#314)
- **Category `parent_uuid` on internal responses.** The internal Category resource now includes `parent_uuid`, so a console can build a category tree from a flat list (Storefront's nested product categories). (#316)

## Fixed

- **A controller's filter is used for every query it serves.** `Find::httpFilterForModel()` cached its answer per model class for the life of the process, ignoring the filter a controller sets on its model (`$filter`) and whether the request was internal. Under Octane, the first controller to list a model decided the filter for every other: Fleet-Ops' order filter was applied to Storefront's order lists, so a network's orders page listed every order in the company. The cache is now keyed by the model's own filter, and a filter found by convention is picked per request audience. `Find::httpRequestForModel()` is keyed by the model's own request class too. (#315)
- **Live and test-mode data no longer share API cache entries.** List, model and relationship cache keys now carry the database connection (`{api_query}:mysql:…` / `{api_query}:sandbox:…`). Before, a console request in test mode could be served the cached live response for the same query (and the reverse), because console sessions carry no API credential to tell the two apart. (#312)
- **Driver-assigned lifecycle events reach `driver.{public_id}` and `driver.{uuid}`.** Order events for records with an assigned driver were only published on `driverAssigned.*`, which clients following the `{type}.{id}` convention never subscribe to. `driverAssigned.*` is still published and is now deprecated. (#311)

## Upgrade Steps

- None. The option is additive.
- fleetbase/storefront v0.4.25 requires this release (`fleetbase/core-api ^1.6.70`) for its phone change endpoint.
- Socket settings: nothing changes until an administrator saves one; the environment values stay the defaults. A socket server older than fleetbase/fleetbase v0.7.70 keeps reading its own environment.
- Otherwise none. Existing cache entries keep their old keys and expire within their TTL (5 minutes for lists, 1 hour for models); `deploy.sh` already runs `php artisan cache:clear`.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
"version": "1.6.70",
"version": "1.6.71",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down
6 changes: 6 additions & 0 deletions config/broadcasting.connections.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
'auth_key' => env('SOCKETCLUSTER_AUTH_KEY'),
'publish_url' => env('SOCKETCLUSTER_PUBLISH_URL', 'http://' . env('SOCKETCLUSTER_HOST', 'socket') . ':8001'),
'token_ttl' => (int) env('SOCKETCLUSTER_TOKEN_TTL', 900),

// Environment fallbacks for the admin socket settings (Configure -> Socket). The
// socket server reads these itself; the API reads them only to report the effective
// values. Blank means "the socket server's own default".
'auth_mode' => env('SOCKETCLUSTER_AUTH_MODE'),
'socket_options' => env('SOCKETCLUSTER_OPTIONS'),
],

// for apple apn
Expand Down
48 changes: 40 additions & 8 deletions src/Events/ResourceLifecycleEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,26 @@ public function broadcastOn(): array|Channel
$this->addRelationshipSpecificChannels($model, $channels);
$this->addChatChannels($model, $channels);

return $channels;
return static::uniqueChannels($channels);
}

/**
* Drops channels whose name is already in the list, keeping the first, so the event goes out
* once per channel name (e.g. `company.{uuid}` from both the session and the model, or a
* driver's own `driver.{id}` channel).
*
* @param array<int, Channel> $channels
*
* @return array<int, Channel>
*/
public static function uniqueChannels(array $channels): array
{
$unique = [];
foreach ($channels as $channel) {
$unique[(string) $channel] ??= $channel;
}

return array_values($unique);
}

/**
Expand Down Expand Up @@ -218,20 +237,33 @@ protected function addApiCredentialChannel($model, &$channels)
/**
* Adds channels related to relationships defined in the model such as driver, customer, facilitator, and vendor.
*
* A record assigned to a driver is published on the driver's own channel (`driver.{public_id}`
* and `driver.{uuid}`), next to the driver's location and geofence events.
*
* Deprecated: `driverAssigned.{uuid}` and `driverAssigned.{public_id}` are still published so
* existing subscribers keep working. New clients should subscribe to `driver.{public_id}`.
*
* @param object $model the model related to the event
* @param array $channels reference to the channels array
*/
protected function addRelationshipSpecificChannels($model, &$channels)
{
$relationships = ['driverAssigned', 'customer', 'facilitator', 'vendor'];
foreach ($relationships as $relationship) {
$relationships = [
'driverAssigned' => ['driver', 'driverAssigned'],
'customer' => ['customer'],
'facilitator' => ['facilitator'],
'vendor' => ['vendor'],
];
foreach ($relationships as $relationship => $prefixes) {
// Foreign keys are snake case (`driver_assigned_uuid`), the relationship names are camel case.
$foreignKey = Str::snake($relationship) . '_uuid';
if ($model && isset($model->{$foreignKey})) {
$channels[] = new Channel($relationship . '.' . $model->{$foreignKey});
}
if ($model && isset($model->{$relationship})) {
$channels[] = new Channel($relationship . '.' . $model->{$relationship}->public_id);
foreach ($prefixes as $prefix) {
if ($model && isset($model->{$foreignKey})) {
$channels[] = new Channel($prefix . '.' . $model->{$foreignKey});
}
if ($model && isset($model->{$relationship})) {
$channels[] = new Channel($prefix . '.' . $model->{$relationship}->public_id);
}
}
}

Expand Down
57 changes: 55 additions & 2 deletions src/Http/Controllers/Internal/v1/SettingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
use Fleetbase\Services\OAuth\OAuthFlowService;
use Fleetbase\Services\SmsService;
use Fleetbase\Support\PlatformApi;
use Fleetbase\Support\SocketCluster\SocketClusterService;
use Fleetbase\Support\SocketCluster\SocketSettings;
use Fleetbase\Support\SocketCluster\SocketToken;
use Fleetbase\Support\Utils;
use Fleetbase\Twilio\Manager as TwilioManager;
use Fleetbase\Twilio\Support\Laravel\Facade as TwilioFacade;
Expand Down Expand Up @@ -1052,7 +1055,7 @@ public function testSentryConfig(AdminRequest $request)
public function testSocketcluster(AdminRequest $request)
{
$userUuid = session('user');
$scoped = \Fleetbase\Support\SocketCluster\SocketToken::enabled();
$scoped = SocketToken::enabled();

if ($scoped && (!is_string($userUuid) || $userUuid === '')) {
return response()->json([
Expand All @@ -1070,7 +1073,7 @@ public function testSocketcluster(AdminRequest $request)
$sent = false;
$response = null;

$socketClusterClient = new \Fleetbase\Support\SocketCluster\SocketClusterService();
$socketClusterClient = new SocketClusterService();

try {
$sent = $socketClusterClient->send($channel, [
Expand Down Expand Up @@ -1103,6 +1106,56 @@ public function testSocketcluster(AdminRequest $request)
);
}

/**
* Socket server settings: the effective values with their sources, and the socket
* server's live status.
*
* The auth key is never part of the response; `settings.key_configured` says whether it is set.
*/
public function getSocketSettings(AdminRequest $request)
{
return response()->json($this->socketSettingsPayload());
}

/**
* Save any subset of the socket settings; a null value clears it back to the environment.
*
* Switching socket authentication on needs SOCKETCLUSTER_AUTH_KEY, which can only be set in
* the environment, so it is refused (422 `key_missing`) without one.
*/
public function saveSocketSettings(AdminRequest $request)
{
$validated = $request->validate(SocketSettings::rules());

if (($validated['enabled'] ?? null) !== null && filter_var($validated['enabled'], FILTER_VALIDATE_BOOLEAN) && SocketToken::key() === null) {
return response()->error('Socket authentication cannot be switched on until SOCKETCLUSTER_AUTH_KEY is set on the API and the socket server.', 422, ['code' => 'key_missing']);
}

$change = SocketSettings::store($validated);

activity('settings')
->causedBy($request->user())
->event('socket_settings_updated')
->withProperties(['fields' => array_keys(array_intersect_key($validated, array_flip(SocketSettings::FIELDS))), 'old' => $change['before'], 'new' => $change['after']])
->log('Socket settings updated');

return response()->json($this->socketSettingsPayload());
}

/**
* @return array{settings: array, status: ?array, status_error: ?string}
*/
protected function socketSettingsPayload(): array
{
$stats = SocketClusterService::stats();

return [
'settings' => SocketSettings::effective(),
'status' => $stats['status'],
'status_error' => $stats['error'],
];
}

/**
* OAuth configuration, as an administrator should see it.
*
Expand Down
23 changes: 21 additions & 2 deletions src/Http/Controllers/SocketAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
use Fleetbase\Support\SocketCluster\ChannelDecision;
use Fleetbase\Support\SocketCluster\SocketChannelRegistry;
use Fleetbase\Support\SocketCluster\SocketPrincipal;
use Fleetbase\Support\SocketCluster\SocketSettings;
use Fleetbase\Support\SocketCluster\SocketToken;
use Fleetbase\Support\Utils;
use Illuminate\Http\Request;
use Laravel\Sanctum\PersonalAccessToken;

/**
* Socket token minting for console, API and platform clients, and the channel authorize
* endpoint the socket server calls. Every action answers 404 while SOCKETCLUSTER_AUTH_KEY
* is unset, which clients read as "connect anonymously".
* and config endpoints the socket server calls. Every action answers 404 while
* SOCKETCLUSTER_AUTH_KEY is unset, which clients read as "connect anonymously".
*/
class SocketAuthController extends Controller
{
Expand Down Expand Up @@ -106,6 +107,24 @@ public function authorizeChannel(Request $request)
return response()->json($decision->toArray());
}

/**
* GET int/v1/socket/config: the effective socket settings the socket server applies at runtime.
*
* Signed with the authorize key and answered while the switch is off, so the socket
* server learns when an administrator switches socket authentication on.
*/
public function config(Request $request)
{
$settings = SocketSettings::effective();

return response()->json([
'enabled' => $settings['enabled'],
'mode' => $settings['mode'],
'origins' => $settings['origins'],
'updated_at' => SocketSettings::updatedAt(),
]);
}

protected static function disabled()
{
return response()->json(['error' => 'Not Found'], 404);
Expand Down
17 changes: 12 additions & 5 deletions src/Http/Middleware/VerifySocketSignature.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,29 @@
/**
* Admits only requests the socket server signed with the derived authorize key.
*
* Used instead of session or token auth on the channel authorize endpoint, which only the
* socket server calls.
* Used instead of session or token auth on the endpoints only the socket server calls. They
* answer 404 while socket authentication is off; with the `key-only` parameter
* (`VerifySocketSignature:key-only`) a configured key is enough, whatever the switch.
*/
class VerifySocketSignature
{
public function handle(Request $request, \Closure $next)
public const KEY_ONLY = 'key-only';

public function handle(Request $request, \Closure $next, ?string $requirement = null)
{
if (!SocketToken::enabled()) {
$requireSwitch = $requirement !== self::KEY_ONLY;
$configured = $requireSwitch ? SocketToken::enabled() : SocketToken::key() !== null;

if (!$configured) {
return response()->json(['error' => 'Not Found'], 404);
}

$valid = SocketSignature::verify(
SocketSignature::AUTHORIZE,
$request->header(SocketSignature::HEADER_TIMESTAMP),
$request->header(SocketSignature::HEADER_SIGNATURE),
$request->getContent()
$request->getContent(),
$requireSwitch
);

if (!$valid) {
Expand Down
2 changes: 2 additions & 0 deletions src/Http/Resources/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function toArray($request)
'company_uuid' => $this->when(Http::isInternalRequest(), $this->company_uuid),
'owner_uuid' => $this->when(Http::isInternalRequest(), $this->owner_uuid),
'owner_type' => $this->when(Http::isInternalRequest(), $this->owner_type),
// Lets a console build a category tree from a flat list without loading each parent.
'parent_uuid' => $this->when(Http::isInternalRequest(), $this->parent_uuid),
'icon' => $this->icon,
'icon_url' => $this->icon_url,
'name' => $this->name,
Expand Down
Loading
Loading