mailwolt/app/Livewire/Setup/Wizard.php

239 lines
8.7 KiB
PHP

<?php
namespace App\Livewire\Setup;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
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 = 5;
// Schritt 1 — System
public string $instance_name = 'Mailwolt';
public string $locale = 'de';
public string $timezone = 'Europe/Berlin';
// Schritt 2 — Domains
public string $ui_domain = '';
public string $mail_domain = '';
public string $webmail_domain = '';
// Schritt 4 — Option
public bool $skipSsl = false;
// Schritt 3 — Admin-Account
public string $admin_name = '';
public string $admin_email = '';
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');
try {
$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', '');
} catch (\Throwable) {
// DB noch nicht migriert — Standardwerte bleiben
}
}
public function updatedUiDomain(): void { $this->fillEmptyDomains($this->ui_domain); }
public function updatedMailDomain(): void { $this->fillEmptyDomains($this->mail_domain); }
public function updatedWebmailDomain(): void { $this->fillEmptyDomains($this->webmail_domain); }
private function fillEmptyDomains(string $value): void
{
if ($value === '') return;
if ($this->ui_domain === '') $this->ui_domain = $value;
if ($this->mail_domain === '') $this->mail_domain = $value;
if ($this->webmail_domain === '') $this->webmail_domain = $value;
}
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(): void
{
$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 + .env 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',
]);
$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,
]);
// Admin anlegen
$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();
// 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 = (!$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);
$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 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);
}
private function writeEnv(array $values): void
{
$path = base_path('.env');
$content = @file_get_contents($path) ?: '';
foreach ($values as $key => $value) {
$escaped = str_contains($value, ' ') ? '"' . $value . '"' : $value;
$line = $key . '=' . $escaped;
$pattern = '/^' . preg_quote($key, '/') . '=[^\r\n]*/m';
if (preg_match($pattern, $content)) {
$content = preg_replace($pattern, $line, $content);
} else {
$content .= "\n{$line}";
}
}
file_put_contents($path, $content);
}
public function render()
{
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
return view('livewire.setup.wizard', compact('timezones'));
}
}