TenantAtlas/app/Services/TenantOnboardingSessionService.php
2026-02-01 12:20:09 +01:00

143 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Tenant;
use App\Models\TenantOnboardingSession;
use App\Models\User;
use Illuminate\Database\QueryException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class TenantOnboardingSessionService
{
/**
* Start a new onboarding session, or resume an existing active session.
*/
public function startOrResume(User $user, ?Tenant $tenant = null): TenantOnboardingSession
{
if ($tenant instanceof Tenant) {
$existing = TenantOnboardingSession::query()
->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<string, mixed> $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<string, mixed> $payload
* @return array<string, mixed>
*/
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<string, mixed> $payload
* @param array<int, string> $forbiddenKeys
* @return array<string, mixed>
*/
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');
}
}