From 7f56926b70c806c21cc7c3e8061fd81a153735f6 Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 26 Apr 2026 10:01:58 +0200 Subject: [PATCH] Refactor: SSL-Verwaltung nach Security/SSL verschoben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Zertifikate einrichten/erneuern nur noch unter Sicherheit → SSL/TLS - SSL-Seite: Provisioning mit Fortschritt, Ablaufdatum + Tage in Tabelle - Einstellungen: nur noch read-only Status + Link zu SSL-Seite Co-Authored-By: Claude Sonnet 4.6 --- .../Ui/Security/SslCertificatesTable.php | 101 +++++++++++++- app/Livewire/Ui/System/SettingsForm.php | 89 +----------- .../security/ssl-certificates-table.blade.php | 132 +++++++++++++++++- .../ui/system/settings-form.blade.php | 84 ++--------- 4 files changed, 233 insertions(+), 173 deletions(-) diff --git a/app/Livewire/Ui/Security/SslCertificatesTable.php b/app/Livewire/Ui/Security/SslCertificatesTable.php index d0a1972..6a0f3be 100644 --- a/app/Livewire/Ui/Security/SslCertificatesTable.php +++ b/app/Livewire/Ui/Security/SslCertificatesTable.php @@ -2,6 +2,7 @@ namespace App\Livewire\Ui\Security; +use App\Models\Setting; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; use Livewire\Component; @@ -12,9 +13,26 @@ class SslCertificatesTable extends Component { public array $certs = []; + // Domains (aus Einstellungen) + public string $uiDomain = ''; + public string $webmailDomain = ''; + public string $mailDomain = ''; + + // Provisioning + public bool $sslProvisioning = false; + public bool $sslDone = false; + public array $sslProgress = ['ui' => 'pending', 'webmail' => 'pending', 'mail' => 'pending']; + + private const SSL_STATE_DIR = '/var/lib/mailwolt/wizard'; + public function mount(): void { + $this->uiDomain = (string) Setting::get('ui_domain', ''); + $this->webmailDomain = (string) Setting::get('webmail_domain', ''); + $this->mailDomain = (string) Setting::get('mail_domain', ''); + $this->certs = $this->loadCertificates(); + $this->restoreSslProvisioningState(); } public function refresh(): void @@ -24,6 +42,55 @@ class SslCertificatesTable extends Component text: 'Zertifikatsliste wurde neu geladen.', duration: 3000); } + public function startSslProvisioning(): void + { + if (! ($this->uiDomain && $this->webmailDomain && $this->mailDomain)) { + $this->dispatch('toast', type: 'warn', badge: 'SSL', + title: 'Domains fehlen', + text: 'Bitte erst alle Domains unter Einstellungen speichern.', duration: 6000); + return; + } + + @mkdir(self::SSL_STATE_DIR, 0755, true); + @unlink(self::SSL_STATE_DIR . '/done'); + foreach (['ui', 'mail', 'webmail'] as $k) { + file_put_contents(self::SSL_STATE_DIR . "/{$k}", 'pending'); + } + + $this->sslProgress = ['ui' => 'pending', 'webmail' => 'pending', 'mail' => 'pending']; + $this->sslDone = false; + $this->sslProvisioning = true; + + $artisan = base_path('artisan'); + $cmd = sprintf( + 'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=1 > /dev/null 2>&1 &', + escapeshellarg($artisan), + escapeshellarg($this->uiDomain), + escapeshellarg($this->mailDomain), + escapeshellarg($this->webmailDomain), + ); + @shell_exec($cmd); + } + + public function pollSsl(): void + { + if (! $this->sslProvisioning || $this->sslDone) return; + + foreach (['ui', 'mail', 'webmail'] as $key) { + $file = self::SSL_STATE_DIR . "/{$key}"; + $this->sslProgress[$key] = is_readable($file) + ? trim((string) @file_get_contents($file)) + : 'pending'; + } + + $done = @file_get_contents(self::SSL_STATE_DIR . '/done'); + if ($done !== false) { + $this->sslDone = true; + $this->sslProvisioning = false; + $this->certs = $this->loadCertificates(); + } + } + public function renew(string $name): void { $safe = preg_replace('/[^a-z0-9._-]/i', '', $name); @@ -44,6 +111,21 @@ class SslCertificatesTable extends Component } } + private function restoreSslProvisioningState(): void + { + $doneFile = self::SSL_STATE_DIR . '/done'; + if (! file_exists($doneFile)) return; + + $this->sslDone = true; + $this->sslProvisioning = false; + foreach (['ui', 'mail', 'webmail'] as $key) { + $file = self::SSL_STATE_DIR . "/{$key}"; + if (is_readable($file)) { + $this->sslProgress[$key] = trim((string) @file_get_contents($file)); + } + } + } + private function loadCertificates(): array { $out = (string) @shell_exec('sudo -n /usr/bin/certbot certificates 2>&1'); @@ -63,6 +145,13 @@ class SslCertificatesTable extends Component $expiryRaw = trim($expiryM[1] ?? ''); $daysLeft = null; $expired = false; + $expiryDate = null; + + // Datum extrahieren (Format: "2026-07-24 10:30:00+00:00 (VALID: 88 days)") + if (preg_match('/(\d{4}-\d{2}-\d{2})/', $expiryRaw, $dateM)) { + $ts = strtotime($dateM[1]); + $expiryDate = $ts ? date('d.m.Y', $ts) : null; + } if (preg_match('/VALID: (\d+) days/i', $expiryRaw, $dM)) { $daysLeft = (int) $dM[1]; @@ -75,12 +164,12 @@ class SslCertificatesTable extends Component $domains = $domainsRaw !== '' ? array_values(array_filter(explode(' ', $domainsRaw))) : []; $certs[] = [ - 'name' => trim($nameM[1]), - 'domains' => $domains, - 'expiry' => $expiryRaw, - 'days_left' => $daysLeft, - 'expired' => $expired, - 'cert_path' => trim($certM[1] ?? ''), + 'name' => trim($nameM[1]), + 'domains' => $domains, + 'expiry_date' => $expiryDate, + 'days_left' => $daysLeft, + 'expired' => $expired, + 'cert_path' => trim($certM[1] ?? ''), ]; } diff --git a/app/Livewire/Ui/System/SettingsForm.php b/app/Livewire/Ui/System/SettingsForm.php index 8977311..24ea737 100644 --- a/app/Livewire/Ui/System/SettingsForm.php +++ b/app/Livewire/Ui/System/SettingsForm.php @@ -24,11 +24,8 @@ class SettingsForm extends Component public string $mail_domain = ''; public string $webmail_domain = ''; - // SSL provisioning - public bool $sslProvisioning = false; - public bool $sslDone = false; - public array $sslProgress = ['ui' => 'pending', 'webmail' => 'pending', 'mail' => 'pending']; - public array $sslCerts = []; + // SSL-Status (read-only, für Anzeige in Einstellungen) + public array $sslCerts = []; private const SSL_STATE_DIR = '/var/lib/mailwolt/wizard'; @@ -132,7 +129,6 @@ class SettingsForm extends Component } $this->loadSslStatus(); - $this->restoreSslProvisioningState(); $this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit); $this->password_min = (int) Setting::get('password_min', $this->password_min); @@ -217,72 +213,6 @@ class SettingsForm extends Component return rtrim($value, '/'); } - public function openSslModal(): void - { - if (! ($this->ui_domain && $this->mail_domain && $this->webmail_domain)) { - $this->dispatch('toast', type: 'warn', badge: 'SSL', - title: 'Domains fehlen', - text: 'Bitte erst alle drei Domains speichern.', duration: 5000); - return; - } - $this->dispatch('openModal', component: 'ui.system.modal.ssl-provision-modal'); - } - - #[\Livewire\Attributes\On('ssl:provision')] - public function provisionSsl(): void - { - $this->applyDomains(true); - } - - public function startSslProvisioning(): void - { - if (! ($this->ui_domain && $this->webmail_domain)) { - $this->dispatch('toast', type: 'warn', badge: 'SSL', - title: 'Domains fehlen', - text: 'Bitte erst UI-Domain und Webmail-Domain speichern.', duration: 5000); - return; - } - - @mkdir(self::SSL_STATE_DIR, 0755, true); - @unlink(self::SSL_STATE_DIR . '/done'); - foreach (['ui', 'mail', 'webmail'] as $k) { - file_put_contents(self::SSL_STATE_DIR . "/{$k}", 'pending'); - } - - $this->sslProgress = ['ui' => 'pending', 'webmail' => 'pending', 'mail' => 'pending']; - $this->sslDone = false; - $this->sslProvisioning = true; - - $artisan = base_path('artisan'); - $cmd = sprintf( - 'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=1 > /dev/null 2>&1 &', - escapeshellarg($artisan), - escapeshellarg($this->ui_domain), - escapeshellarg($this->mail_domain), - escapeshellarg($this->webmail_domain), - ); - @shell_exec($cmd); - } - - public function pollSsl(): void - { - if (! $this->sslProvisioning || $this->sslDone) return; - - foreach (['ui', 'mail', 'webmail'] as $key) { - $file = self::SSL_STATE_DIR . "/{$key}"; - $this->sslProgress[$key] = is_readable($file) - ? trim((string) @file_get_contents($file)) - : 'pending'; - } - - $done = @file_get_contents(self::SSL_STATE_DIR . '/done'); - if ($done !== false) { - $this->sslDone = true; - $this->sslProvisioning = false; - $this->loadSslStatus(); - } - } - public function loadSslStatus(): void { clearstatcache(true); @@ -335,21 +265,6 @@ class SettingsForm extends Component $this->sslCerts = $certs; } - private function restoreSslProvisioningState(): void - { - $doneFile = self::SSL_STATE_DIR . '/done'; - if (! file_exists($doneFile)) return; - - $this->sslDone = true; - $this->sslProvisioning = false; - foreach (['ui', 'mail', 'webmail'] as $key) { - $file = self::SSL_STATE_DIR . "/{$key}"; - if (is_readable($file)) { - $this->sslProgress[$key] = trim((string) @file_get_contents($file)); - } - } - } - public function updatedUiDomain(): void { $this->validateOnly('ui_domain', $this->domainRules(), $this->domainMessages()); } public function updatedMailDomain(): void { $this->validateOnly('mail_domain', $this->domainRules(), $this->domainMessages()); } public function updatedWebmailDomain(): void { $this->validateOnly('webmail_domain', $this->domainRules(), $this->domainMessages()); } diff --git a/resources/views/livewire/ui/security/ssl-certificates-table.blade.php b/resources/views/livewire/ui/security/ssl-certificates-table.blade.php index 74961e7..12ec0f9 100644 --- a/resources/views/livewire/ui/security/ssl-certificates-table.blade.php +++ b/resources/views/livewire/ui/security/ssl-certificates-table.blade.php @@ -2,6 +2,9 @@ SSL/TLS
+ @if($sslProvisioning) +
+ @endif
@@ -25,10 +28,120 @@
+ + {{-- ═══ Zertifikate einrichten / erneuern ═══ --}}
- Let's Encrypt Zertifikate + Let's Encrypt einrichten +
+
+
+ + {{-- Domain-Übersicht --}} + @php + $domainMap = [ + 'ui' => ['label' => 'UI', 'domain' => $uiDomain], + 'webmail' => ['label' => 'Webmail', 'domain' => $webmailDomain], + 'mail' => ['label' => 'Mailserver', 'domain' => $mailDomain], + ]; + @endphp +
+ @foreach($domainMap as $key => $info) + @php + $d = $info['domain']; + $hasRenewal = $d && file_exists("/etc/letsencrypt/renewal/{$d}.conf"); + $hasLive = $d && is_dir("/etc/letsencrypt/live/{$d}"); + $certOk = $hasRenewal || $hasLive; + @endphp +
+ @if(!$d) + + @elseif($certOk) + + @else + + @endif +
+ {{ $info['label'] }} + @if($d) + {{ $d }} + @endif +
+ @if(!$d) + Keine Domain + @elseif($certOk) + Vorhanden + @else + Fehlt + @endif +
+ @endforeach +
+ + {{-- Fortschritt während Provisioning --}} + @if($sslProvisioning || $sslDone) +
+
+ {{ $sslProvisioning ? 'Zertifikate werden eingerichtet…' : 'Einrichtung abgeschlossen' }} +
+ @foreach(['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver'] as $key => $label) + @php $st = $sslProgress[$key] ?? 'pending'; @endphp +
+ @if($st === 'done') + + @elseif($st === 'error' || $st === 'nodns') + + @elseif($st === 'running') + + @else + + @endif + + {{ $label }} + @if($st === 'nodns') — kein DNS-Eintrag + @elseif($st === 'error') — fehlgeschlagen + @elseif($st === 'running') — läuft… + @endif + +
+ @endforeach +
+ @endif + + {{-- Button --}} + @if(!$uiDomain || !$webmailDomain || !$mailDomain) +
+ Bitte erst alle Domains unter + Einstellungen + speichern. +
+ @else + + @endif + +
+
+ + {{-- ═══ Ausgestellte Zertifikate ═══ --}} +
+
+
+ Ausgestellte Zertifikate
@@ -49,7 +162,7 @@
Noch keine Zertifikate ausgestellt
-
certbot ist installiert — führe certbot certonly aus um ein Zertifikat zu erstellen
+
Oben auf "Let's Encrypt Zertifikate einrichten" klicken
@else
@@ -58,7 +171,7 @@ Name Domains - Ablauf + Gültig bis Status Aktionen @@ -79,9 +192,14 @@ @endforeach
- - @if($cert['days_left'] !== null) - {{ $cert['days_left'] }} Tage + + @if($cert['expiry_date']) + {{ $cert['expiry_date'] }} + @if($cert['days_left'] !== null) +
noch {{ $cert['days_left'] }} Tage
+ @endif + @elseif($cert['days_left'] !== null) + {{ $cert['days_left'] }} Tage @else @endif @@ -111,6 +229,6 @@
@endif
-
+ diff --git a/resources/views/livewire/ui/system/settings-form.blade.php b/resources/views/livewire/ui/system/settings-form.blade.php index 45380a0..66d5671 100644 --- a/resources/views/livewire/ui/system/settings-form.blade.php +++ b/resources/views/livewire/ui/system/settings-form.blade.php @@ -294,18 +294,17 @@
SSL-Zertifikate
+
+ + + +
-
- - {{-- Cert-Status-Tabelle --}} - @php - $certLabels = ['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver']; - @endphp -
+
+ @php $certLabels = ['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver']; @endphp @foreach(['ui','webmail','mail'] as $key) @php $c = $sslCerts[$key] ?? null; @endphp -
- {{-- Icon --}} +
@if(!$c || $c['status'] === 'nodomain') @elseif($c['status'] === 'ok') @@ -315,12 +314,10 @@ @else @endif - {{-- Label + Domain --}}
{{ $certLabels[$key] }} {{ $c['domain'] ?? '—' }}
- {{-- Status --}} @if(!$c || $c['status'] === 'nodomain') Keine Domain @elseif($c['status'] === 'missing') @@ -334,69 +331,10 @@ @endif
@endforeach -
- - {{-- Fortschritt während Provisioning --}} - @if($sslProvisioning || $sslDone) - @if($sslProvisioning) -
- @endif -
-
- {{ $sslProvisioning ? 'Zertifikate werden eingerichtet…' : 'Einrichtung abgeschlossen' }} -
- @foreach(['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver'] as $key => $label) - @php $st = $sslProgress[$key] ?? 'pending'; @endphp -
- @if($st === 'done') - - @elseif($st === 'error' || $st === 'nodns') - - @elseif($st === 'skip') - - @elseif($st === 'running') - - @else - - @endif - - {{ $label }} - @if($st === 'nodns') — kein DNS-Eintrag - @elseif($st === 'error') — fehlgeschlagen - @elseif($st === 'skip') — übersprungen - @elseif($st === 'running') — läuft… - @endif - -
- @endforeach -
- @endif - - {{-- Buttons --}} - @if(app()->isProduction()) - - @else - - @endif - + Zertifikate einrichten / erneuern → +