Feature: Wizard Schritt 5 — Domain-Setup mit Fortschrittsanzeige
- Neuer Schritt 5: SSL-Registrierung läuft im Hintergrund pro Domain - Artisan-Command mailwolt:wizard-domains schreibt per-Domain Status-Dateien - Wizard pollt alle 2s: pending → running → done/nodns/error - "Zum Login" Button erscheint wenn alle Domains abgeschlossen - Mail-Domain erhält ebenfalls SSL-Zertifikat (für STARTTLS/IMAPS) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main v1.1.147
parent
35fe7c2c6f
commit
077a029ff4
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class WizardDomains extends Command
|
||||
{
|
||||
protected $signature = 'mailwolt:wizard-domains
|
||||
{--ui= : UI-Domain}
|
||||
{--mail= : Mail-Domain}
|
||||
{--webmail= : Webmail-Domain}
|
||||
{--ssl=1 : SSL automatisch (1/0)}';
|
||||
|
||||
protected $description = 'Wizard: Domains einrichten mit Status-Dateien';
|
||||
|
||||
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$ui = $this->option('ui');
|
||||
$mail = $this->option('mail');
|
||||
$webmail = $this->option('webmail');
|
||||
$ssl = (bool)(int)$this->option('ssl');
|
||||
|
||||
@mkdir(self::STATE_DIR, 0755, true);
|
||||
|
||||
// Start: alle auf pending
|
||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'pending');
|
||||
}
|
||||
|
||||
$domains = ['ui' => $ui, 'mail' => $mail, 'webmail' => $webmail];
|
||||
$allOk = true;
|
||||
|
||||
foreach ($domains as $key => $domain) {
|
||||
if (!$domain) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'skip');
|
||||
continue;
|
||||
}
|
||||
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'running');
|
||||
|
||||
// DNS prüfen
|
||||
$hasDns = checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
|
||||
if (!$hasDns) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'nodns');
|
||||
$allOk = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// SSL-Zertifikat anfordern
|
||||
if ($ssl) {
|
||||
$out = shell_exec(sprintf(
|
||||
'sudo -n certbot certonly --nginx --non-interactive --agree-tos -m root@%s -d %s 2>&1',
|
||||
escapeshellarg($domain),
|
||||
escapeshellarg($domain)
|
||||
));
|
||||
$certOk = str_contains((string) $out, 'Successfully') || str_contains((string) $out, 'Certificate not yet due for renewal');
|
||||
if (!$certOk) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
||||
$allOk = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'done');
|
||||
}
|
||||
|
||||
// Nginx neu konfigurieren (alle Domains auf einmal)
|
||||
if ($allOk) {
|
||||
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
||||
shell_exec(sprintf(
|
||||
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d 2>&1',
|
||||
escapeshellarg($helper),
|
||||
escapeshellarg($ui),
|
||||
escapeshellarg($webmail),
|
||||
escapeshellarg($mail),
|
||||
$ssl ? 1 : 0,
|
||||
));
|
||||
}
|
||||
|
||||
file_put_contents(self::STATE_DIR . '/done', $allOk ? '1' : '0');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ use Livewire\Component;
|
|||
class Wizard extends Component
|
||||
{
|
||||
public int $step = 1;
|
||||
public int $totalSteps = 4;
|
||||
public int $totalSteps = 5;
|
||||
|
||||
// Schritt 1 — System
|
||||
public string $instance_name = 'Mailwolt';
|
||||
|
|
@ -32,6 +32,16 @@ class Wizard extends Component
|
|||
public string $admin_password = '';
|
||||
public string $admin_password_confirmation = '';
|
||||
|
||||
// Schritt 5 — Domain-Setup Status
|
||||
public array $domainStatus = [
|
||||
'ui' => 'pending',
|
||||
'mail' => 'pending',
|
||||
'webmail' => 'pending',
|
||||
];
|
||||
public bool $setupDone = false;
|
||||
|
||||
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->instance_name = config('app.name', 'Mailwolt');
|
||||
|
|
@ -82,16 +92,15 @@ class Wizard extends Component
|
|||
$this->step = max($this->step - 1, 1);
|
||||
}
|
||||
|
||||
public function finish(): mixed
|
||||
public function finish(): void
|
||||
{
|
||||
// Schritt-3-Validierung nochmals sicherstellen
|
||||
$this->validate([
|
||||
'admin_name' => 'required|string|min:2|max:64',
|
||||
'admin_email' => 'required|email|max:190',
|
||||
'admin_password' => 'required|string|min:10|same:admin_password_confirmation',
|
||||
]);
|
||||
|
||||
// Settings speichern
|
||||
// Settings + .env speichern
|
||||
Setting::setMany([
|
||||
'locale' => $this->locale,
|
||||
'timezone' => $this->timezone,
|
||||
|
|
@ -101,7 +110,6 @@ class Wizard extends Component
|
|||
'setup_completed' => '1',
|
||||
]);
|
||||
|
||||
// .env aktualisieren
|
||||
$this->writeEnv([
|
||||
'APP_NAME' => $this->instance_name,
|
||||
'APP_HOST' => $this->ui_domain,
|
||||
|
|
@ -110,7 +118,7 @@ class Wizard extends Component
|
|||
'WEBMAIL_DOMAIN' => $this->webmail_domain,
|
||||
]);
|
||||
|
||||
// Admin anlegen oder aktualisieren
|
||||
// Admin anlegen
|
||||
$user = User::where('email', $this->admin_email)->first() ?? new User();
|
||||
$user->name = $this->admin_name;
|
||||
$user->email = $this->admin_email;
|
||||
|
|
@ -118,6 +126,45 @@ class Wizard extends Component
|
|||
$user->role = 'admin';
|
||||
$user->save();
|
||||
|
||||
// Status-Verzeichnis leeren und Domain-Setup im Hintergrund starten
|
||||
@mkdir(self::STATE_DIR, 0755, true);
|
||||
@unlink(self::STATE_DIR . '/done');
|
||||
foreach (['ui', 'mail', 'webmail'] as $k) {
|
||||
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
||||
}
|
||||
|
||||
$ssl = 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);
|
||||
|
||||
$this->step = 5;
|
||||
}
|
||||
|
||||
public function pollSetup(): void
|
||||
{
|
||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||
$file = self::STATE_DIR . "/{$key}";
|
||||
$this->domainStatus[$key] = is_readable($file)
|
||||
? trim(@file_get_contents($file))
|
||||
: 'pending';
|
||||
}
|
||||
|
||||
$done = @file_get_contents(self::STATE_DIR . '/done');
|
||||
if ($done !== false) {
|
||||
$this->setupDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function goToLogin(): mixed
|
||||
{
|
||||
return redirect()->route('login')->with('setup_done', true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
</div>
|
||||
|
||||
{{-- ═══ Schritt-Indikator ═══ --}}
|
||||
@if($step < 5)
|
||||
<div style="display:flex;align-items:center;gap:0;margin-bottom:28px">
|
||||
@foreach([1 => 'System', 2 => 'Domains', 3 => 'Admin', 4 => 'Fertig'] as $n => $label)
|
||||
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:5px">
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ═══ Karte ═══ --}}
|
||||
<div style="background:var(--mw-bg2);border:1px solid var(--mw-b2);border-radius:14px;padding:28px 24px">
|
||||
|
|
@ -47,7 +49,6 @@
|
|||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">System-Einstellungen</div>
|
||||
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">Grundkonfiguration für deine Mailwolt-Instanz</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<div>
|
||||
<label class="mw-modal-label">Instanzname</label>
|
||||
|
|
@ -78,12 +79,10 @@
|
|||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains</div>
|
||||
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">Domains müssen bereits auf diesen Server zeigen</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border-radius:8px;background:rgba(251,191,36,.06);border:1px solid rgba(251,191,36,.2);margin-bottom:16px">
|
||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;margin-top:1px;color:#f59e0b"><path d="M7 1.5L12.5 11H1.5L7 1.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M7 5.5v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="7" cy="10" r=".7" fill="currentColor"/></svg>
|
||||
<span style="font-size:11.5px;color:rgba(251,191,36,.85);line-height:1.5">DNS-Einträge zuerst setzen, dann hier eintragen. Kein <code style="font-size:10.5px">http://</code> am Anfang.</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<div>
|
||||
<label class="mw-modal-label">UI Domain <span style="color:var(--mw-t4);font-weight:400">(Control Panel)</span></label>
|
||||
|
|
@ -91,7 +90,7 @@
|
|||
@error('ui_domain') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="mw-modal-label">Mailserver Domain <span style="color:var(--mw-t4);font-weight:400">(MX / SMTP)</span></label>
|
||||
<label class="mw-modal-label">Mailserver Domain <span style="color:var(--mw-t4);font-weight:400">(MX / SMTP / IMAP)</span></label>
|
||||
<input type="text" wire:model="mail_domain" class="mw-modal-input" placeholder="mx.example.com">
|
||||
@error('mail_domain') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
|
@ -108,7 +107,6 @@
|
|||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Admin-Account</div>
|
||||
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">Dieser Account hat vollen Zugriff auf das Control Panel</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<div>
|
||||
<label class="mw-modal-label">Name</label>
|
||||
|
|
@ -137,7 +135,6 @@
|
|||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Alles bereit</div>
|
||||
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">Überprüfe die Einstellungen und schließe die Einrichtung ab</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:4px">
|
||||
@foreach([
|
||||
['label' => 'Instanz', 'value' => $instance_name],
|
||||
|
|
@ -153,11 +150,54 @@
|
|||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- ── Schritt 5: Domain-Setup ── --}}
|
||||
@elseif($step === 5)
|
||||
@if(!$setupDone)
|
||||
<div wire:poll.2s="pollSetup"></div>
|
||||
@endif
|
||||
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains werden eingerichtet</div>
|
||||
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">SSL-Zertifikate werden beantragt und Nginx wird konfiguriert</div>
|
||||
</div>
|
||||
|
||||
@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-t5)', 'bg' => 'var(--mw-bg3)', 'label' => 'Übersprungen'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
@foreach(['ui' => 'UI Domain', 'mail' => 'Mail Domain', 'webmail' => 'Webmail Domain'] as $key => $typeLabel)
|
||||
@php
|
||||
$st = $domainStatus[$key] ?? 'pending';
|
||||
$cfg = $statusConfig[$st] ?? $statusConfig['pending'];
|
||||
@endphp
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:{{ $cfg['bg'] }};border-radius:8px;border:1px solid var(--mw-b2)">
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:{{ $cfg['bg'] }};border:1px solid {{ $cfg['color'] }};display:flex;align-items:center;justify-content:center;font-size:13px;color:{{ $cfg['color'] }};flex-shrink:0;{{ isset($cfg['spin']) ? 'animation:spin .9s linear infinite' : '' }}">
|
||||
{{ $cfg['icon'] }}
|
||||
</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:11px;color:var(--mw-t4)">{{ $typeLabel }}</div>
|
||||
<div style="font-size:12.5px;color:var(--mw-t1);font-family:monospace;margin-top:1px">{{ $labels[$key] }}</div>
|
||||
</div>
|
||||
<span style="font-size:11.5px;color:{{ $cfg['color'] }}">{{ $cfg['label'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ═══ Navigation ═══ --}}
|
||||
@if($step < 5)
|
||||
<div style="display:flex;align-items:center;margin-top:20px;gap:10px">
|
||||
@if($step > 1)
|
||||
<button wire:click="back" class="mbx-btn-mute" style="font-size:12.5px">
|
||||
|
|
@ -180,5 +220,13 @@
|
|||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@elseif($step === 5 && $setupDone)
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:20px">
|
||||
<button wire:click="goToLogin" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Zum Login
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue