authorize('site.manage'); $this->acmeEmail = (string) Settings::get('proxy.acme_email', ''); } protected function rules(): array { return [ // Ein Hostname und nichts anderes. Der root-eigene Helfer prüft es // ein zweites Mal — das hier ist die Höflichkeit, jenes die Sperre. 'hostname' => [ 'required', 'string', 'max:253', 'regex:/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/', Rule::unique('proxy_hosts', 'hostname'), ], 'purpose' => ['required', Rule::in(ProxyHost::PURPOSES)], 'note' => ['nullable', 'string', 'max:255'], ]; } public function add(): void { $this->authorize('site.manage'); $data = $this->validate(); ProxyHost::create($data); $this->reset('hostname', 'note'); $this->purpose = 'public'; $this->notice = __('proxy.added'); } public function remove(int $id): void { $this->authorize('site.manage'); ProxyHost::query()->whereKey($id)->delete(); // Kein Anwenden von selbst. Einen Namen aus der Liste zu nehmen und ihn // im selben Atemzug aus dem laufenden Proxy zu entfernen wäre zwei // Entscheidungen in einem Klick — und die zweite nimmt eine Seite vom // Netz. $this->notice = __('proxy.removed'); } public function saveAcmeEmail(): void { $this->authorize('site.manage'); $this->validateOnly('acmeEmail', ['acmeEmail' => ['nullable', 'email', 'max:255']]); Settings::set('proxy.acme_email', trim($this->acmeEmail)); $this->notice = __('proxy.email_saved'); } /** * Was ein Besucher gerade wirklich bekommt. * * Nacheinander und mit kurzem Zeitablauf: die Liste ist zweistellig, und * ein Hostname, der nicht auflöst, darf die Seite nicht minutenlang * blockieren. */ public function checkCertificates(CertificateInspector $inspector): void { $this->authorize('site.manage'); foreach (ProxyHost::query()->get() as $host) { $result = $inspector->inspect($host->hostname); $host->forceFill([ 'certificate_expires_at' => $result['expires_at'], 'certificate_checked_at' => now(), 'certificate_error' => $result['error'], ])->save(); } $this->notice = __('proxy.checked'); } /** * Den Proxy die Liste übernehmen lassen — und dabei fehlende Zertifikate * anfordern. * * Ein eigener Knopf „erneuern" wäre daneben: Caddy erneuert von selbst bei * zwei Dritteln der Laufzeit. Was ein Betreiber wirklich braucht, ist ein * zweiter Versuch, nachdem eine Ausstellung gescheitert ist — und genau das * ist ein Neuladen. */ public function apply(UpdateChannel $channel): void { $this->authorize('site.manage'); $this->notice = $channel->requestProxyHosts((string) auth('operator')->id()) ? __('proxy.applying') : __('proxy.apply_failed'); } public function render() { return view('livewire.admin.proxy-hosts', [ 'hosts' => ProxyHost::query()->orderBy('purpose')->orderBy('hostname')->get(), 'purposes' => ProxyHost::PURPOSES, // Leer heißt: die Adresse des Inhabers. Eine Kontaktadresse, an die // die Ablaufwarnungen gehen, darf nicht leer bleiben, und die eine // Adresse, von der diese Installation sicher weiß, dass sie gelesen // wird, ist seine. 'fallbackEmail' => $this->ownerEmail(), ]); } private function ownerEmail(): ?string { return Operator::query() ->whereHas('roles', fn ($q) => $q->where('name', 'Owner')) ->orderBy('id') ->value('email'); } }