170 lines
6.6 KiB
PHP
170 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\System\Pages;
|
|
|
|
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;
|
|
|
|
class RepairWorkspaceOwners extends Page
|
|
{
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
$breakGlass = app(BreakGlassSession::class);
|
|
|
|
return [
|
|
Action::make('assign_owner')
|
|
->label('Assign owner (break-glass)')
|
|
->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()),
|
|
];
|
|
}
|
|
}
|