Refactor: SSL-Verwaltung nach Security/SSL verschoben
- 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 <noreply@anthropic.com>main v1.1.283
parent
904d60ed2b
commit
7f56926b70
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire\Ui\Security;
|
namespace App\Livewire\Ui\Security;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -12,9 +13,26 @@ class SslCertificatesTable extends Component
|
||||||
{
|
{
|
||||||
public array $certs = [];
|
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
|
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->certs = $this->loadCertificates();
|
||||||
|
$this->restoreSslProvisioningState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
|
|
@ -24,6 +42,55 @@ class SslCertificatesTable extends Component
|
||||||
text: 'Zertifikatsliste wurde neu geladen.', duration: 3000);
|
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
|
public function renew(string $name): void
|
||||||
{
|
{
|
||||||
$safe = preg_replace('/[^a-z0-9._-]/i', '', $name);
|
$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
|
private function loadCertificates(): array
|
||||||
{
|
{
|
||||||
$out = (string) @shell_exec('sudo -n /usr/bin/certbot certificates 2>&1');
|
$out = (string) @shell_exec('sudo -n /usr/bin/certbot certificates 2>&1');
|
||||||
|
|
@ -63,6 +145,13 @@ class SslCertificatesTable extends Component
|
||||||
$expiryRaw = trim($expiryM[1] ?? '');
|
$expiryRaw = trim($expiryM[1] ?? '');
|
||||||
$daysLeft = null;
|
$daysLeft = null;
|
||||||
$expired = false;
|
$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)) {
|
if (preg_match('/VALID: (\d+) days/i', $expiryRaw, $dM)) {
|
||||||
$daysLeft = (int) $dM[1];
|
$daysLeft = (int) $dM[1];
|
||||||
|
|
@ -75,12 +164,12 @@ class SslCertificatesTable extends Component
|
||||||
$domains = $domainsRaw !== '' ? array_values(array_filter(explode(' ', $domainsRaw))) : [];
|
$domains = $domainsRaw !== '' ? array_values(array_filter(explode(' ', $domainsRaw))) : [];
|
||||||
|
|
||||||
$certs[] = [
|
$certs[] = [
|
||||||
'name' => trim($nameM[1]),
|
'name' => trim($nameM[1]),
|
||||||
'domains' => $domains,
|
'domains' => $domains,
|
||||||
'expiry' => $expiryRaw,
|
'expiry_date' => $expiryDate,
|
||||||
'days_left' => $daysLeft,
|
'days_left' => $daysLeft,
|
||||||
'expired' => $expired,
|
'expired' => $expired,
|
||||||
'cert_path' => trim($certM[1] ?? ''),
|
'cert_path' => trim($certM[1] ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,8 @@ class SettingsForm extends Component
|
||||||
public string $mail_domain = '';
|
public string $mail_domain = '';
|
||||||
public string $webmail_domain = '';
|
public string $webmail_domain = '';
|
||||||
|
|
||||||
// SSL provisioning
|
// SSL-Status (read-only, für Anzeige in Einstellungen)
|
||||||
public bool $sslProvisioning = false;
|
public array $sslCerts = [];
|
||||||
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';
|
private const SSL_STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||||
|
|
||||||
|
|
@ -132,7 +129,6 @@ class SettingsForm extends Component
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->loadSslStatus();
|
$this->loadSslStatus();
|
||||||
$this->restoreSslProvisioningState();
|
|
||||||
$this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit);
|
$this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit);
|
||||||
$this->password_min = (int) Setting::get('password_min', $this->password_min);
|
$this->password_min = (int) Setting::get('password_min', $this->password_min);
|
||||||
|
|
||||||
|
|
@ -217,72 +213,6 @@ class SettingsForm extends Component
|
||||||
return rtrim($value, '/');
|
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
|
public function loadSslStatus(): void
|
||||||
{
|
{
|
||||||
clearstatcache(true);
|
clearstatcache(true);
|
||||||
|
|
@ -335,21 +265,6 @@ class SettingsForm extends Component
|
||||||
$this->sslCerts = $certs;
|
$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 updatedUiDomain(): void { $this->validateOnly('ui_domain', $this->domainRules(), $this->domainMessages()); }
|
||||||
public function updatedMailDomain(): void { $this->validateOnly('mail_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()); }
|
public function updatedWebmailDomain(): void { $this->validateOnly('webmail_domain', $this->domainRules(), $this->domainMessages()); }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
<x-slot:breadcrumb>SSL/TLS</x-slot:breadcrumb>
|
<x-slot:breadcrumb>SSL/TLS</x-slot:breadcrumb>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@if($sslProvisioning)
|
||||||
|
<div wire:poll.2s="pollSsl"></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="mbx-page-header">
|
<div class="mbx-page-header">
|
||||||
<div class="mbx-page-title">
|
<div class="mbx-page-title">
|
||||||
|
|
@ -25,10 +28,120 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mbx-sections">
|
<div class="mbx-sections">
|
||||||
|
|
||||||
|
{{-- ═══ Zertifikate einrichten / erneuern ═══ --}}
|
||||||
<div class="mbx-section">
|
<div class="mbx-section">
|
||||||
<div class="mbx-domain-head">
|
<div class="mbx-domain-head">
|
||||||
<div class="mbx-domain-info">
|
<div class="mbx-domain-info">
|
||||||
<span class="mbx-badge-mute">Let's Encrypt Zertifikate</span>
|
<span class="mbx-badge-mute">Let's Encrypt einrichten</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:14px 16px;display:flex;flex-direction:column;gap:12px">
|
||||||
|
|
||||||
|
{{-- Domain-Übersicht --}}
|
||||||
|
@php
|
||||||
|
$domainMap = [
|
||||||
|
'ui' => ['label' => 'UI', 'domain' => $uiDomain],
|
||||||
|
'webmail' => ['label' => 'Webmail', 'domain' => $webmailDomain],
|
||||||
|
'mail' => ['label' => 'Mailserver', 'domain' => $mailDomain],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
<div style="display:flex;flex-direction:column;gap:5px">
|
||||||
|
@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
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:7px;background:var(--mw-bg3);border:1px solid var(--mw-b2)">
|
||||||
|
@if(!$d)
|
||||||
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:var(--mw-t4)"><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>
|
||||||
|
@elseif($certOk)
|
||||||
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:#22c55e"><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"/><path d="M5 9.5l1.3 1.3 2.7-2.7" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
@else
|
||||||
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:#f87171"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M5 5l4 4M9 5l-4 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||||
|
@endif
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<span style="font-size:11px;font-weight:500;color:var(--mw-t3)">{{ $info['label'] }}</span>
|
||||||
|
@if($d)
|
||||||
|
<span style="font-size:10.5px;color:var(--mw-t4);margin-left:6px;font-family:monospace">{{ $d }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if(!$d)
|
||||||
|
<span style="font-size:10.5px;color:var(--mw-t4)">Keine Domain</span>
|
||||||
|
@elseif($certOk)
|
||||||
|
<span style="font-size:10.5px;color:#22c55e">Vorhanden</span>
|
||||||
|
@else
|
||||||
|
<span style="font-size:10.5px;color:#f87171">Fehlt</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Fortschritt während Provisioning --}}
|
||||||
|
@if($sslProvisioning || $sslDone)
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:8px;background:rgba(99,102,241,.06);border:1px solid rgba(99,102,241,.2)">
|
||||||
|
<div style="font-size:11px;font-weight:600;color:var(--mw-t2);margin-bottom:2px">
|
||||||
|
{{ $sslProvisioning ? 'Zertifikate werden eingerichtet…' : 'Einrichtung abgeschlossen' }}
|
||||||
|
</div>
|
||||||
|
@foreach(['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver'] as $key => $label)
|
||||||
|
@php $st = $sslProgress[$key] ?? 'pending'; @endphp
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
@if($st === 'done')
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:#22c55e"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.1"/><path d="M3.5 6l1.8 1.8 3.2-3.2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
@elseif($st === 'error' || $st === 'nodns')
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:#f87171"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.1"/><path d="M4 4l4 4M8 4l-4 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||||
|
@elseif($st === 'running')
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:#818cf8;animation:spin 1s linear infinite"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.3" stroke-dasharray="16 8" stroke-linecap="round"/></svg>
|
||||||
|
@else
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:var(--mw-t4)"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.1" stroke-dasharray="3 3"/></svg>
|
||||||
|
@endif
|
||||||
|
<span style="font-size:11px;color:{{ in_array($st,['error','nodns']) ? '#f87171' : ($st==='done' ? '#22c55e' : 'var(--mw-t3)') }}">
|
||||||
|
{{ $label }}
|
||||||
|
@if($st === 'nodns') <span style="font-size:10px"> — kein DNS-Eintrag</span>
|
||||||
|
@elseif($st === 'error') <span style="font-size:10px"> — fehlgeschlagen</span>
|
||||||
|
@elseif($st === 'running') <span style="font-size:10px;color:var(--mw-t4)"> — läuft…</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Button --}}
|
||||||
|
@if(!$uiDomain || !$webmailDomain || !$mailDomain)
|
||||||
|
<div style="font-size:11.5px;color:var(--mw-t4);padding:8px 0">
|
||||||
|
Bitte erst alle Domains unter
|
||||||
|
<a href="{{ route('settings') }}" style="color:var(--mw-v)">Einstellungen</a>
|
||||||
|
speichern.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<button wire:click="startSslProvisioning"
|
||||||
|
wire:loading.attr="disabled"
|
||||||
|
wire:target="startSslProvisioning"
|
||||||
|
@disabled($sslProvisioning)
|
||||||
|
class="{{ $sslDone ? 'mw-btn-secondary' : 'mw-btn-primary' }}"
|
||||||
|
style="width:100%;justify-content:center">
|
||||||
|
<svg width="12" height="12" 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">
|
||||||
|
@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>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ═══ Ausgestellte Zertifikate ═══ --}}
|
||||||
|
<div class="mbx-section">
|
||||||
|
<div class="mbx-domain-head">
|
||||||
|
<div class="mbx-domain-info">
|
||||||
|
<span class="mbx-badge-mute">Ausgestellte Zertifikate</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -49,7 +162,7 @@
|
||||||
<path d="M7 8V6a5 5 0 0 1 10 0v2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
<path d="M7 8V6a5 5 0 0 1 10 0v2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div style="font-size:12.5px;color:var(--mw-t3)">Noch keine Zertifikate ausgestellt</div>
|
<div style="font-size:12.5px;color:var(--mw-t3)">Noch keine Zertifikate ausgestellt</div>
|
||||||
<div style="font-size:11px;color:var(--mw-t4);margin-top:4px">certbot ist installiert — führe <code style="font-size:10px">certbot certonly</code> aus um ein Zertifikat zu erstellen</div>
|
<div style="font-size:11px;color:var(--mw-t4);margin-top:4px">Oben auf "Let's Encrypt Zertifikate einrichten" klicken</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="mbx-table-wrap">
|
<div class="mbx-table-wrap">
|
||||||
|
|
@ -58,7 +171,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="mbx-th">Name</th>
|
<th class="mbx-th">Name</th>
|
||||||
<th class="mbx-th">Domains</th>
|
<th class="mbx-th">Domains</th>
|
||||||
<th class="mbx-th" style="width:110px">Ablauf</th>
|
<th class="mbx-th" style="width:130px">Gültig bis</th>
|
||||||
<th class="mbx-th" style="width:90px">Status</th>
|
<th class="mbx-th" style="width:90px">Status</th>
|
||||||
<th class="mbx-th mbx-th-right" style="width:70px">Aktionen</th>
|
<th class="mbx-th mbx-th-right" style="width:70px">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -79,9 +192,14 @@
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="mbx-td mbx-td-muted">
|
<td class="mbx-td">
|
||||||
@if($cert['days_left'] !== null)
|
@if($cert['expiry_date'])
|
||||||
{{ $cert['days_left'] }} Tage
|
<span style="font-size:12px;color:var(--mw-t2)">{{ $cert['expiry_date'] }}</span>
|
||||||
|
@if($cert['days_left'] !== null)
|
||||||
|
<div style="font-size:10.5px;color:var(--mw-t4);margin-top:1px">noch {{ $cert['days_left'] }} Tage</div>
|
||||||
|
@endif
|
||||||
|
@elseif($cert['days_left'] !== null)
|
||||||
|
<span style="color:var(--mw-t3)">{{ $cert['days_left'] }} Tage</span>
|
||||||
@else
|
@else
|
||||||
<span style="color:var(--mw-t4)">—</span>
|
<span style="color:var(--mw-t4)">—</span>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -111,6 +229,6 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -294,18 +294,17 @@
|
||||||
<div class="mbx-domain-info">
|
<div class="mbx-domain-info">
|
||||||
<span class="mbx-badge-mute">SSL-Zertifikate</span>
|
<span class="mbx-badge-mute">SSL-Zertifikate</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mbx-domain-actions">
|
||||||
|
<a href="{{ route('ssl') }}" class="mbx-act-btn" title="SSL-Verwaltung öffnen">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><path d="M5.5 2H2a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><path d="M9 1h4v4M13 1 7.5 6.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:14px 16px;display:flex;flex-direction:column;gap:12px">
|
<div style="padding:12px 16px;display:flex;flex-direction:column;gap:6px">
|
||||||
|
@php $certLabels = ['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver']; @endphp
|
||||||
{{-- Cert-Status-Tabelle --}}
|
|
||||||
@php
|
|
||||||
$certLabels = ['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver'];
|
|
||||||
@endphp
|
|
||||||
<div style="display:flex;flex-direction:column;gap:6px">
|
|
||||||
@foreach(['ui','webmail','mail'] as $key)
|
@foreach(['ui','webmail','mail'] as $key)
|
||||||
@php $c = $sslCerts[$key] ?? null; @endphp
|
@php $c = $sslCerts[$key] ?? null; @endphp
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:7px;background:var(--mw-bg3);border:1px solid var(--mw-b2)">
|
<div style="display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:7px;background:var(--mw-bg3);border:1px solid var(--mw-b2)">
|
||||||
{{-- Icon --}}
|
|
||||||
@if(!$c || $c['status'] === 'nodomain')
|
@if(!$c || $c['status'] === 'nodomain')
|
||||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:var(--mw-t4)"><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>
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:var(--mw-t4)"><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>
|
||||||
@elseif($c['status'] === 'ok')
|
@elseif($c['status'] === 'ok')
|
||||||
|
|
@ -315,12 +314,10 @@
|
||||||
@else
|
@else
|
||||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:#f87171"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M5 5l4 4M9 5l-4 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:#f87171"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M5 5l4 4M9 5l-4 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||||
@endif
|
@endif
|
||||||
{{-- Label + Domain --}}
|
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0">
|
||||||
<span style="font-size:11px;font-weight:500;color:var(--mw-t3)">{{ $certLabels[$key] }}</span>
|
<span style="font-size:11px;font-weight:500;color:var(--mw-t3)">{{ $certLabels[$key] }}</span>
|
||||||
<span style="font-size:10.5px;color:var(--mw-t4);margin-left:6px;font-family:monospace">{{ $c['domain'] ?? '—' }}</span>
|
<span style="font-size:10.5px;color:var(--mw-t4);margin-left:6px;font-family:monospace">{{ $c['domain'] ?? '—' }}</span>
|
||||||
</div>
|
</div>
|
||||||
{{-- Status --}}
|
|
||||||
@if(!$c || $c['status'] === 'nodomain')
|
@if(!$c || $c['status'] === 'nodomain')
|
||||||
<span style="font-size:10.5px;color:var(--mw-t4)">Keine Domain</span>
|
<span style="font-size:10.5px;color:var(--mw-t4)">Keine Domain</span>
|
||||||
@elseif($c['status'] === 'missing')
|
@elseif($c['status'] === 'missing')
|
||||||
|
|
@ -334,69 +331,10 @@
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
<a href="{{ route('ssl') }}" style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--mw-v);padding:4px 2px;text-decoration:none">
|
||||||
|
|
||||||
{{-- Fortschritt während Provisioning --}}
|
|
||||||
@if($sslProvisioning || $sslDone)
|
|
||||||
@if($sslProvisioning)
|
|
||||||
<div wire:poll.2s="pollSsl"></div>
|
|
||||||
@endif
|
|
||||||
<div style="display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:8px;background:rgba(99,102,241,.06);border:1px solid rgba(99,102,241,.2)">
|
|
||||||
<div style="font-size:11px;font-weight:600;color:var(--mw-t2);margin-bottom:2px">
|
|
||||||
{{ $sslProvisioning ? 'Zertifikate werden eingerichtet…' : 'Einrichtung abgeschlossen' }}
|
|
||||||
</div>
|
|
||||||
@foreach(['ui' => 'UI', 'webmail' => 'Webmail', 'mail' => 'Mailserver'] as $key => $label)
|
|
||||||
@php $st = $sslProgress[$key] ?? 'pending'; @endphp
|
|
||||||
<div style="display:flex;align-items:center;gap:8px">
|
|
||||||
@if($st === 'done')
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:#22c55e"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.1"/><path d="M3.5 6l1.8 1.8 3.2-3.2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
@elseif($st === 'error' || $st === 'nodns')
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:#f87171"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.1"/><path d="M4 4l4 4M8 4l-4 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
||||||
@elseif($st === 'skip')
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:var(--mw-t4)"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.1"/><path d="M4 6h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
||||||
@elseif($st === 'running')
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:#818cf8;animation:spin 1s linear infinite"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.3" stroke-dasharray="16 8" stroke-linecap="round"/></svg>
|
|
||||||
@else
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink:0;color:var(--mw-t4)"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.1" stroke-dasharray="3 3"/></svg>
|
|
||||||
@endif
|
|
||||||
<span style="font-size:11px;color:{{ in_array($st,['error','nodns']) ? '#f87171' : ($st==='done' ? '#22c55e' : 'var(--mw-t3)') }}">
|
|
||||||
{{ $label }}
|
|
||||||
@if($st === 'nodns') <span style="font-size:10px"> — kein DNS-Eintrag</span>
|
|
||||||
@elseif($st === 'error') <span style="font-size:10px"> — fehlgeschlagen</span>
|
|
||||||
@elseif($st === 'skip') <span style="font-size:10px;color:var(--mw-t4)"> — übersprungen</span>
|
|
||||||
@elseif($st === 'running') <span style="font-size:10px;color:var(--mw-t4)"> — läuft…</span>
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Buttons --}}
|
|
||||||
@if(app()->isProduction())
|
|
||||||
<button wire:click="startSslProvisioning"
|
|
||||||
wire:loading.attr="disabled"
|
|
||||||
wire:target="startSslProvisioning"
|
|
||||||
@disabled($sslProvisioning)
|
|
||||||
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>
|
<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">
|
Zertifikate einrichten / erneuern →
|
||||||
@if($sslProvisioning) Läuft…
|
</a>
|
||||||
@elseif($sslDone) Erneut einrichten
|
|
||||||
@else Let's Encrypt Zertifikate einrichten
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
<span wire:loading wire:target="startSslProvisioning">Startet…</span>
|
|
||||||
</button>
|
|
||||||
@else
|
|
||||||
<button wire:click="openSslModal"
|
|
||||||
class="mbx-btn-mute" style="width:100%;justify-content:center;font-size:11.5px">
|
|
||||||
<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>
|
|
||||||
SSL manuell erzwingen (certbot)
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue