Fix: SSL-Cert-Status robuster + Provisioning-State nach Reload erhalten

- loadSslStatus(): clearstatcache() + renewal-Conf als primärer Existenzcheck
  (renewal/ ist immer 755, zuverlässiger als is_dir auf live/ das 750 sein kann)
  + /usr/bin/openssl (voller Pfad für sudoers-Matching)
- restoreSslProvisioningState(): stellt letzten Provisioning-Zustand aus
  State-Dateien wieder her, so dass Status nach Page-Reload sichtbar bleibt
- Button zeigt nach Abschluss "Erneut einrichten" statt disabled zu bleiben

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main v1.1.275
boban 2026-04-26 08:44:08 +02:00
parent 17ed5b18d9
commit bd7bb50a52
2 changed files with 42 additions and 13 deletions

View File

@ -132,6 +132,7 @@ 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);
@ -284,6 +285,8 @@ class SettingsForm extends Component
public function loadSslStatus(): void
{
clearstatcache(true);
$domains = [
'ui' => $this->ui_domain,
'webmail' => $this->webmail_domain,
@ -296,23 +299,30 @@ class SettingsForm extends Component
$certs[$key] = ['domain' => '', 'has_cert' => false, 'expiry' => null, 'status' => 'nodomain'];
continue;
}
$certDir = "/etc/letsencrypt/live/{$domain}";
$certFile = "{$certDir}/fullchain.pem";
// live/ ist 755 (traversierbar), archive/ ist 700 — file_exists() schlägt fehl
// weil der Symlink auf archive/ zeigt. is_dir() auf live/domain/ funktioniert.
if (! is_dir($certDir)) {
$certFile = "/etc/letsencrypt/live/{$domain}/fullchain.pem";
$certDir = "/etc/letsencrypt/live/{$domain}";
// renewal/ ist immer 755 und world-readable — zuverlässigster Existenzcheck.
// is_dir() als Fallback falls renewal-Datei fehlt (manuell ausgestellte Certs).
$renewalConf = "/etc/letsencrypt/renewal/{$domain}.conf";
$certExists = file_exists($renewalConf) || is_dir($certDir);
if (! $certExists) {
$certs[$key] = ['domain' => $domain, 'has_cert' => false, 'expiry' => null, 'status' => 'missing'];
continue;
}
// sudo nötig weil archive/ (Symlink-Ziel) nur root lesbar ist
$expiry = trim((string) @shell_exec("sudo -n openssl x509 -enddate -noout -in " . escapeshellarg($certFile) . " 2>/dev/null | sed 's/notAfter=//'"));
// sudo nötig: archive/ ist 700 (Symlink-Ziel), voller Pfad für sudoers-Matching
$expiry = trim((string) @shell_exec(
"sudo -n /usr/bin/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',
$daysLeft === null => 'ok',
$daysLeft <= 0 => 'expired',
$daysLeft <= 14 => 'expiring',
default => 'ok',
};
$certs[$key] = [
'domain' => $domain,
@ -325,6 +335,21 @@ 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()); }

View File

@ -378,10 +378,14 @@
wire:loading.attr="disabled"
wire:target="startSslProvisioning"
@disabled($sslProvisioning)
class="mbx-btn-primary" style="width:100%;justify-content:center;font-size:12px">
class="{{ $sslDone ? 'mbx-btn-secondary' : 'mbx-btn-primary' }}"
style="width:100%;justify-content:center;font-size:12px">
<svg width="11" height="11" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="6" width="11" height="7.5" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
<span wire:loading.remove wire:target="startSslProvisioning">
{{ $sslProvisioning ? 'Läuft…' : 'Let\'s Encrypt Zertifikate einrichten' }}
@if($sslProvisioning) Läuft…
@elseif($sslDone) Erneut einrichten
@else Let's Encrypt Zertifikate einrichten
@endif
</span>
<span wire:loading wire:target="startSslProvisioning">Startet…</span>
</button>