selected server uuids (scopeType=servers) */ public array $selectedServers = []; // New-runbook form public string $runbookName = ''; public string $runbookCommand = ''; /** @var array last run's results */ public array $results = []; public function mount(): void { abort_unless(Auth::user()?->can('operate'), 403); } public function title(): string { return __('commands.title'); } private function gate(): void { abort_unless(Auth::user()?->can('operate'), 403); } /** Resolve the current target selection to active-credential servers (uuids → models). */ private function targetServers(): Collection { $q = Server::withActiveCredential(); if ($this->scopeType === 'group' && $this->scopeGroupUuid) { $group = ServerGroup::where('uuid', $this->scopeGroupUuid)->first(); return $group ? $q->inGroup($group->id)->get() : new Collection; } if ($this->scopeType === 'servers') { return $q->whereIn('uuid', $this->selectedServers)->get(); } return $q->get(); // 'all' } private function issueRun(string $command): void { $command = trim($command); if ($command === '') { $this->addError('command', __('commands.command_required')); return; } $servers = $this->targetServers(); if ($servers->isEmpty()) { $this->addError('command', __('commands.no_targets')); return; } $ids = $servers->pluck('id')->all(); $target = Str::limit($command, 60).' · '.__('commands.n_servers', ['count' => count($ids)]); $this->dispatch('openModal', component: 'modals.confirm-action', arguments: [ 'heading' => __('commands.confirm_heading'), 'body' => __('commands.confirm_body', ['count' => count($ids), 'command' => Str::limit($command, 120)]), 'confirmLabel' => __('commands.run'), 'danger' => true, 'icon' => 'command', 'notify' => '', // the handler reports the real outcome 'token' => ConfirmToken::issue('commandRun', ['command' => $command, 'serverIds' => $ids], 'command.run', $target, null), ], ); } /** Ad-hoc run from the textarea. */ public function run(): void { $this->gate(); $this->issueRun($this->command); } /** Run a saved runbook (its command + the current target selection). */ public function runRunbook(string $uuid): void { $this->gate(); $runbook = Runbook::where('uuid', $uuid)->firstOrFail(); $this->issueRun($runbook->command); } #[On('commandRun')] public function execute(string $confirmToken, CommandRunner $runner): void { $this->gate(); try { $payload = ConfirmToken::consume($confirmToken, 'commandRun'); } catch (InvalidConfirmToken) { return; // forged / replayed / direct-bypass — no-op (the modal already audited command.run) } $servers = Server::whereIn('id', $payload['params']['serverIds'])->get(); $this->results = $runner->run($payload['params']['command'], $servers); $ok = count(array_filter($this->results, fn ($r) => $r['ok'])); $this->dispatch('notify', message: __('commands.ran', ['ok' => $ok, 'total' => count($this->results)])); } public function createRunbook(): void { $this->gate(); $data = $this->validate([ 'runbookName' => ['required', 'string', 'max:80', Rule::unique('runbooks', 'name')], 'runbookCommand' => ['required', 'string', 'max:4000'], ]); $runbook = Runbook::create(['name' => $data['runbookName'], 'command' => $data['runbookCommand']]); $this->audit('runbook.create', $runbook->name); $this->reset('runbookName', 'runbookCommand'); $this->dispatch('notify', message: __('commands.runbook_created', ['name' => $runbook->name])); } public function confirmDeleteRunbook(string $uuid): void { $this->gate(); $runbook = Runbook::where('uuid', $uuid)->firstOrFail(); $this->dispatch('openModal', component: 'modals.confirm-action', arguments: [ 'heading' => __('commands.delete_heading'), 'body' => __('commands.delete_body', ['name' => $runbook->name]), 'confirmLabel' => __('common.delete'), 'danger' => true, 'icon' => 'trash', 'notify' => __('commands.runbook_deleted', ['name' => $runbook->name]), 'token' => ConfirmToken::issue('runbookDeleted', ['uuid' => $runbook->uuid], 'runbook.delete', $runbook->name, null), ], ); } #[On('runbookDeleted')] public function deleteRunbook(string $confirmToken): void { $this->gate(); try { $payload = ConfirmToken::consume($confirmToken, 'runbookDeleted'); } catch (InvalidConfirmToken) { return; } Runbook::where('uuid', $payload['params']['uuid'])->first()?->delete(); } private function audit(string $action, string $target): void { AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()?->name ?? 'system', 'action' => $action, 'target' => $target, 'ip' => request()->ip(), ]); } public function render(): View { return view('livewire.commands.index', [ 'runbooks' => Runbook::orderBy('name')->get(), 'groups' => ServerGroup::orderBy('name')->get(['uuid', 'name']), 'servers' => Server::orderBy('name')->get(['uuid', 'name']), ])->title($this->title()); } }