schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\TextInput::make('tenant_id') ->label('Tenant ID (GUID)') ->required() ->maxLength(255) ->unique(ignoreRecord: true), Forms\Components\TextInput::make('domain') ->label('Primary domain') ->maxLength(255), Forms\Components\TextInput::make('app_client_id') ->label('App Client ID') ->maxLength(255), Forms\Components\TextInput::make('app_client_secret') ->label('App Client Secret') ->password() ->dehydrateStateUsing(fn ($state) => filled($state) ? $state : null) ->dehydrated(fn ($state) => filled($state)), Forms\Components\TextInput::make('app_certificate_thumbprint') ->label('Certificate thumbprint') ->maxLength(255), Forms\Components\Textarea::make('app_notes') ->label('Notes') ->rows(3), ]); } public static function table(Table $table): Table { return $table ->modifyQueryUsing(fn (\Illuminate\Database\Eloquent\Builder $query) => $query->withTrashed()) ->columns([ Tables\Columns\TextColumn::make('name') ->searchable(), Tables\Columns\TextColumn::make('tenant_id') ->label('Tenant ID') ->copyable() ->searchable(), Tables\Columns\TextColumn::make('domain') ->copyable() ->toggleable(), Tables\Columns\IconColumn::make('is_current') ->label('Current') ->boolean(), Tables\Columns\TextColumn::make('status') ->badge() ->sortable(), Tables\Columns\TextColumn::make('app_status') ->badge(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->since(), ]) ->filters([ Tables\Filters\TrashedFilter::make() ->label('Archive filter') ->placeholder('Active only') ->trueLabel('Active + archived') ->falseLabel('Archived only') ->default(true), Tables\Filters\SelectFilter::make('app_status') ->options([ 'ok' => 'OK', 'consent_required' => 'Consent required', 'error' => 'Error', 'unknown' => 'Unknown', ]), ]) ->actions([ Actions\ViewAction::make(), ActionGroup::make([ Actions\EditAction::make(), Actions\RestoreAction::make() ->label('Restore') ->color('success') ->successNotificationTitle('Tenant reactivated') ->after(function (Tenant $record, AuditLogger $auditLogger) { $auditLogger->log( tenant: $record, action: 'tenant.restored', resourceType: 'tenant', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['tenant_id' => $record->tenant_id]] ); }), Actions\Action::make('makeCurrent') ->label('Make current') ->color('success') ->icon('heroicon-o-check-circle') ->requiresConfirmation() ->visible(fn (Tenant $record) => $record->isActive() && ! $record->is_current) ->action(function (Tenant $record, AuditLogger $auditLogger) { $record->makeCurrent(); $auditLogger->log( tenant: $record, action: 'tenant.current_set', resourceType: 'tenant', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['tenant_id' => $record->tenant_id]] ); Notification::make() ->title('Current tenant updated') ->success() ->send(); }), Actions\Action::make('admin_consent') ->label('Admin consent') ->icon('heroicon-o-clipboard-document') ->url(fn (Tenant $record) => static::adminConsentUrl($record)) ->visible(fn (Tenant $record) => static::adminConsentUrl($record) !== null) ->openUrlInNewTab(), Actions\Action::make('verify') ->label('Verify configuration') ->icon('heroicon-o-check-badge') ->color('primary') ->requiresConfirmation() ->action(function ( Tenant $record, TenantConfigService $configService, TenantPermissionService $permissionService, AuditLogger $auditLogger ) { static::verifyTenant($record, $configService, $permissionService, $auditLogger); }), Actions\Action::make('archive') ->label('Deactivate') ->color('danger') ->icon('heroicon-o-archive-box-x-mark') ->requiresConfirmation() ->visible(fn (Tenant $record) => ! $record->trashed()) ->action(function (Tenant $record, AuditLogger $auditLogger) { $record->delete(); $auditLogger->log( tenant: $record, action: 'tenant.archived', resourceType: 'tenant', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['tenant_id' => $record->tenant_id]] ); Notification::make() ->title('Tenant deactivated') ->body('The tenant has been archived and hidden from lists.') ->success() ->send(); }), Actions\Action::make('forceDelete') ->label('Force delete') ->color('danger') ->icon('heroicon-o-trash') ->requiresConfirmation() ->visible(fn (?Tenant $record) => $record?->trashed()) ->action(function (?Tenant $record, AuditLogger $auditLogger) { if ($record === null) { return; } $tenant = Tenant::withTrashed()->find($record->id); if (! $tenant?->trashed()) { Notification::make() ->title('Tenant must be archived first') ->danger() ->send(); return; } $auditLogger->log( tenant: $tenant, action: 'tenant.force_deleted', resourceType: 'tenant', resourceId: (string) $tenant->id, status: 'success', context: ['metadata' => ['tenant_id' => $tenant->tenant_id]] ); $tenant->forceDelete(); Notification::make() ->title('Tenant permanently deleted') ->success() ->send(); }), ])->icon('heroicon-o-ellipsis-vertical'), ]) ->bulkActions([]) ->headerActions([]); } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Infolists\Components\TextEntry::make('name'), Infolists\Components\TextEntry::make('tenant_id')->label('Tenant ID')->copyable(), Infolists\Components\TextEntry::make('domain')->copyable(), Infolists\Components\TextEntry::make('app_client_id')->label('App Client ID')->copyable(), Infolists\Components\TextEntry::make('status')->badge(), Infolists\Components\TextEntry::make('app_status')->badge(), Infolists\Components\TextEntry::make('app_notes')->label('Notes'), Infolists\Components\TextEntry::make('created_at')->dateTime(), Infolists\Components\TextEntry::make('updated_at')->dateTime(), Infolists\Components\TextEntry::make('admin_consent_url') ->label('Admin consent URL') ->state(fn (Tenant $record) => static::adminConsentUrl($record)) ->visible(fn (?string $state) => filled($state)) ->copyable(), Infolists\Components\RepeatableEntry::make('permissions') ->label('Required permissions') ->state(fn (Tenant $record) => app(TenantPermissionService::class)->compare($record, persist: false)['permissions']) ->schema([ Infolists\Components\TextEntry::make('key')->label('Permission')->badge(), Infolists\Components\TextEntry::make('type')->badge(), Infolists\Components\TextEntry::make('features') ->label('Features') ->formatStateUsing(fn ($state) => is_array($state) ? implode(', ', $state) : (string) $state), Infolists\Components\TextEntry::make('status') ->badge(), ]) ->columnSpanFull(), ]); } public static function getPages(): array { return [ 'index' => Pages\ListTenants::route('/'), 'create' => Pages\CreateTenant::route('/create'), 'view' => Pages\ViewTenant::route('/{record}'), 'edit' => Pages\EditTenant::route('/{record}/edit'), ]; } public static function adminConsentUrl(Tenant $tenant): ?string { $tenantId = $tenant->graphTenantId(); $clientId = $tenant->app_client_id; $redirectUri = route('admin.consent.callback'); $scope = config('graph.scope') ?: 'https://graph.microsoft.com/.default'; $state = sprintf('tenantpilot|%s', $tenant->id); if (! $tenantId || ! $clientId || ! $redirectUri) { return null; } $query = http_build_query([ 'client_id' => $clientId, 'state' => $state, 'redirect_uri' => $redirectUri, 'scope' => $scope, ]); return sprintf('https://login.microsoftonline.com/%s/v2.0/adminconsent?%s', $tenantId, $query); } public static function entraUrl(Tenant $tenant): ?string { if ($tenant->app_client_id) { return sprintf( 'https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/%s', $tenant->app_client_id ); } if ($tenant->graphTenantId()) { return sprintf( 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview/tenantId/%s', $tenant->graphTenantId() ); } return null; } public static function verifyTenant( Tenant $tenant, TenantConfigService $configService, TenantPermissionService $permissionService, AuditLogger $auditLogger ): void { $configResult = $configService->testConnectivity($tenant); $permissionStatuses = []; foreach ($permissionService->getRequiredPermissions() as $permission) { $permissionStatuses[$permission['key']] = [ 'status' => $configResult['success'] ? 'ok' : 'error', 'details' => $configResult['success'] ? null : ['message' => $configResult['error_message']], ]; } $permissions = $permissionService->compare($tenant, $permissionStatuses); $appStatus = $configResult['success'] ? 'ok' : ($configResult['requires_consent'] ? 'consent_required' : 'error'); $tenant->update([ 'app_status' => $appStatus, 'app_notes' => $configResult['error_message'], ]); $user = auth()->user(); $auditLogger->log( tenant: $tenant, action: 'tenant.config.verified', context: [ 'metadata' => [ 'app_status' => $appStatus, 'error' => $configResult['error_message'], ], ], actorId: $user?->id, actorEmail: $user?->email, actorName: $user?->name, status: $appStatus === 'ok' ? 'success' : 'error', resourceType: 'tenant', resourceId: (string) $tenant->id, ); $auditLogger->log( tenant: $tenant, action: 'tenant.permissions.checked', context: [ 'metadata' => [ 'overall_status' => $permissions['overall_status'], ], ], actorId: $user?->id, actorEmail: $user?->email, actorName: $user?->name, status: match ($permissions['overall_status']) { 'ok' => 'success', 'error' => 'error', default => 'partial', }, resourceType: 'tenant', resourceId: (string) $tenant->id, ); $notification = Notification::make() ->title($configResult['success'] ? 'Configuration verified' : 'Verification failed') ->body($configResult['success'] ? 'Graph connectivity confirmed. Permission status: '.$permissions['overall_status'] : ($configResult['error_message'] ?? 'Graph connectivity failed')); if ($configResult['success']) { $notification->success(); } elseif ($configResult['requires_consent']) { $notification->warning(); } else { $notification->danger(); } $notification->send(); } }