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,124 @@ 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+ protected function syncCurrentSessionToSandbox (Request $ request ): void
70+ {
71+ $ userUuid = session ('user ' );
72+ $ companyUuid = session ('company ' );
73+
74+ if (!$ userUuid || !$ companyUuid ) {
75+ return ;
76+ }
77+
78+ // Temporarily disable FK checks so we can upsert in any order.
79+ Schema::connection ('sandbox ' )->disableForeignKeyConstraints ();
80+
81+ try {
82+ // --- Sync User ---
83+ $ user = User::on ('mysql ' )->withoutGlobalScopes ()->where ('uuid ' , $ userUuid )->first ();
84+ if ($ user ) {
85+ $ this ->upsertModelToSandbox ($ user );
86+ }
87+
88+ // --- Sync Company ---
89+ $ company = Company::on ('mysql ' )->withoutGlobalScopes ()->where ('uuid ' , $ companyUuid )->first ();
90+ if ($ company ) {
91+ $ this ->upsertModelToSandbox ($ company );
92+ }
93+
94+ // --- Sync CompanyUser pivot ---
95+ $ companyUser = CompanyUser::on ('mysql ' )
96+ ->withoutGlobalScopes ()
97+ ->where ('user_uuid ' , $ userUuid )
98+ ->where ('company_uuid ' , $ companyUuid )
99+ ->first ();
100+ if ($ companyUser ) {
101+ $ this ->upsertModelToSandbox ($ companyUser );
102+ }
103+ } finally {
104+ Schema::connection ('sandbox ' )->enableForeignKeyConstraints ();
105+ }
106+ }
107+
108+ /**
109+ * Upserts a single Eloquent model record into the sandbox database.
110+ *
111+ * Mirrors the approach used in the `sandbox:sync` Artisan command:
112+ * reduces the record to its fillable attributes, normalises datetime
113+ * fields to strings, JSON-encodes any Json-cast columns, then performs
114+ * an `updateOrInsert` keyed on `uuid`.
115+ */
116+ protected function upsertModelToSandbox (\Illuminate \Database \Eloquent \Model $ model ): void
117+ {
118+ $ clone = collect ($ model ->toArray ())
119+ ->only ($ model ->getFillable ())
120+ ->toArray ();
121+
122+ if (!isset ($ clone ['uuid ' ]) || !is_string ($ clone ['uuid ' ])) {
123+ return ;
124+ }
125+
126+ // Normalise datetime columns to plain strings.
127+ foreach ($ clone as $ key => $ value ) {
128+ if (isset ($ clone [$ key ]) && Str::endsWith ($ key , '_at ' )) {
129+ try {
130+ $ clone [$ key ] = Carbon::parse ($ clone [$ key ])->toDateTimeString ();
131+ } catch (\Exception $ e ) {
132+ $ clone [$ key ] = null ;
133+ }
134+ }
135+ }
136+
137+ // JSON-encode any Json-cast columns that are still arrays/objects.
138+ $ jsonColumns = collect ($ model ->getCasts ())
139+ ->filter (fn ($ cast ) => Str::contains ($ cast , 'Json ' ))
140+ ->keys ()
141+ ->toArray ();
142+
143+ foreach ($ clone as $ key => $ value ) {
144+ if (in_array ($ key , $ jsonColumns ) && (is_object ($ value ) || is_array ($ value ))) {
145+ $ clone [$ key ] = json_encode ($ value );
146+ }
147+ }
148+
149+ DB ::connection ('sandbox ' )
150+ ->table ($ model ->getTable ())
151+ ->updateOrInsert (['uuid ' => $ clone ['uuid ' ]], $ clone );
152+ }
153+
31154 /**
32155 * Export the companies/users api credentials to excel or csv.
33156 *
0 commit comments