mailwolt/app/Livewire/Ui/Security/SslCertificatesTable.php

186 lines
6.4 KiB
PHP

<?php
namespace App\Livewire\Ui\Security;
use App\Models\Setting;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.dvx')]
#[Title('SSL/TLS · Mailwolt')]
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
{
$this->certs = $this->loadCertificates();
$this->dispatch('toast', type: 'done', badge: 'SSL', title: 'Aktualisiert',
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 clubird: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);
if ($safe === '') return;
$out = (string) @shell_exec(
"sudo -n /usr/bin/certbot renew --cert-name {$safe} --force-renewal 2>&1"
);
$this->certs = $this->loadCertificates();
if (str_contains($out, 'Successfully renewed') || str_contains($out, 'success')) {
$this->dispatch('toast', type: 'done', badge: 'SSL',
title: 'Zertifikat erneuert', text: "Zertifikat <b>{$safe}</b> wurde erfolgreich erneuert.", duration: 5000);
} else {
$this->dispatch('toast', type: 'error', badge: 'SSL',
title: 'Fehler', text: nl2br(htmlspecialchars(substr($out, 0, 300))), duration: 0);
}
}
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');
if (empty(trim($out))) return [['_error' => 'unavailable']];
if (str_contains($out, 'No certificates found')) return [];
$certs = [];
$blocks = preg_split('/\n(?=\s*Certificate Name:)/m', $out);
foreach ($blocks as $block) {
if (!preg_match('/Certificate Name:\s*(.+)/i', $block, $nameM)) continue;
preg_match('/Domains:\s*(.+)/i', $block, $domainsM);
preg_match('/Expiry Date:\s*(.+)/i', $block, $expiryM);
preg_match('/Certificate Path:\s*(.+)/i', $block, $certM);
$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];
} elseif (preg_match('/INVALID/i', $expiryRaw)) {
$expired = true;
$daysLeft = 0;
}
$domainsRaw = trim($domainsM[1] ?? '');
$domains = $domainsRaw !== '' ? array_values(array_filter(explode(' ', $domainsRaw))) : [];
$certs[] = [
'name' => trim($nameM[1]),
'domains' => $domains,
'expiry_date' => $expiryDate,
'days_left' => $daysLeft,
'expired' => $expired,
'cert_path' => trim($certM[1] ?? ''),
];
}
usort($certs, fn($a, $b) => ($a['days_left'] ?? 999) <=> ($b['days_left'] ?? 999));
return $certs;
}
public function render()
{
return view('livewire.ui.security.ssl-certificates-table');
}
}