821 lines
25 KiB
PHP
821 lines
25 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages;
|
|
|
|
use App\Jobs\ProviderConnectionHealthCheckJob;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPermission;
|
|
use App\Models\TenantOnboardingSession;
|
|
use App\Models\User;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Jobs\TenantOnboardingVerifyJob;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Services\Auth\RoleCapabilityMap;
|
|
use App\Services\TenantOnboardingAuditService;
|
|
use App\Services\TenantOnboardingSessionService;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\OperationRunService;
|
|
use App\Services\Providers\CredentialManager;
|
|
use App\Services\Providers\ProviderOperationStartGate;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\OperationRunStatus;
|
|
use Filament\Forms\Components\Checkbox;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Concerns\InteractsWithForms;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Schemas\Components\Wizard;
|
|
use Filament\Schemas\Components\Wizard\Step;
|
|
use Filament\Schemas\Schema;
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Support\Str;
|
|
|
|
class TenantOnboardingWizard extends Page implements HasForms
|
|
{
|
|
use InteractsWithForms;
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static ?string $slug = 'tenant-onboarding';
|
|
|
|
protected static ?string $title = 'Tenant onboarding';
|
|
|
|
protected string $view = 'filament.pages.tenant-onboarding-wizard';
|
|
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
public array $data = [];
|
|
|
|
public ?string $sessionId = null;
|
|
|
|
public ?int $tenantId = null;
|
|
|
|
public ?string $currentStep = null;
|
|
|
|
public ?int $verificationRunId = null;
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->authorizeAccess();
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenant = $this->resolveTenantFromRequest();
|
|
|
|
$sessionService = app(TenantOnboardingSessionService::class);
|
|
|
|
if (filled(request()->query('session'))) {
|
|
$session = $sessionService->resumeById($user, (string) request()->query('session'));
|
|
} else {
|
|
$session = $sessionService->startOrResume($user, $tenant);
|
|
}
|
|
|
|
$this->sessionId = (string) $session->getKey();
|
|
$this->tenantId = $session->tenant_id;
|
|
$this->currentStep = (string) $session->current_step;
|
|
|
|
$this->data = array_merge($this->data, $session->payload ?? []);
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
$this->data = array_merge($this->data, [
|
|
'name' => $tenant->name,
|
|
'tenant_id' => $tenant->tenant_id,
|
|
'domain' => $tenant->domain,
|
|
'environment' => $tenant->environment,
|
|
'app_client_id' => $tenant->app_client_id,
|
|
'app_certificate_thumbprint' => $tenant->app_certificate_thumbprint,
|
|
'app_notes' => $tenant->app_notes,
|
|
]);
|
|
}
|
|
|
|
$this->form->fill($this->data);
|
|
}
|
|
|
|
public function enqueueVerification(): void
|
|
{
|
|
$this->authorizeAccess();
|
|
|
|
$this->requireCapability(Capabilities::PROVIDER_RUN);
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenant = $this->requireTenant();
|
|
|
|
/** @var OperationRunService $runs */
|
|
$runs = app(OperationRunService::class);
|
|
|
|
$run = $runs->ensureRunWithIdentity(
|
|
tenant: $tenant,
|
|
type: 'tenant.rbac.verify',
|
|
identityInputs: [
|
|
'purpose' => 'tenant_rbac_verify',
|
|
],
|
|
context: [
|
|
'operation' => [
|
|
'type' => 'tenant.rbac.verify',
|
|
],
|
|
'target_scope' => [
|
|
'tenant_id' => $tenant->getKey(),
|
|
'entra_tenant_id' => $tenant->tenant_id,
|
|
],
|
|
],
|
|
initiator: $user,
|
|
);
|
|
|
|
$this->verificationRunId = (int) $run->getKey();
|
|
|
|
if ($run->wasRecentlyCreated) {
|
|
$runs->dispatchOrFail($run, function (OperationRun $run) use ($tenant, $user): void {
|
|
TenantOnboardingVerifyJob::dispatch(
|
|
tenantId: (int) $tenant->getKey(),
|
|
userId: (int) $user->getKey(),
|
|
operationRun: $run,
|
|
);
|
|
});
|
|
|
|
Notification::make()->title('Verification queued')->success()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()->title('Verification already in progress')->info()->send();
|
|
}
|
|
|
|
public function enqueueConnectionCheck(): void
|
|
{
|
|
$this->authorizeAccess();
|
|
|
|
$this->requireCapability(Capabilities::PROVIDER_RUN);
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenant = $this->requireTenant();
|
|
|
|
$connection = $this->ensureDefaultMicrosoftProviderConnection($tenant);
|
|
|
|
/** @var ProviderOperationStartGate $gate */
|
|
$gate = app(ProviderOperationStartGate::class);
|
|
|
|
$result = $gate->start(
|
|
tenant: $tenant,
|
|
connection: $connection,
|
|
operationType: 'provider.connection.check',
|
|
dispatcher: function (OperationRun $operationRun) use ($tenant, $user, $connection): void {
|
|
ProviderConnectionHealthCheckJob::dispatch(
|
|
tenantId: (int) $tenant->getKey(),
|
|
userId: (int) $user->getKey(),
|
|
providerConnectionId: (int) $connection->getKey(),
|
|
operationRun: $operationRun,
|
|
);
|
|
},
|
|
initiator: $user,
|
|
);
|
|
|
|
if ($result->status === 'scope_busy') {
|
|
Notification::make()->title('Scope busy')->warning()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if ($result->status === 'deduped') {
|
|
Notification::make()->title('Connection check already queued')->info()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()->title('Connection check queued')->success()->send();
|
|
}
|
|
|
|
public function form(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->statePath('data')
|
|
->components([
|
|
Wizard::make($this->getSteps())
|
|
->startOnStep(fn (): int => $this->getStartStep())
|
|
->submitAction('')
|
|
->cancelAction(''),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, Step>
|
|
*/
|
|
private function getSteps(): array
|
|
{
|
|
$steps = [
|
|
Step::make('Welcome')
|
|
->id('welcome')
|
|
->description('Requirements and what to expect.')
|
|
->schema([
|
|
\Filament\Forms\Components\Placeholder::make('welcome_copy')
|
|
->label('')
|
|
->content('This wizard will create or update a tenant record without making any outbound calls. You can resume at any time.'),
|
|
])
|
|
->afterValidation(fn (): mixed => $this->persistStep('tenant_details')),
|
|
|
|
Step::make('Tenant Details')
|
|
->id('tenant_details')
|
|
->description('Basic tenant metadata')
|
|
->schema([
|
|
TextInput::make('name')
|
|
->label('Display name')
|
|
->required()
|
|
->maxLength(255),
|
|
Select::make('environment')
|
|
->options([
|
|
'prod' => 'PROD',
|
|
'dev' => 'DEV',
|
|
'staging' => 'STAGING',
|
|
'other' => 'Other',
|
|
])
|
|
->default('other')
|
|
->required(),
|
|
TextInput::make('tenant_id')
|
|
->label('Tenant ID (GUID)')
|
|
->required()
|
|
->rule('uuid')
|
|
->maxLength(255),
|
|
TextInput::make('domain')
|
|
->label('Primary domain')
|
|
->maxLength(255),
|
|
])
|
|
->afterValidation(fn (): mixed => $this->handleTenantDetailsCompleted()),
|
|
];
|
|
|
|
$steps = array_merge($steps, $this->credentialsRequired()
|
|
? [$this->credentialsStep()]
|
|
: []);
|
|
|
|
$steps[] = Step::make('Admin Consent & Permissions')
|
|
->id('permissions')
|
|
->description('Grant permissions and verify access')
|
|
->schema([
|
|
\Filament\Forms\Components\Placeholder::make('permissions_copy')
|
|
->label('')
|
|
->content('Next, you will grant admin consent and verify permissions. (Verification runs are implemented in the next phase.)'),
|
|
])
|
|
->afterValidation(fn (): mixed => $this->persistStep('verification'));
|
|
|
|
$steps[] = Step::make('Verification / First Run')
|
|
->id('verification')
|
|
->description('Finish setup and validate readiness')
|
|
->schema([
|
|
\Filament\Forms\Components\Placeholder::make('verification_copy')
|
|
->label('')
|
|
->content('Verification checks are enqueue-only and will appear here once implemented.'),
|
|
])
|
|
->afterValidation(fn (): mixed => $this->persistStep('verification'));
|
|
|
|
return $steps;
|
|
}
|
|
|
|
private function credentialsStep(): Step
|
|
{
|
|
return Step::make('App / Credentials')
|
|
->id('credentials')
|
|
->description('Set credentials (if required)')
|
|
->schema([
|
|
TextInput::make('app_client_id')
|
|
->label('App Client ID')
|
|
->required()
|
|
->maxLength(255),
|
|
TextInput::make('app_client_secret')
|
|
->label('App Client Secret')
|
|
->password()
|
|
->required()
|
|
->maxLength(255),
|
|
TextInput::make('app_certificate_thumbprint')
|
|
->label('Certificate thumbprint')
|
|
->maxLength(255),
|
|
Textarea::make('app_notes')
|
|
->label('Notes')
|
|
->rows(3),
|
|
Checkbox::make('acknowledge_credentials')
|
|
->label('I understand this will store credentials encrypted and they cannot be shown again.')
|
|
->accepted()
|
|
->required(),
|
|
])
|
|
->afterValidation(fn (): mixed => $this->handleCredentialsCompleted());
|
|
}
|
|
|
|
private function handleTenantDetailsCompleted(): void
|
|
{
|
|
$this->authorizeAccess();
|
|
$this->requireCapability(Capabilities::TENANT_MANAGE);
|
|
|
|
$tenant = $this->upsertTenantFromData();
|
|
|
|
$this->ensureDefaultMicrosoftProviderConnection($tenant);
|
|
|
|
$this->tenantId = (int) $tenant->getKey();
|
|
|
|
$nextStep = $this->credentialsRequired() ? 'credentials' : 'permissions';
|
|
|
|
$this->persistStep($nextStep, $tenant);
|
|
|
|
Notification::make()->title('Tenant details saved')->success()->send();
|
|
}
|
|
|
|
private function handleCredentialsCompleted(): void
|
|
{
|
|
$this->authorizeAccess();
|
|
$this->requireCapability(Capabilities::TENANT_MANAGE);
|
|
|
|
$tenant = $this->requireTenant();
|
|
|
|
$secret = (string) ($this->data['app_client_secret'] ?? '');
|
|
|
|
$tenant->forceFill([
|
|
'app_client_id' => $this->data['app_client_id'] ?? null,
|
|
'app_certificate_thumbprint' => $this->data['app_certificate_thumbprint'] ?? null,
|
|
'app_notes' => $this->data['app_notes'] ?? null,
|
|
]);
|
|
|
|
if (filled($secret)) {
|
|
$tenant->forceFill(['app_client_secret' => $secret]);
|
|
}
|
|
|
|
$tenant->save();
|
|
|
|
if (filled($secret) && filled($tenant->app_client_id) && filled($tenant->tenant_id)) {
|
|
$connection = $this->ensureDefaultMicrosoftProviderConnection($tenant);
|
|
|
|
app(CredentialManager::class)->upsertClientSecretCredential(
|
|
connection: $connection,
|
|
clientId: (string) $tenant->app_client_id,
|
|
clientSecret: (string) $secret,
|
|
);
|
|
}
|
|
|
|
if (filled($secret)) {
|
|
$actor = auth()->user();
|
|
|
|
app(TenantOnboardingAuditService::class)->credentialsUpdated(
|
|
tenant: $tenant,
|
|
actor: $actor instanceof User ? $actor : null,
|
|
context: [
|
|
'app_client_id_set' => filled($tenant->app_client_id),
|
|
'app_client_secret_set' => true,
|
|
],
|
|
);
|
|
}
|
|
|
|
$this->data['app_client_secret'] = null;
|
|
$this->form->fill($this->data);
|
|
|
|
$this->persistStep('permissions', $tenant);
|
|
|
|
Notification::make()->title('Credentials saved')->success()->send();
|
|
}
|
|
|
|
private function persistStep(string $currentStep, ?Tenant $tenant = null): void
|
|
{
|
|
$this->authorizeAccess();
|
|
$this->requireCapability(Capabilities::TENANT_MANAGE);
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$session = $this->requireSession();
|
|
|
|
$service = app(TenantOnboardingSessionService::class);
|
|
|
|
$updated = $service->persistProgress(
|
|
session: $session,
|
|
currentStep: $currentStep,
|
|
payload: $this->data,
|
|
tenant: $tenant,
|
|
);
|
|
|
|
$this->sessionId = (string) $updated->getKey();
|
|
$this->tenantId = $updated->tenant_id;
|
|
$this->currentStep = (string) $updated->current_step;
|
|
}
|
|
|
|
/**
|
|
* DB-only: uses config + stored tenant_permissions.
|
|
*
|
|
* @return array<int, array{key:string,type:string,description:?string,features:array<int,string>,status:string}>
|
|
*/
|
|
public function permissionRows(): array
|
|
{
|
|
if (! $this->tenantId) {
|
|
return [];
|
|
}
|
|
|
|
$tenant = Tenant::query()->whereKey($this->tenantId)->first();
|
|
if (! $tenant instanceof Tenant) {
|
|
return [];
|
|
}
|
|
|
|
$required = config('intune_permissions.permissions', []);
|
|
$required = is_array($required) ? $required : [];
|
|
|
|
$granted = TenantPermission::query()
|
|
->where('tenant_id', $tenant->getKey())
|
|
->get()
|
|
->keyBy('permission_key');
|
|
|
|
$rows = [];
|
|
|
|
foreach ($required as $permission) {
|
|
if (! is_array($permission)) {
|
|
continue;
|
|
}
|
|
|
|
$key = (string) ($permission['key'] ?? '');
|
|
if ($key === '') {
|
|
continue;
|
|
}
|
|
|
|
$stored = $granted->get($key);
|
|
$status = $stored instanceof TenantPermission
|
|
? (string) $stored->status
|
|
: 'missing';
|
|
|
|
$rows[] = [
|
|
'key' => $key,
|
|
'type' => (string) ($permission['type'] ?? 'application'),
|
|
'description' => isset($permission['description']) && is_string($permission['description']) ? $permission['description'] : null,
|
|
'features' => is_array($permission['features'] ?? null) ? $permission['features'] : [],
|
|
'status' => $status,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
public function latestVerificationRunStatus(): ?string
|
|
{
|
|
if (! $this->tenantId) {
|
|
return null;
|
|
}
|
|
|
|
$run = OperationRun::query()
|
|
->where('tenant_id', $this->tenantId)
|
|
->where('type', 'tenant.rbac.verify')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if (! $run instanceof OperationRun) {
|
|
return null;
|
|
}
|
|
|
|
return (string) $run->status;
|
|
}
|
|
|
|
public function latestConnectionCheckRunStatus(): ?string
|
|
{
|
|
if (! $this->tenantId) {
|
|
return null;
|
|
}
|
|
|
|
$tenant = Tenant::query()->whereKey($this->tenantId)->first();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return null;
|
|
}
|
|
|
|
$connection = $tenant->providerConnections()
|
|
->where('provider', 'microsoft')
|
|
->where('entra_tenant_id', $tenant->tenant_id)
|
|
->orderByDesc('is_default')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if (! $connection instanceof ProviderConnection) {
|
|
return null;
|
|
}
|
|
|
|
$run = OperationRun::query()
|
|
->where('tenant_id', $tenant->getKey())
|
|
->where('type', 'provider.connection.check')
|
|
->where('context->provider_connection_id', (int) $connection->getKey())
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if (! $run instanceof OperationRun) {
|
|
return null;
|
|
}
|
|
|
|
return (string) $run->status;
|
|
}
|
|
|
|
public function isReadyToCompleteOnboarding(): bool
|
|
{
|
|
if (! $this->tenantId) {
|
|
return false;
|
|
}
|
|
|
|
$tenant = Tenant::query()->whereKey($this->tenantId)->first();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return false;
|
|
}
|
|
|
|
$connection = $tenant->providerConnections()
|
|
->where('provider', 'microsoft')
|
|
->where('entra_tenant_id', $tenant->tenant_id)
|
|
->orderByDesc('is_default')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$connectionOk = $connection instanceof ProviderConnection
|
|
&& (string) $connection->health_status === 'ok'
|
|
&& (string) $connection->status === 'connected';
|
|
|
|
$permissionsOk = collect($this->permissionRows())
|
|
->every(fn (array $row): bool => (string) ($row['status'] ?? 'missing') === 'granted');
|
|
|
|
$verifyRunOk = OperationRun::query()
|
|
->where('tenant_id', $tenant->getKey())
|
|
->where('type', 'tenant.rbac.verify')
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('outcome', 'succeeded')
|
|
->exists();
|
|
|
|
return $connectionOk && $permissionsOk && $verifyRunOk;
|
|
}
|
|
|
|
private function ensureDefaultMicrosoftProviderConnection(Tenant $tenant): ProviderConnection
|
|
{
|
|
$existing = $tenant->providerConnections()
|
|
->where('provider', 'microsoft')
|
|
->where('entra_tenant_id', $tenant->tenant_id)
|
|
->orderByDesc('is_default')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if ($existing instanceof ProviderConnection) {
|
|
if (! $existing->is_default) {
|
|
$existing->makeDefault();
|
|
}
|
|
|
|
return $existing;
|
|
}
|
|
|
|
return ProviderConnection::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Microsoft Graph',
|
|
'is_default' => true,
|
|
'status' => 'needs_consent',
|
|
'health_status' => 'unknown',
|
|
'scopes_granted' => [],
|
|
'metadata' => [],
|
|
]);
|
|
}
|
|
|
|
private function upsertTenantFromData(): Tenant
|
|
{
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenantGuid = Str::lower((string) ($this->data['tenant_id'] ?? ''));
|
|
|
|
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->first();
|
|
$isNewTenant = ! $tenant instanceof Tenant;
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
$tenant = new Tenant();
|
|
$tenant->forceFill([
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
|
|
$tenant->forceFill([
|
|
'name' => $this->data['name'] ?? null,
|
|
'tenant_id' => $tenantGuid,
|
|
'domain' => $this->data['domain'] ?? null,
|
|
'environment' => $this->data['environment'] ?? 'other',
|
|
'onboarding_status' => 'in_progress',
|
|
'onboarding_completed_at' => null,
|
|
]);
|
|
|
|
try {
|
|
$tenant->save();
|
|
} catch (QueryException $exception) {
|
|
throw $exception;
|
|
}
|
|
|
|
$alreadyMember = $user->tenants()->whereKey($tenant->getKey())->exists();
|
|
|
|
if (! $alreadyMember) {
|
|
$user->tenants()->syncWithoutDetaching([
|
|
$tenant->getKey() => [
|
|
'role' => 'owner',
|
|
'source' => 'manual',
|
|
'created_by_user_id' => $user->getKey(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
if ($isNewTenant && ! $alreadyMember) {
|
|
app(AuditLogger::class)->log(
|
|
tenant: $tenant,
|
|
action: 'tenant_membership.bootstrap_assign',
|
|
context: [
|
|
'metadata' => [
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'owner',
|
|
'source' => 'manual',
|
|
],
|
|
],
|
|
actorId: (int) $user->getKey(),
|
|
actorEmail: $user->email,
|
|
actorName: $user->name,
|
|
status: 'success',
|
|
resourceType: 'tenant',
|
|
resourceId: (string) $tenant->getKey(),
|
|
);
|
|
}
|
|
|
|
return $tenant;
|
|
}
|
|
|
|
private function authorizeAccess(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenant = $this->resolveTenantFromRequest();
|
|
|
|
/** @var CapabilityResolver $resolver */
|
|
$resolver = app(CapabilityResolver::class);
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
if (! $resolver->isMember($user, $tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $resolver->can($user, $tenant, Capabilities::TENANT_VIEW)) {
|
|
abort(403);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// For creating a new tenant (not yet in scope), require that the user can manage at least one tenant.
|
|
$canManageAny = $user->tenantMemberships()
|
|
->pluck('role')
|
|
->contains(fn (mixed $role): bool => RoleCapabilityMap::hasCapability((string) $role, Capabilities::TENANT_MANAGE));
|
|
|
|
if (! $canManageAny) {
|
|
abort(403);
|
|
}
|
|
}
|
|
|
|
private function resolveTenantFromRequest(): ?Tenant
|
|
{
|
|
$tenantExternalId = request()->query('tenant');
|
|
|
|
if (! is_string($tenantExternalId) || blank($tenantExternalId)) {
|
|
return null;
|
|
}
|
|
|
|
return Tenant::query()
|
|
->where('external_id', $tenantExternalId)
|
|
->where('status', 'active')
|
|
->first();
|
|
}
|
|
|
|
private function credentialsRequired(): bool
|
|
{
|
|
return (bool) config('tenantpilot.onboarding.credentials_required', false);
|
|
}
|
|
|
|
private function getStartStep(): int
|
|
{
|
|
$session = $this->requireSession();
|
|
|
|
$keys = $this->getStepKeys();
|
|
$index = array_search((string) $session->current_step, $keys, true);
|
|
|
|
if ($index === false) {
|
|
return 1;
|
|
}
|
|
|
|
return $index + 1;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getStepKeys(): array
|
|
{
|
|
$keys = ['welcome', 'tenant_details'];
|
|
|
|
if ($this->credentialsRequired()) {
|
|
$keys[] = 'credentials';
|
|
}
|
|
|
|
$keys[] = 'permissions';
|
|
$keys[] = 'verification';
|
|
|
|
return $keys;
|
|
}
|
|
|
|
private function requireSession(): TenantOnboardingSession
|
|
{
|
|
if (! filled($this->sessionId)) {
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$this->sessionId = (string) app(TenantOnboardingSessionService::class)->startOrResume($user)->getKey();
|
|
}
|
|
|
|
return TenantOnboardingSession::query()->whereKey($this->sessionId)->firstOrFail();
|
|
}
|
|
|
|
private function requireTenant(): Tenant
|
|
{
|
|
if (! $this->tenantId) {
|
|
abort(400, 'Tenant not initialized');
|
|
}
|
|
|
|
return Tenant::query()->whereKey($this->tenantId)->firstOrFail();
|
|
}
|
|
|
|
public function tenantHasClientSecret(): bool
|
|
{
|
|
if (! $this->tenantId) {
|
|
return false;
|
|
}
|
|
|
|
$tenant = Tenant::query()->whereKey($this->tenantId)->first();
|
|
|
|
return $tenant instanceof Tenant && filled($tenant->getRawOriginal('app_client_secret'));
|
|
}
|
|
|
|
public function canRunProviderOperations(): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
$tenant = $this->resolveTenantForAuthorization();
|
|
|
|
return $tenant instanceof Tenant
|
|
&& app(CapabilityResolver::class)->can($user, $tenant, Capabilities::PROVIDER_RUN);
|
|
}
|
|
|
|
private function requireCapability(string $capability): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenant = $this->resolveTenantForAuthorization();
|
|
|
|
if (! $tenant instanceof Tenant || ! app(CapabilityResolver::class)->can($user, $tenant, $capability)) {
|
|
abort(403);
|
|
}
|
|
}
|
|
|
|
private function resolveTenantForAuthorization(): ?Tenant
|
|
{
|
|
if ($this->tenantId) {
|
|
$tenant = Tenant::query()->whereKey($this->tenantId)->first();
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
return $tenant;
|
|
}
|
|
}
|
|
|
|
return $this->resolveTenantFromRequest();
|
|
}
|
|
}
|