From b899cd9b6cc3cbfdedd4605203a57127be2a4642 Mon Sep 17 00:00:00 2001 From: Ronald A Richardson Date: Mon, 20 Apr 2026 22:08:21 -0400 Subject: [PATCH] fix: sync user/company to sandbox before creating test API key When a test/sandbox API key is created the request carries the Access-Console-Sandbox header, which causes SetupFleetbaseSession to switch the default database connection to 'sandbox'. The subsequent INSERT into api_credentials on the sandbox DB references user_uuid and company_uuid values that come from the production session. Because those rows do not necessarily exist in the sandbox database the foreign key constraint api_credentials_user_uuid_foreign (and the companion company_uuid constraint) fires and the insert fails with SQLSTATE[23000] 1452. Fix: override createRecord() in ApiCredentialController to detect the sandbox header and, before delegating to the generic create path, mirror the current user, company, and company_user pivot row from production into the sandbox DB using an on-demand upsert (the same pattern used by the sandbox:sync Artisan command). Foreign key checks are temporarily disabled during the upsert to avoid ordering issues, then re-enabled before the api_credentials insert proceeds. --- .../Internal/v1/ApiCredentialController.php | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/Http/Controllers/Internal/v1/ApiCredentialController.php b/src/Http/Controllers/Internal/v1/ApiCredentialController.php index a3a994e8..eb98af49 100644 --- a/src/Http/Controllers/Internal/v1/ApiCredentialController.php +++ b/src/Http/Controllers/Internal/v1/ApiCredentialController.php @@ -6,9 +6,14 @@ use Fleetbase\Http\Controllers\FleetbaseController; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Models\ApiCredential; +use Fleetbase\Models\Company; +use Fleetbase\Models\CompanyUser; +use Fleetbase\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; @@ -28,6 +33,132 @@ class ApiCredentialController extends FleetbaseController */ public $service = 'developers'; + /** + * Create a new API credential record. + * + * Overrides the generic createRecord to ensure that when a test/sandbox key + * is being created, the current user and company are mirrored into the sandbox + * database first. Without this, the foreign key constraint on `api_credentials.user_uuid` + * (which references `users.uuid` in the sandbox DB) will fail because the user + * and company rows only exist in the production database. + * + * @return \Illuminate\Http\Response + */ + public function createRecord(Request $request) + { + // Determine if this is a sandbox/test key creation request. + $isSandbox = \Fleetbase\Support\Utils::isTrue($request->header('Access-Console-Sandbox')); + + if ($isSandbox) { + // Ensure the current user and company exist in the sandbox DB before + // attempting the insert, so the FK constraints are satisfied. + $this->syncCurrentSessionToSandbox($request); + } + + return parent::createRecord($request); + } + + /** + * Mirrors the currently authenticated user, their company, and the company–user + * membership record into the sandbox database. + * + * This is a targeted, on-demand version of the `sandbox:sync` Artisan command, + * scoped only to the records needed to satisfy the foreign key constraints when + * inserting a new test-mode `api_credentials` row. + * + * @param Request $request + * + * @return void + */ + protected function syncCurrentSessionToSandbox(Request $request): void + { + $userUuid = session('user'); + $companyUuid = session('company'); + + if (!$userUuid || !$companyUuid) { + return; + } + + // Temporarily disable FK checks so we can upsert in any order. + Schema::connection('sandbox')->disableForeignKeyConstraints(); + + try { + // --- Sync User --- + $user = User::on('mysql')->withoutGlobalScopes()->where('uuid', $userUuid)->first(); + if ($user) { + $this->upsertModelToSandbox($user); + } + + // --- Sync Company --- + $company = Company::on('mysql')->withoutGlobalScopes()->where('uuid', $companyUuid)->first(); + if ($company) { + $this->upsertModelToSandbox($company); + } + + // --- Sync CompanyUser pivot --- + $companyUser = CompanyUser::on('mysql') + ->withoutGlobalScopes() + ->where('user_uuid', $userUuid) + ->where('company_uuid', $companyUuid) + ->first(); + if ($companyUser) { + $this->upsertModelToSandbox($companyUser); + } + } finally { + Schema::connection('sandbox')->enableForeignKeyConstraints(); + } + } + + /** + * Upserts a single Eloquent model record into the sandbox database. + * + * Mirrors the approach used in the `sandbox:sync` Artisan command: + * reduces the record to its fillable attributes, normalises datetime + * fields to strings, JSON-encodes any Json-cast columns, then performs + * an `updateOrInsert` keyed on `uuid`. + * + * @param \Illuminate\Database\Eloquent\Model $model + * + * @return void + */ + protected function upsertModelToSandbox(\Illuminate\Database\Eloquent\Model $model): void + { + $clone = collect($model->toArray()) + ->only($model->getFillable()) + ->toArray(); + + if (!isset($clone['uuid']) || !is_string($clone['uuid'])) { + return; + } + + // Normalise datetime columns to plain strings. + foreach ($clone as $key => $value) { + if (isset($clone[$key]) && Str::endsWith($key, '_at')) { + try { + $clone[$key] = Carbon::parse($clone[$key])->toDateTimeString(); + } catch (\Exception $e) { + $clone[$key] = null; + } + } + } + + // JSON-encode any Json-cast columns that are still arrays/objects. + $jsonColumns = collect($model->getCasts()) + ->filter(fn ($cast) => Str::contains($cast, 'Json')) + ->keys() + ->toArray(); + + foreach ($clone as $key => $value) { + if (in_array($key, $jsonColumns) && (is_object($value) || is_array($value))) { + $clone[$key] = json_encode($value); + } + } + + DB::connection('sandbox') + ->table($model->getTable()) + ->updateOrInsert(['uuid' => $clone['uuid']], $clone); + } + /** * Export the companies/users api credentials to excel or csv. *