From 01e7db589a53a3ddf44f91c8796230450376b131 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 24 Apr 2026 13:16:42 +0200 Subject: [PATCH] Fix: Installer + Wizard Step 5 robuster gegen IPv6/SSL-Fehler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - installer.sh: mailwolt-apply-domains mit 3-Phasen certbot (HTTP → LE → SSL), IPv6-Check vor certbot, Zertifikat-Ablauf-Check (10 Tage), Version-Datei schreiben - WizardDomains: noipv6-Status aus Helper-Output erkennen - Wizard: retryDomains()-Methode für Wiederholung ohne neuen Wizard-Durchlauf - Step 5 Blade: Hints pro Fehlerstatus, Retry-Button, "Trotzdem zum Login" - UpdatePage: Version aus Datei, Fallback auf git describe (kein "dev" mehr) - UpdatePage: refreshLowLevelState behandelt fehlende State-Datei als idle Co-Authored-By: Claude Sonnet 4.6 --- app/Console/Commands/WizardDomains.php | 10 +- app/Livewire/Setup/Wizard.php | 23 +++ app/Livewire/Ui/System/UpdatePage.php | 14 +- installer.sh | 193 +++++++++++++----- .../views/livewire/setup/wizard.blade.php | 92 +++++++-- 5 files changed, 260 insertions(+), 72 deletions(-) diff --git a/app/Console/Commands/WizardDomains.php b/app/Console/Commands/WizardDomains.php index 6e8b147..0bcd9e3 100644 --- a/app/Console/Commands/WizardDomains.php +++ b/app/Console/Commands/WizardDomains.php @@ -67,12 +67,18 @@ class WizardDomains extends Command $ssl ? 1 : 0, )); - $helperOk = $out !== null && !str_contains((string) $out, '[x]'); + $helperOk = $out !== null && !str_contains((string) $out, '[x]'); + $outStr = (string) $out; foreach (['ui', 'mail', 'webmail'] as $key) { $status = file_get_contents(self::STATE_DIR . "/{$key}"); if ($status === 'running' || $status === 'pending') { - file_put_contents(self::STATE_DIR . "/{$key}", $helperOk ? 'done' : 'error'); + $domain = $domains[$key] ?? ''; + if ($domain && str_contains($outStr, "[!] {$domain}:")) { + file_put_contents(self::STATE_DIR . "/{$key}", 'noipv6'); + } else { + file_put_contents(self::STATE_DIR . "/{$key}", $helperOk ? 'done' : 'error'); + } } } diff --git a/app/Livewire/Setup/Wizard.php b/app/Livewire/Setup/Wizard.php index 345d71e..6bb16d8 100644 --- a/app/Livewire/Setup/Wizard.php +++ b/app/Livewire/Setup/Wizard.php @@ -182,6 +182,29 @@ class Wizard extends Component } } + public function retryDomains(): void + { + @unlink(self::STATE_DIR . '/done'); + foreach (['ui', 'mail', 'webmail'] as $k) { + file_put_contents(self::STATE_DIR . "/{$k}", 'pending'); + } + + $this->domainStatus = ['ui' => 'pending', 'mail' => 'pending', 'webmail' => 'pending']; + $this->setupDone = false; + + $ssl = (!$this->skipSsl && app()->isProduction()) ? 1 : 0; + $artisan = base_path('artisan'); + $cmd = sprintf( + 'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &', + escapeshellarg($artisan), + escapeshellarg($this->ui_domain), + escapeshellarg($this->mail_domain), + escapeshellarg($this->webmail_domain), + $ssl, + ); + @shell_exec($cmd); + } + public function goToLogin(): mixed { return redirect()->route('login')->with('setup_done', true); diff --git a/app/Livewire/Ui/System/UpdatePage.php b/app/Livewire/Ui/System/UpdatePage.php index 21c8aa9..d055a22 100644 --- a/app/Livewire/Ui/System/UpdatePage.php +++ b/app/Livewire/Ui/System/UpdatePage.php @@ -205,7 +205,7 @@ class UpdatePage extends Component $rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: ''); $this->lowState = $state !== '' ? $state : null; - $this->running = ($this->lowState !== 'done'); + $this->running = ($this->lowState === 'running'); $this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null; } @@ -255,16 +255,14 @@ class UpdatePage extends Component protected function readCurrentVersion(): ?string { - // Lokal: direkt aus git describe lesen damit Entwicklungsumgebung immer aktuell ist - if (app()->isLocal()) { - $tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null')); - $v = $this->normalizeVersion($tag); - if ($v) return $v; - } - $v = @trim(@file_get_contents(self::VERSION_FILE) ?: ''); if ($v !== '') return $v; + // Fallback: git tag (lokal immer, production wenn Datei fehlt) + $tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null')); + $v = $this->normalizeVersion($tag); + if ($v) return $v; + $raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: ''); if ($raw !== '') return $this->normalizeVersion($raw); diff --git a/installer.sh b/installer.sh index 8064885..0d46922 100644 --- a/installer.sh +++ b/installer.sh @@ -583,20 +583,11 @@ PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock" APP_DIR="/var/www/mailwolt" NGINX_SITE="/etc/nginx/sites-available/mailwolt.conf" +ACME_ROOT="/var/www/letsencrypt" +mkdir -p "${ACME_ROOT}/.well-known/acme-challenge" -# Alle Server-Namen sammeln -ALL_NAMES="${UI_HOST} ${WEBMAIL_HOST}" - -# Zertifikat-Pfade ermitteln (certbot oder self-signed) -if [ "$SSL_AUTO" = "1" ] && [ -f "/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" ]; then - CERT="/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" - KEY="/etc/letsencrypt/live/${UI_HOST}/privkey.pem" -else - CERT="/etc/mailwolt/ssl/cert.pem" - KEY="/etc/mailwolt/ssl/key.pem" -fi - -cat > "$NGINX_SITE" < "${NGINX_SITE}" </dev/null | grep -q 'inet6' +} + +cert_needs_action() { + local domain="$1" + local cert="/etc/letsencrypt/live/${domain}/fullchain.pem" + [ ! -f "${cert}" ] && return 0 + # 0 = gültig für >10 Tage → überspringen; 1 = läuft ab → erneuern + openssl x509 -checkend 864000 -noout -in "${cert}" 2>/dev/null && return 1 + return 0 +} + +certbot_safe() { + local domain="$1" + local has_aaaa + has_aaaa=$(dig +short AAAA "${domain}" 2>/dev/null | head -1) + if [ -n "${has_aaaa}" ] && ! has_global_ipv6; then + echo "[!] ${domain}: AAAA-Record vorhanden aber kein IPv6 auf diesem Server — Let's Encrypt würde fehlschlagen. Self-signed wird verwendet." >&2 + return 1 + fi + certbot certonly --webroot \ + -w "${ACME_ROOT}" \ + -d "${domain}" \ + --non-interactive --agree-tos \ + --email "webmaster@${domain}" \ + --no-eff-email +} + +if [ "${SSL_AUTO}" = "1" ]; then + for DOMAIN in "${UI_HOST}" "${WEBMAIL_HOST}"; do + [ -z "${DOMAIN}" ] && continue + if cert_needs_action "${DOMAIN}"; then + certbot_safe "${DOMAIN}" || true + fi + done +fi + +# --- Phase 3: Finale Vhosts (LE-Cert oder self-signed Fallback) --- +( +cat < "${NGINX_SITE}" + nginx -t && systemctl reload nginx HELPER chmod 755 /usr/local/sbin/mailwolt-apply-domains -# ===== Sudoers für www-data (certbot + helper) ===== +# ===== Sudoers für www-data (helper) ===== cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS' -www-data ALL=(root) NOPASSWD: /usr/bin/certbot +www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains SUDOERS chmod 440 /etc/sudoers.d/mailwolt-certbot # git safe.directory damit spätere pulls als root möglich sind git config --global --add safe.directory "${APP_DIR}" || true +# ===== Version-Datei schreiben ===== +mkdir -p /var/lib/mailwolt +GIT_TAG="$(sudo -u "$APP_USER" -H bash -lc "git -C ${APP_DIR} describe --tags --abbrev=0 2>/dev/null || echo ''")" +if [ -n "$GIT_TAG" ]; then + echo "${GIT_TAG#v}" > /var/lib/mailwolt/version + echo "$GIT_TAG" > /var/lib/mailwolt/version_raw + log "Version: ${GIT_TAG}" +else + warn "Kein Git-Tag gefunden — Version-Datei wird nicht geschrieben" +fi + # ===== App-User/Gruppen & Rechte (am ENDE ausführen) ===== # User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe if ! id -u "$APP_USER" >/dev/null 2>&1; then diff --git a/resources/views/livewire/setup/wizard.blade.php b/resources/views/livewire/setup/wizard.blade.php index bc628b0..aa1a715 100644 --- a/resources/views/livewire/setup/wizard.blade.php +++ b/resources/views/livewire/setup/wizard.blade.php @@ -167,6 +167,11 @@
@endif + @php + $anyFailed = collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6'])); + $allDone = $setupDone; + @endphp +
Domains werden eingerichtet
SSL-Zertifikate werden beantragt und Nginx wird konfiguriert
@@ -175,33 +180,90 @@ @php $labels = ['ui' => $ui_domain, 'mail' => $mail_domain, 'webmail' => $webmail_domain]; $statusConfig = [ - 'pending' => ['icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)', 'label' => 'Warte …'], - 'running' => ['icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)', 'label' => 'Wird registriert …', 'spin' => true], - 'done' => ['icon' => '✓', 'color' => 'rgba(34,197,94,.9)', 'bg' => 'rgba(34,197,94,.07)', 'label' => 'Abgeschlossen'], - 'nodns' => ['icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)', 'label' => 'Kein DNS-Eintrag gefunden'], - 'error' => ['icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)', 'label' => 'Fehler bei Registrierung'], - 'skip' => ['icon' => '–', 'color' => 'var(--mw-t4)', 'bg' => 'var(--mw-bg3)', 'label' => 'SSL übersprungen'], + 'pending' => [ + 'icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)', + 'label' => 'Warte …', 'hint' => null, + ], + 'running' => [ + 'icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)', + 'label' => 'Wird registriert …', 'spin' => true, 'hint' => null, + ], + 'done' => [ + 'icon' => '✓', 'color' => 'rgba(34,197,94,.9)', 'bg' => 'rgba(34,197,94,.07)', + 'label' => 'SSL-Zertifikat ausgestellt', 'hint' => null, + ], + 'nodns' => [ + 'icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)', + 'label' => 'Kein DNS-Eintrag gefunden', + 'hint' => 'Der A-Record dieser Domain zeigt nicht auf diesen Server oder ist noch nicht propagiert. DNS-Einstellungen prüfen und danach Retry klicken.', + 'hints_extra' => ['DNS A-Record auf Server-IP setzen', 'DNS-Propagierung abwarten (bis zu 24h)', 'Mit dig +short A domain.com prüfen'], + ], + 'noipv6' => [ + 'icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)', + 'label' => 'IPv6 nicht konfiguriert', + 'hint' => 'Die Domain hat einen AAAA-Record, aber dieser Server hat kein aktives IPv6. Let\'s Encrypt prüft alle DNS-Records.', + 'hints_extra' => ['IPv6 am Server aktivieren', 'ODER: AAAA-Record aus dem DNS entfernen'], + ], + 'error' => [ + 'icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)', + 'label' => 'SSL-Zertifikat fehlgeschlagen', + 'hint' => 'Let\'s Encrypt konnte die Domain nicht verifizieren. Self-signed Zertifikat wird verwendet.', + 'hints_extra' => ['Port 80 muss von außen erreichbar sein', 'Firewall-Regeln prüfen (ufw allow 80)', 'AAAA-Record ohne IPv6 am Server entfernen', 'http://domain/.well-known/acme-challenge/ im Browser testen'], + ], + 'skip' => [ + 'icon' => '–', 'color' => 'var(--mw-t4)', 'bg' => 'var(--mw-bg3)', + 'label' => 'SSL übersprungen', 'hint' => null, + ], ]; @endphp -
+
@foreach(['ui' => 'UI Domain', 'mail' => 'Mail Domain', 'webmail' => 'Webmail Domain'] as $key => $typeLabel) @php $st = $domainStatus[$key] ?? 'pending'; $cfg = $statusConfig[$st] ?? $statusConfig['pending']; @endphp -
-
- {{ $cfg['icon'] }} +
+
+
+ {{ $cfg['icon'] }} +
+
+
{{ $typeLabel }}
+
{{ $labels[$key] }}
+
+ {{ $cfg['label'] }}
-
-
{{ $typeLabel }}
-
{{ $labels[$key] }}
+ @if(!empty($cfg['hint'])) +
+
+ {{ $cfg['hint'] }} +
+ @if(!empty($cfg['hints_extra'])) +
    + @foreach($cfg['hints_extra'] as $hint) +
  • {{ $hint }}
  • + @endforeach +
+ @endif
- {{ $cfg['label'] }} + @endif
@endforeach
+ + @if($allDone && $anyFailed) +
+
+ Einige Domains konnten nicht vollständig eingerichtet werden. Du kannst es erneut versuchen oder mit Self-signed Zertifikat fortfahren. +
+ +
+ @endif @endif
@@ -234,7 +296,7 @@
@endif