diff --git a/app/Livewire/Setup/Wizard.php b/app/Livewire/Setup/Wizard.php index 29fb65a..893ac09 100644 --- a/app/Livewire/Setup/Wizard.php +++ b/app/Livewire/Setup/Wizard.php @@ -2,212 +2,148 @@ namespace App\Livewire\Setup; -use App\Jobs\ProvisionCertJob; -use App\Support\Setting; -use App\Models\SystemTask; +use App\Models\Setting; use App\Models\User; -use App\Support\EnvWriter; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Redis; -use Livewire\Attributes\Validate; +use Livewire\Attributes\Layout; +use Livewire\Attributes\Title; use Livewire\Component; +#[Layout('layouts.setup')] +#[Title('Einrichtung · Mailwolt')] class Wizard extends Component { public int $step = 1; + public int $totalSteps = 4; - // Step 1 - #[Validate('required|string|min:3')] - public string $form_domain = ''; + // Schritt 1 — System + public string $instance_name = 'Mailwolt'; + public string $locale = 'de'; + public string $timezone = 'Europe/Berlin'; - #[Validate('required|timezone')] - public string $form_timezone = 'UTC'; + // Schritt 2 — Domains + public string $ui_domain = ''; + public string $mail_domain = ''; + public string $webmail_domain = ''; - public bool $form_cert_force_https = true; + // Schritt 3 — Admin-Account + public string $admin_name = ''; + public string $admin_email = ''; + public string $admin_password = ''; + public string $admin_password_confirmation = ''; - // Step 2 - #[Validate('required|string|min:3')] - public string $form_admin_name = ''; - - #[Validate('required|email')] - public string $form_admin_email = ''; - - /** optional: wenn du Login auch über username erlauben willst */ - public ?string $form_admin_username = null; - - #[Validate('required|string|min:8|same:form_admin_password_confirmation')] - public string $form_admin_password = ''; - - public string $form_admin_password_confirmation = ''; - - // Step 3 (Zertifikat) - public bool $form_cert_create_now = false; - - #[Validate('required_if:form_cert_create_now,true|email')] - public string $form_cert_email = ''; - - public function nextStep() + public function mount(): void { - if ($this->step === 1) { - $this->validateOnly('form_domain'); - $this->validateOnly('form_timezone'); - } elseif ($this->step === 2) { - $this->validate([ - 'form_admin_name' => 'required|string|min:3', - 'form_admin_email' => 'required|email', - 'form_admin_password' => 'required|string|min:8|same:form_admin_password_confirmation', - ]); - } - - $this->step = min($this->step + 1, 3); + $this->instance_name = config('app.name', 'Mailwolt'); + $this->timezone = Setting::get('timezone', 'Europe/Berlin'); + $this->locale = Setting::get('locale', 'de'); + $this->ui_domain = Setting::get('ui_domain', ''); + $this->mail_domain = Setting::get('mail_domain', ''); + $this->webmail_domain = Setting::get('webmail_domain', ''); } - public function prevStep() + public function next(): void + { + match ($this->step) { + 1 => $this->validate([ + 'instance_name' => 'required|string|min:2|max:64', + 'locale' => 'required|in:de,en,fr', + 'timezone' => 'required|timezone', + ]), + 2 => $this->validate([ + 'ui_domain' => ['required', 'regex:/^(?!https?:\/\/)(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/'], + 'mail_domain' => ['required', 'regex:/^(?!https?:\/\/)(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/'], + 'webmail_domain' => ['required', 'regex:/^(?!https?:\/\/)(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/'], + ], [ + 'ui_domain.required' => 'Pflichtfeld.', + 'mail_domain.required' => 'Pflichtfeld.', + 'webmail_domain.required' => 'Pflichtfeld.', + 'ui_domain.regex' => 'Ungültige Domain — kein Schema (http://) erlaubt.', + 'mail_domain.regex' => 'Ungültige Domain — kein Schema (http://) erlaubt.', + 'webmail_domain.regex' => 'Ungültige Domain — kein Schema (http://) erlaubt.', + ]), + 3 => $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', + 'admin_password_confirmation' => 'required', + ], [ + 'admin_password.min' => 'Mindestens 10 Zeichen.', + 'admin_password.same' => 'Passwörter stimmen nicht überein.', + ]), + default => null, + }; + + $this->step = min($this->step + 1, $this->totalSteps); + } + + public function back(): void { $this->step = max($this->step - 1, 1); } - public function finish() + public function finish(): mixed { - // Step 3 Validierung (nur wenn sofort erstellen) - if ($this->form_cert_create_now) { - $this->validateOnly('form_cert_email'); - } - - // 1) Settings persistieren - Setting::set('app.domain', $this->form_domain); - Setting::set('app.timezone', $this->form_timezone); - Setting::set('app.force_https', (bool)$this->form_cert_force_https); - - // Optional: .env spiegeln, damit URLs/HMR etc. sofort passen - $scheme = $this->form_cert_force_https ? 'https' : 'http'; - EnvWriter::set([ - 'APP_HOST' => $this->form_domain, - 'APP_URL' => "{$scheme}://{$this->form_domain}", - 'APP_TIMEZONE' => $this->form_timezone, + // 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', ]); - // 2) Admin anlegen/aktualisieren - $user = User::query() - ->where('email', $this->form_admin_email) - ->when($this->form_admin_username, fn($q) => $q->orWhere('username', $this->form_admin_username) - ) - ->first(); + // Settings speichern + Setting::setMany([ + 'locale' => $this->locale, + 'timezone' => $this->timezone, + 'ui_domain' => $this->ui_domain, + 'mail_domain' => $this->mail_domain, + 'webmail_domain' => $this->webmail_domain, + 'setup_completed' => '1', + ]); - if (!$user) { - $user = new User(); - $user->email = $this->form_admin_email; - if ($this->form_admin_username) { - $user->username = $this->form_admin_username; - } - } else { - // vorhandene Email/Username harmonisieren - $user->email = $this->form_admin_email; - if ($this->form_admin_username) { - $user->username = $this->form_admin_username; - } - } + // .env aktualisieren + $this->writeEnv([ + 'APP_NAME' => $this->instance_name, + 'APP_HOST' => $this->ui_domain, + 'APP_URL' => 'https://' . $this->ui_domain, + 'MTA_SUB' => explode('.', $this->mail_domain)[0] ?? '', + 'WEBMAIL_DOMAIN' => $this->webmail_domain, + ]); - $user->name = $this->form_admin_name; - $user->is_admin = true; - $user->password = Hash::make($this->form_admin_password); - $user->required_change_password = true; + // Admin anlegen oder aktualisieren + $user = User::where('email', $this->admin_email)->first() ?? new User(); + $user->name = $this->admin_name; + $user->email = $this->admin_email; + $user->password = Hash::make($this->admin_password); + $user->role = 'admin'; $user->save(); - // 3) Zertifikat jetzt ausstellen (optional) - $taskKey = 'issue-cert:' . $this->form_domain; + return redirect()->route('login')->with('setup_done', true); + } - if ($this->form_cert_create_now) { - SystemTask::updateOrCreate( - ['key' => $taskKey], - [ - 'type' => 'issue-cert', - 'status' => 'queued', - 'message' => 'Warte auf Ausführung…', - 'payload' => [ - 'domain' => $this->form_domain, - 'email' => $this->form_cert_email, - 'mode' => 'letsencrypt' - ], - ] - ); + private function writeEnv(array $values): void + { + $path = base_path('.env'); + $content = @file_get_contents($path) ?: ''; - Cache::store('redis')->put($taskKey, [ - 'type' => 'issue-cert', - 'status' => 'queued', - 'message' => 'Warte auf Ausführung…', - 'payload' => [ - 'domain' => $this->form_domain, - 'email' => $this->form_cert_create_now ? $this->form_cert_email : null, - 'mode' => $this->form_cert_create_now ? 'letsencrypt' : 'self-signed', - ], - ], now()->addMinutes(30)); + foreach ($values as $key => $value) { + $escaped = str_contains($value, ' ') ? '"' . $value . '"' : $value; + $line = $key . '=' . $escaped; + $pattern = '/^' . preg_quote($key, '/') . '=[^\r\n]*/m'; - Redis::sadd('ui:toasts', $taskKey); - - ProvisionCertJob::dispatch( - domain: $this->form_domain, - email: $this->form_cert_email, - taskKey: $taskKey, - useLetsEncrypt: true - ); - session()->flash('task_key', $taskKey); - session()->flash('banner_ok', 'Let’s Encrypt wird gestartet…'); - } else { - // automatisch self-signed - SystemTask::updateOrCreate( - ['key' => $taskKey], - [ - 'type' => 'issue-cert', - 'status' => 'queued', - 'message' => 'Warte auf Ausführung…', - 'payload' => [ - 'domain' => $this->form_domain, - 'mode' => 'self-signed' - ], - ] - ); - - Cache::store('redis')->put($taskKey, [ - 'type' => 'issue-cert', - 'status' => 'queued', - 'message' => 'Warte auf Ausführung…', - 'payload' => [ - 'domain' => $this->form_domain, - 'email' => $this->form_cert_create_now ? $this->form_cert_email : null, - 'mode' => $this->form_cert_create_now ? 'letsencrypt' : 'self-signed', - ], - ], now()->addMinutes(30)); - - Cache::store('redis')->put($taskKey, [ - 'type' => 'issue-cert', - 'status' => 'queued', - 'message' => 'Warte auf Ausführung…', - 'payload' => [ - 'domain' => $this->form_domain, - 'email' => $this->form_cert_create_now ? $this->form_cert_email : null, - 'mode' => $this->form_cert_create_now ? 'letsencrypt' : 'self-signed', - ], - ], now()->addMinutes(30)); - - Redis::sadd('ui:toasts', $taskKey); - - ProvisionCertJob::dispatch( - domain: $this->form_domain, - email: null, - taskKey: $taskKey, - useLetsEncrypt: false - ); - session()->flash('task_key', $taskKey); - session()->flash('banner_ok', 'Self-Signed Zertifikat wird erstellt…'); + if (preg_match($pattern, $content)) { + $content = preg_replace($pattern, $line, $content); + } else { + $content .= "\n{$line}"; + } } - return redirect()->route('dashboard'); + file_put_contents($path, $content); } public function render() { - return view('livewire.setup.wizard'); + $timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL); + return view('livewire.setup.wizard', compact('timezones')); } } diff --git a/resources/views/layouts/setup.blade.php b/resources/views/layouts/setup.blade.php new file mode 100644 index 0000000..54689fe --- /dev/null +++ b/resources/views/layouts/setup.blade.php @@ -0,0 +1,18 @@ + + + + + + Einrichtung · Mailwolt + @vite(['resources/css/app.css']) + @livewireStyles + + + + {{ $slot }} + + @livewireScripts + @stack('scripts') + + diff --git a/resources/views/livewire/setup/wizard.blade.php b/resources/views/livewire/setup/wizard.blade.php index 1d90cf3..c15308f 100644 --- a/resources/views/livewire/setup/wizard.blade.php +++ b/resources/views/livewire/setup/wizard.blade.php @@ -1,34 +1,184 @@ -
- -
-
- @if ($step === 1) - @include('livewire.setup.step-domain') - @elseif ($step === 2) - @include('livewire.setup.step-admin') - @elseif ($step === 3) - @include('livewire.setup.step-certificate') - @endif +
+ + {{-- ═══ Logo ═══ --}} +
+
+ + + +
- -
- @if ($step > 1) - - @else - - @endif - - @if ($step < 3) - - @else - - @endif +
+
MAILWOLT
+
Ersteinrichtung
+ + {{-- ═══ Schritt-Indikator ═══ --}} +
+ @foreach([1 => 'System', 2 => 'Domains', 3 => 'Admin', 4 => 'Fertig'] as $n => $label) +
+
+ @if($step > $n) + + @else + {{ $n }} + @endif +
+ {{ $label }} +
+ @if($n < 4) +
+ @endif + @endforeach +
+ + {{-- ═══ Karte ═══ --}} +
+ + {{-- ── Schritt 1: System ── --}} + @if($step === 1) +
+
System-Einstellungen
+
Grundkonfiguration für deine Mailwolt-Instanz
+
+ +
+
+ + + @error('instance_name')
{{ $message }}
@enderror +
+
+ + +
+
+ + +
+
+ + {{-- ── Schritt 2: Domains ── --}} + @elseif($step === 2) +
+
Domains
+
Domains müssen bereits auf diesen Server zeigen
+
+ +
+ + DNS-Einträge zuerst setzen, dann hier eintragen. Kein http:// am Anfang. +
+ +
+
+ + + @error('ui_domain')
{{ $message }}
@enderror +
+
+ + + @error('mail_domain')
{{ $message }}
@enderror +
+
+ + + @error('webmail_domain')
{{ $message }}
@enderror +
+
+ + {{-- ── Schritt 3: Admin ── --}} + @elseif($step === 3) +
+
Admin-Account
+
Dieser Account hat vollen Zugriff auf das Control Panel
+
+ +
+
+ + + @error('admin_name')
{{ $message }}
@enderror +
+
+ + + @error('admin_email')
{{ $message }}
@enderror +
+
+ + + @error('admin_password')
{{ $message }}
@enderror +
+
+ + +
+
+ + {{-- ── Schritt 4: Zusammenfassung ── --}} + @elseif($step === 4) +
+
Alles bereit
+
Überprüfe die Einstellungen und schließe die Einrichtung ab
+
+ +
+ @foreach([ + ['label' => 'Instanz', 'value' => $instance_name], + ['label' => 'Zeitzone', 'value' => $timezone], + ['label' => 'UI Domain', 'value' => $ui_domain], + ['label' => 'Mail Domain', 'value' => $mail_domain], + ['label' => 'Webmail Domain', 'value' => $webmail_domain], + ['label' => 'Admin E-Mail', 'value' => $admin_email], + ] as $row) +
+ {{ $row['label'] }} + {{ $row['value'] ?: '—' }} +
+ @endforeach +
+ @endif + +
+ + {{-- ═══ Navigation ═══ --}} +
+ @if($step > 1) + + @endif + + @if($step < 4) + + @else + + @endif +
+
diff --git a/routes/web.php b/routes/web.php index e3c2177..e755347 100644 --- a/routes/web.php +++ b/routes/web.php @@ -109,6 +109,6 @@ Route::middleware('guest.only')->group(function () { }); Route::middleware('auth')->group(function () { - Route::get('/setup', [SetupWizard::class, 'show'])->name('setup'); + Route::get('/setup', \App\Livewire\Setup\Wizard::class)->name('setup'); });