186 lines
6.4 KiB
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');
|
|
}
|
|
}
|