TenantAtlas/app/Filament/System/Pages/RepairWorkspaceOwners.php
ahmido 0cf612826f feat(114): system console control tower (merged) (#139)
Feature branch PR for Spec 114.

This branch contains the merged agent session work (see merge commit on branch).

Tests
- `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #139
2026-02-28 00:15:31 +00:00

279 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\System\Pages;
use App\Filament\System\Widgets\RepairWorkspaceOwnersStats;
use App\Models\AuditLog;
use App\Models\PlatformUser;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Services\Auth\BreakGlassSession;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\PlatformCapabilities;
use App\Support\Auth\WorkspaceRole;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Widgets\Widget;
use Filament\Widgets\WidgetConfiguration;
use Illuminate\Database\Eloquent\Builder;
class RepairWorkspaceOwners extends Page implements HasTable
{
use InteractsWithTable;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-wrench-screwdriver';
protected static ?string $navigationLabel = 'Repair workspace owners';
protected static string|\UnitEnum|null $navigationGroup = 'Recovery';
protected string $view = 'filament.system.pages.repair-workspace-owners';
public static function canAccess(): bool
{
$user = auth('platform')->user();
if (! $user instanceof PlatformUser) {
return false;
}
return $user->hasCapability(PlatformCapabilities::USE_BREAK_GLASS);
}
public static function getNavigationBadge(): ?string
{
$total = Workspace::query()->count();
$withOwners = WorkspaceMembership::query()
->where('role', WorkspaceRole::Owner->value)
->distinct('workspace_id')
->count('workspace_id');
$ownerless = $total - $withOwners;
return $ownerless > 0 ? (string) $ownerless : null;
}
public static function getNavigationBadgeColor(): string|array|null
{
return 'danger';
}
public function mount(): void
{
$this->mountInteractsWithTable();
}
/**
* @return array<class-string<Widget>|WidgetConfiguration>
*/
protected function getHeaderWidgets(): array
{
return [
RepairWorkspaceOwnersStats::class,
];
}
public function table(Table $table): Table
{
return $table
->heading('Workspaces')
->description('Current workspace ownership status.')
->defaultSort('name', 'asc')
->query(function (): Builder {
return Workspace::query()
->withCount([
'memberships as owner_count' => function (Builder $query): void {
$query->where('role', WorkspaceRole::Owner->value);
},
'memberships as member_count',
'tenants as tenant_count',
]);
})
->columns([
TextColumn::make('name')
->label('Workspace')
->searchable()
->sortable(),
TextColumn::make('owner_count')
->label('Owners')
->badge()
->color(fn (int $state): string => $state > 0 ? 'success' : 'danger')
->sortable(),
TextColumn::make('member_count')
->label('Members')
->sortable(),
TextColumn::make('tenant_count')
->label('Tenants')
->sortable(),
TextColumn::make('updated_at')
->label('Last activity')
->since()
->sortable(),
])
->emptyStateHeading('No workspaces')
->emptyStateDescription('No workspaces exist in the system yet.')
->bulkActions([]);
}
/**
* @return array<array{action: string, actor: string|null, workspace: string|null, recorded_at: string}>
*/
public function getRecentBreakGlassActions(): array
{
return AuditLog::query()
->where('action', 'like', '%break_glass%')
->orderByDesc('recorded_at')
->limit(10)
->get()
->map(fn (AuditLog $log): array => [
'action' => (string) $log->action,
'actor' => $log->actor_email ?: 'Unknown',
'workspace' => $log->metadata['metadata']['workspace_id'] ?? null
? Workspace::query()->whereKey((int) $log->metadata['metadata']['workspace_id'])->value('name')
: null,
'recorded_at' => $log->recorded_at?->diffForHumans() ?? 'Unknown',
])
->all();
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
$breakGlass = app(BreakGlassSession::class);
return [
Action::make('assign_owner')
->label('Emergency: Assign Owner')
->icon('heroicon-o-shield-exclamation')
->color('danger')
->requiresConfirmation()
->modalHeading('Assign workspace owner')
->modalDescription('This is a recovery action. It is audited and should only be used when the workspace owner set is broken.')
->form([
Select::make('workspace_id')
->label('Workspace')
->required()
->searchable()
->getSearchResultsUsing(function (string $search): array {
return Workspace::query()
->where('name', 'like', "%{$search}%")
->orderBy('name')
->limit(25)
->pluck('name', 'id')
->all();
})
->getOptionLabelUsing(function ($value): ?string {
if (! is_numeric($value)) {
return null;
}
return Workspace::query()->whereKey((int) $value)->value('name');
}),
Select::make('target_user_id')
->label('User')
->required()
->searchable()
->getSearchResultsUsing(function (string $search): array {
return User::query()
->where('email', 'like', "%{$search}%")
->orderBy('email')
->limit(25)
->pluck('email', 'id')
->all();
})
->getOptionLabelUsing(function ($value): ?string {
if (! is_numeric($value)) {
return null;
}
return User::query()->whereKey((int) $value)->value('email');
}),
Textarea::make('reason')
->label('Reason')
->required()
->minLength(5)
->maxLength(500)
->rows(4),
])
->action(function (array $data, BreakGlassSession $breakGlass, WorkspaceAuditLogger $auditLogger): void {
$platformUser = auth('platform')->user();
if (! $platformUser instanceof PlatformUser) {
abort(403);
}
if (! $platformUser->hasCapability(PlatformCapabilities::USE_BREAK_GLASS)) {
abort(403);
}
if (! $breakGlass->isActive()) {
abort(403);
}
$workspaceId = (int) ($data['workspace_id'] ?? 0);
$targetUserId = (int) ($data['target_user_id'] ?? 0);
$reason = (string) ($data['reason'] ?? '');
$workspace = Workspace::query()->whereKey($workspaceId)->firstOrFail();
$targetUser = User::query()->whereKey($targetUserId)->firstOrFail();
$membership = WorkspaceMembership::query()->firstOrNew([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $targetUser->getKey(),
]);
$fromRole = $membership->exists ? (string) $membership->role : null;
$membership->forceFill([
'role' => WorkspaceRole::Owner->value,
])->save();
$auditLogger->log(
workspace: $workspace,
action: AuditActionId::WorkspaceMembershipBreakGlassAssignOwner->value,
context: [
'metadata' => [
'workspace_id' => (int) $workspace->getKey(),
'actor_user_id' => (int) $platformUser->getKey(),
'target_user_id' => (int) $targetUser->getKey(),
'attempted_role' => WorkspaceRole::Owner->value,
'from_role' => $fromRole,
'reason' => trim($reason),
'source' => 'break_glass',
],
],
actor: null,
status: 'success',
resourceType: 'workspace',
resourceId: (string) $workspace->getKey(),
actorId: (int) $platformUser->getKey(),
actorEmail: $platformUser->email,
actorName: $platformUser->name,
);
Notification::make()
->title('Owner assigned')
->success()
->send();
})
->disabled(fn (): bool => ! $breakGlass->isActive())
->tooltip(fn (): ?string => ! $breakGlass->isActive() ? 'Activate break-glass mode on the Dashboard first.' : null),
];
}
}