diff --git a/app/Livewire/Ui/System/SettingsForm.php b/app/Livewire/Ui/System/SettingsForm.php index 963cac7..8107ec2 100644 --- a/app/Livewire/Ui/System/SettingsForm.php +++ b/app/Livewire/Ui/System/SettingsForm.php @@ -24,6 +24,14 @@ 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 = []; + + private const SSL_STATE_DIR = '/var/lib/mailwolt/wizard'; + // Sicherheit public int $rate_limit = 5; public int $password_min = 10; @@ -123,6 +131,7 @@ class SettingsForm extends Component Setting::setMany($persist); } + $this->loadSslStatus(); $this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit); $this->password_min = (int) Setting::get('password_min', $this->password_min); @@ -188,12 +197,13 @@ class SettingsForm extends Component // Immer .env aktualisieren — unabhängig von DNS oder Nginx $this->syncDomainsToEnv(); - // Nginx-Konfiguration nur anwenden wenn alle Domains gesetzt sind + // Nginx-Konfiguration (ohne certbot) — nur wenn alle Domains gesetzt sind if ($this->ui_domain && $this->mail_domain && $this->webmail_domain) { - $sslAuto = app()->isProduction(); - $this->applyDomains($sslAuto); + $this->applyDomains(false); } + $this->loadSslStatus(); + $this->dispatch('toast', type: 'done', badge: 'Domains', title: 'Domains gespeichert', text: 'Konfiguration wurde übernommen.', duration: 4000); @@ -223,6 +233,94 @@ class SettingsForm extends Component $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 + { + $domains = [ + 'ui' => $this->ui_domain, + 'webmail' => $this->webmail_domain, + 'mail' => $this->mail_domain, + ]; + + $certs = []; + foreach ($domains as $key => $domain) { + if (! $domain) { + $certs[$key] = ['domain' => '', 'has_cert' => false, 'expiry' => null, 'status' => 'nodomain']; + continue; + } + $certFile = "/etc/letsencrypt/live/{$domain}/fullchain.pem"; + if (! file_exists($certFile)) { + $certs[$key] = ['domain' => $domain, 'has_cert' => false, 'expiry' => null, 'status' => 'missing']; + continue; + } + $expiry = trim((string) @shell_exec("openssl x509 -enddate -noout -in " . escapeshellarg($certFile) . " 2>/dev/null | sed 's/notAfter=//'")); + $expiryTs = $expiry ? strtotime($expiry) : null; + $daysLeft = $expiryTs ? (int) round(($expiryTs - time()) / 86400) : null; + $certStatus = match (true) { + $daysLeft === null => 'ok', + $daysLeft <= 0 => 'expired', + $daysLeft <= 14 => 'expiring', + default => 'ok', + }; + $certs[$key] = [ + 'domain' => $domain, + 'has_cert' => true, + 'expiry' => $expiryTs ? date('d.m.Y', $expiryTs) : null, + 'days' => $daysLeft, + 'status' => $certStatus, + ]; + } + $this->sslCerts = $certs; + } + 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/css/app.css b/resources/css/app.css index a99b10f..d74bd70 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -2462,3 +2462,5 @@ textarea.sec-input { height: auto; line-height: 1.55; padding: 8px 10px; resize: .mq-pag-btn:hover:not(:disabled) { background: var(--mw-bg3); color: var(--mw-t1); } .mq-pag-btn.active { background: rgba(124,58,237,.2); border-color: rgba(124,58,237,.5); color: #c4b5fd; } .mq-pag-btn:disabled { opacity: .35; cursor: default; } + +@keyframes spin { to { transform: rotate(360deg); } } diff --git a/resources/views/livewire/ui/system/settings-form.blade.php b/resources/views/livewire/ui/system/settings-form.blade.php index 382857c..248d00c 100644 --- a/resources/views/livewire/ui/system/settings-form.blade.php +++ b/resources/views/livewire/ui/system/settings-form.blade.php @@ -285,22 +285,114 @@ Speichert… + + + + {{-- SSL-Zertifikate --}} +