66use Fleetbase \Http \Controllers \FleetbaseController ;
77use Fleetbase \Http \Requests \ExportRequest ;
88use Fleetbase \Models \ApiCredential ;
9+ use Fleetbase \Models \Company ;
10+ use Fleetbase \Models \CompanyUser ;
11+ use Fleetbase \Models \User ;
912use Illuminate \Http \Request ;
13+ use Illuminate \Support \Carbon ;
1014use Illuminate \Support \Facades \Auth ;
1115use Illuminate \Support \Facades \DB ;
16+ use Illuminate \Support \Facades \Schema ;
1217use Illuminate \Support \Str ;
1318use Maatwebsite \Excel \Facades \Excel ;
1419
@@ -28,6 +33,132 @@ class ApiCredentialController extends FleetbaseController
2833 */
2934 public $ service = 'developers ' ;
3035
36+ /**
37+ * Create a new API credential record.
38+ *
39+ * Overrides the generic createRecord to ensure that when a test/sandbox key
40+ * is being created, the current user and company are mirrored into the sandbox
41+ * database first. Without this, the foreign key constraint on `api_credentials.user_uuid`
42+ * (which references `users.uuid` in the sandbox DB) will fail because the user
43+ * and company rows only exist in the production database.
44+ *
45+ * @return \Illuminate\Http\Response
46+ */
47+ public function createRecord (Request $ request )
48+ {
49+ // Determine if this is a sandbox/test key creation request.
50+ $ isSandbox = \Fleetbase \Support \Utils::isTrue ($ request ->header ('Access-Console-Sandbox ' ));
51+
52+ if ($ isSandbox ) {
53+ // Ensure the current user and company exist in the sandbox DB before
54+ // attempting the insert, so the FK constraints are satisfied.
55+ $ this ->syncCurrentSessionToSandbox ($ request );
56+ }
57+
58+ return parent ::createRecord ($ request );
59+ }
60+
61+ /**
62+ * Mirrors the currently authenticated user, their company, and the company–user
63+ * membership record into the sandbox database.
64+ *
65+ * This is a targeted, on-demand version of the `sandbox:sync` Artisan command,
66+ * scoped only to the records needed to satisfy the foreign key constraints when
67+ * inserting a new test-mode `api_credentials` row.
68+ *
69+ * @param Request $request
70+ *
71+ * @return void
72+ */
73+ protected function syncCurrentSessionToSandbox (Request $ request ): void
74+ {
75+ $ userUuid = session ('user ' );
76+ $ companyUuid = session ('company ' );
77+
78+ if (!$ userUuid || !$ companyUuid ) {
79+ return ;
80+ }
81+
82+ // Temporarily disable FK checks so we can upsert in any order.
83+ Schema::connection ('sandbox ' )->disableForeignKeyConstraints ();
84+
85+ try {
86+ // --- Sync User ---
87+ $ user = User::on ('mysql ' )->withoutGlobalScopes ()->where ('uuid ' , $ userUuid )->first ();
88+ if ($ user ) {
89+ $ this ->upsertModelToSandbox ($ user );
90+ }
91+
92+ // --- Sync Company ---
93+ $ company = Company::on ('mysql ' )->withoutGlobalScopes ()->where ('uuid ' , $ companyUuid )->first ();
94+ if ($ company ) {
95+ $ this ->upsertModelToSandbox ($ company );
96+ }
97+
98+ // --- Sync CompanyUser pivot ---
99+ $ companyUser = CompanyUser::on ('mysql ' )
100+ ->withoutGlobalScopes ()
101+ ->where ('user_uuid ' , $ userUuid )
102+ ->where ('company_uuid ' , $ companyUuid )
103+ ->first ();
104+ if ($ companyUser ) {
105+ $ this ->upsertModelToSandbox ($ companyUser );
106+ }
107+ } finally {
108+ Schema::connection ('sandbox ' )->enableForeignKeyConstraints ();
109+ }
110+ }
111+
112+ /**
113+ * Upserts a single Eloquent model record into the sandbox database.
114+ *
115+ * Mirrors the approach used in the `sandbox:sync` Artisan command:
116+ * reduces the record to its fillable attributes, normalises datetime
117+ * fields to strings, JSON-encodes any Json-cast columns, then performs
118+ * an `updateOrInsert` keyed on `uuid`.
119+ *
120+ * @param \Illuminate\Database\Eloquent\Model $model
121+ *
122+ * @return void
123+ */
124+ protected function upsertModelToSandbox (\Illuminate \Database \Eloquent \Model $ model ): void
125+ {
126+ $ clone = collect ($ model ->toArray ())
127+ ->only ($ model ->getFillable ())
128+ ->toArray ();
129+
130+ if (!isset ($ clone ['uuid ' ]) || !is_string ($ clone ['uuid ' ])) {
131+ return ;
132+ }
133+
134+ // Normalise datetime columns to plain strings.
135+ foreach ($ clone as $ key => $ value ) {
136+ if (isset ($ clone [$ key ]) && Str::endsWith ($ key , '_at ' )) {
137+ try {
138+ $ clone [$ key ] = Carbon::parse ($ clone [$ key ])->toDateTimeString ();
139+ } catch (\Exception $ e ) {
140+ $ clone [$ key ] = null ;
141+ }
142+ }
143+ }
144+
145+ // JSON-encode any Json-cast columns that are still arrays/objects.
146+ $ jsonColumns = collect ($ model ->getCasts ())
147+ ->filter (fn ($ cast ) => Str::contains ($ cast , 'Json ' ))
148+ ->keys ()
149+ ->toArray ();
150+
151+ foreach ($ clone as $ key => $ value ) {
152+ if (in_array ($ key , $ jsonColumns ) && (is_object ($ value ) || is_array ($ value ))) {
153+ $ clone [$ key ] = json_encode ($ value );
154+ }
155+ }
156+
157+ DB ::connection ('sandbox ' )
158+ ->table ($ model ->getTable ())
159+ ->updateOrInsert (['uuid ' => $ clone ['uuid ' ]], $ clone );
160+ }
161+
31162 /**
32163 * Export the companies/users api credentials to excel or csv.
33164 *
0 commit comments