where('tenant_id', $tenant->getKey()) ->where('status', 'active') ->first(); if ($existing instanceof TenantOnboardingSession) { return $existing; } } return TenantOnboardingSession::query()->create([ 'tenant_id' => $tenant?->getKey(), 'created_by_user_id' => $user->getKey(), 'status' => 'active', 'current_step' => 'welcome', 'payload' => [], ]); } public function resumeById(User $user, string $sessionId): TenantOnboardingSession { $session = TenantOnboardingSession::query()->whereKey($sessionId)->firstOrFail(); if ((int) $session->created_by_user_id !== (int) $user->getKey()) { abort(404); } return $session; } /** * Persist wizard progress + non-secret payload. * * @param array $payload */ public function persistProgress(TenantOnboardingSession $session, string $currentStep, array $payload, ?Tenant $tenant = null): TenantOnboardingSession { $payload = $this->sanitizePayload($payload); return DB::transaction(function () use ($session, $currentStep, $payload, $tenant): TenantOnboardingSession { $session->forceFill([ 'current_step' => $currentStep, 'payload' => array_merge($session->payload ?? [], $payload), ]); if ($tenant instanceof Tenant) { $session->tenant()->associate($tenant); } try { $session->save(); } catch (QueryException $exception) { // If another active session already exists for the tenant, resume it. if (($tenant instanceof Tenant) && $this->isActiveSessionUniqueViolation($exception)) { $existing = TenantOnboardingSession::query() ->where('tenant_id', $tenant->getKey()) ->where('status', 'active') ->first(); if ($existing instanceof TenantOnboardingSession) { return $existing; } } throw $exception; } return $session; }); } /** * @param array $payload * @return array */ public function sanitizePayload(array $payload): array { $forbiddenKeys = [ 'app_client_secret', 'client_secret', 'secret', 'token', 'access_token', 'refresh_token', 'password', ]; return $this->forgetKeysRecursive($payload, $forbiddenKeys); } /** * @param array $payload * @param array $forbiddenKeys * @return array */ private function forgetKeysRecursive(array $payload, array $forbiddenKeys): array { foreach ($forbiddenKeys as $key) { Arr::forget($payload, $key); } foreach ($payload as $key => $value) { if (! is_array($value)) { continue; } $payload[$key] = $this->forgetKeysRecursive($value, $forbiddenKeys); } return $payload; } private function isActiveSessionUniqueViolation(QueryException $exception): bool { $message = Str::lower($exception->getMessage()); return str_contains($message, 'tenant_onboarding_sessions_active_unique') || str_contains($message, 'unique') && str_contains($message, 'tenant_onboarding_sessions'); } }