131 lines
4.5 KiB
PHP
131 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Patch;
|
|
|
|
use App\Models\Server;
|
|
use App\Services\MaintenanceService;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use App\Support\Confirm\InvalidConfirmToken;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Component;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Fleet patch view: pending + security update counts per server, one-click patch. `operate`-gated
|
|
* (route + mount + per-method). Counts scan is lazy + per-server guarded. Applying upgrades is a
|
|
* stateful action → a signed ConfirmToken + R5 modal (the modal audits patch.apply from the sealed
|
|
* token; the handler does not re-audit). Targets resolve a client uuid → the sealed server id.
|
|
*/
|
|
#[Layout('layouts.app')]
|
|
class Index extends Component
|
|
{
|
|
/** @var array<string, array{pending:?int, security:?int}|array{error:string}> uuid → counts */
|
|
public array $counts = [];
|
|
|
|
/** @var array<string, array{ok:bool, output:string}> uuid → last patch result */
|
|
public array $results = [];
|
|
|
|
public bool $ready = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
abort_unless(Auth::user()?->can('operate'), 403);
|
|
}
|
|
|
|
public function title(): string
|
|
{
|
|
return __('patch.title');
|
|
}
|
|
|
|
private function gate(): void
|
|
{
|
|
abort_unless(Auth::user()?->can('operate'), 403);
|
|
}
|
|
|
|
public function scan(MaintenanceService $maint): void
|
|
{
|
|
$this->gate();
|
|
$this->counts = [];
|
|
|
|
foreach (Server::withActiveCredential()->orderBy('name')->get() as $server) {
|
|
try {
|
|
$this->counts[$server->uuid] = $maint->updateCounts($server);
|
|
} catch (Throwable $e) {
|
|
$this->counts[$server->uuid] = ['error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
$this->ready = true;
|
|
}
|
|
|
|
public function patch(string $uuid): void
|
|
{
|
|
$this->gate();
|
|
$server = Server::withActiveCredential()->where('uuid', $uuid)->firstOrFail();
|
|
$c = $this->counts[$uuid] ?? [];
|
|
$pending = $c['pending'] ?? '?';
|
|
|
|
$this->dispatch('openModal',
|
|
component: 'modals.confirm-action',
|
|
arguments: [
|
|
'heading' => __('patch.confirm_heading'),
|
|
'body' => __('patch.confirm_body', ['server' => $server->name, 'count' => $pending]),
|
|
'confirmLabel' => __('patch.apply'),
|
|
'danger' => true,
|
|
'icon' => 'rotate',
|
|
'notify' => '', // the handler reports the outcome
|
|
'token' => ConfirmToken::issue('patchApply', ['uuid' => $server->uuid], 'patch.apply', $server->name, $server->id),
|
|
],
|
|
);
|
|
}
|
|
|
|
#[On('patchApply')]
|
|
public function applyPatch(string $confirmToken, MaintenanceService $maint): void
|
|
{
|
|
$this->gate();
|
|
|
|
try {
|
|
$payload = ConfirmToken::consume($confirmToken, 'patchApply');
|
|
} catch (InvalidConfirmToken) {
|
|
return; // forged / replayed — the modal already audited patch.apply
|
|
}
|
|
|
|
$server = Server::where('uuid', $payload['params']['uuid'])->first();
|
|
if (! $server) {
|
|
return;
|
|
}
|
|
|
|
$res = $maint->applyUpgrades($server);
|
|
$res['output'] = mb_scrub($res['output'], 'UTF-8'); // remote output into a Livewire prop → scrub invalid bytes
|
|
$this->results[$server->uuid] = $res;
|
|
|
|
// Refresh this server's counts so the badge reflects the post-patch state.
|
|
try {
|
|
$this->counts[$server->uuid] = $maint->updateCounts($server);
|
|
} catch (Throwable) {
|
|
// keep the old counts if the re-scan fails
|
|
}
|
|
|
|
$this->dispatch('notify', message: $res['ok']
|
|
? __('patch.patched', ['server' => $server->name])
|
|
: __('patch.patch_failed', ['error' => $res['output']]));
|
|
}
|
|
|
|
public function render(): View
|
|
{
|
|
// Fleet roll-up (only over successfully-scanned servers).
|
|
$scored = array_filter($this->counts, fn ($c) => isset($c['pending']) && $c['pending'] !== null);
|
|
$totalPending = array_sum(array_map(fn ($c) => $c['pending'], $scored));
|
|
$totalSecurity = array_sum(array_map(fn ($c) => $c['security'] ?? 0, $scored));
|
|
|
|
return view('livewire.patch.index', [
|
|
'servers' => Server::withActiveCredential()->orderBy('name')->get(['uuid', 'name']),
|
|
'totalPending' => (int) $totalPending,
|
|
'totalSecurity' => (int) $totalSecurity,
|
|
])->title($this->title());
|
|
}
|
|
}
|