> endpoint uuid → check result (+ 'status') */ public array $checks = []; public bool $ready = false; public function mount(): void { abort_unless(Auth::user()?->can('operate'), 403); } public function title(): string { return __('certs.title'); } private function gate(): void { abort_unless(Auth::user()?->can('operate'), 403); } 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 scan(CertService $certs): void { $this->gate(); $this->checks = []; foreach (CertEndpoint::orderBy('host')->get() as $ep) { try { $res = $certs->check($ep->host, $ep->port); $res['status'] = $res['ok'] ? $certs->status($res['daysLeft'] ?? null) : 'error'; $this->checks[$ep->uuid] = $res; } catch (Throwable $e) { $this->checks[$ep->uuid] = ['ok' => false, 'status' => 'error', 'error' => $e->getMessage()]; } } $this->ready = true; } public function addEndpoint(CertService $certs): void { $this->gate(); $data = $this->validate([ 'label' => ['nullable', 'string', 'max:60'], // A hostname or an IP — never a scheme/path/shell char (it's used in ssl://host:port). 'host' => ['required', 'string', 'max:253', 'regex:/^[A-Za-z0-9]([A-Za-z0-9.\-]*[A-Za-z0-9])?$/'], 'port' => ['required', 'integer', 'min:1', 'max:65535'], ]); $ep = CertEndpoint::create($data); $this->audit('cert.endpoint_add', $data['host'].':'.$data['port']); // Probe the new endpoint right away so its validity shows immediately (not only after a // manual refresh / the next scheduled scan). try { $res = $certs->check($ep->host, $ep->port); $res['status'] = $res['ok'] ? $certs->status($res['daysLeft'] ?? null) : 'error'; $this->checks[$ep->uuid] = $res; } catch (Throwable $e) { $this->checks[$ep->uuid] = ['ok' => false, 'status' => 'error', 'error' => $e->getMessage()]; } $this->reset('label', 'host'); $this->port = 443; $this->dispatch('notify', message: __('certs.added', ['host' => $data['host']])); } public function confirmDeleteEndpoint(string $uuid): void { $this->gate(); $ep = CertEndpoint::where('uuid', $uuid)->firstOrFail(); $this->dispatch('openModal', component: 'modals.confirm-action', arguments: [ 'heading' => __('certs.delete_heading'), 'body' => __('certs.delete_body', ['host' => $ep->host]), 'confirmLabel' => __('common.delete'), 'danger' => true, 'icon' => 'trash', 'notify' => __('certs.deleted', ['host' => $ep->host]), 'token' => ConfirmToken::issue('certEndpointDeleted', ['uuid' => $ep->uuid], 'cert.endpoint_delete', $ep->host.':'.$ep->port, null), ], ); } #[On('certEndpointDeleted')] public function deleteEndpoint(string $confirmToken): void { $this->gate(); try { $payload = ConfirmToken::consume($confirmToken, 'certEndpointDeleted'); } catch (InvalidConfirmToken) { return; } CertEndpoint::where('uuid', $payload['params']['uuid'])->first()?->delete(); } public function render(): View { return view('livewire.certs.index', [ 'endpoints' => CertEndpoint::orderBy('host')->get(), ])->title($this->title()); } }