Feature: Setup-Wizard neu — 4-Schritte-Einrichtung mit Dark-Design

- Wizard komplett überarbeitet: System / Domains / Admin / Zusammenfassung
- Eigenes Layout (layouts/setup.blade.php), zentriert, kein Sidebar
- Schritt-Indikator mit Checkmarks für abgeschlossene Schritte
- Per-Schritt Validierung, live Fehleranzeige
- Weiter/Zurück-Buttons mit korrekter Ausrichtung (margin-left:auto)
- Livewire wire:loading-Spinner auf SVG-Icons (behebt JS-Fehler in core.js)
- finish() schreibt Settings, .env und legt Admin-User an

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main v1.1.139
boban 2026-04-23 01:49:13 +02:00
parent 2ae126cf20
commit 2049057f7f
4 changed files with 308 additions and 204 deletions

View File

@ -2,212 +2,148 @@
namespace App\Livewire\Setup; namespace App\Livewire\Setup;
use App\Jobs\ProvisionCertJob; use App\Models\Setting;
use App\Support\Setting;
use App\Models\SystemTask;
use App\Models\User; use App\Models\User;
use App\Support\EnvWriter;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redis; use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
#[Layout('layouts.setup')]
#[Title('Einrichtung · Mailwolt')]
class Wizard extends Component class Wizard extends Component
{ {
public int $step = 1; public int $step = 1;
public int $totalSteps = 4;
// Step 1 // Schritt 1 — System
#[Validate('required|string|min:3')] public string $instance_name = 'Mailwolt';
public string $form_domain = ''; public string $locale = 'de';
public string $timezone = 'Europe/Berlin';
#[Validate('required|timezone')] // Schritt 2 — Domains
public string $form_timezone = 'UTC'; 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 public function mount(): void
#[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()
{ {
if ($this->step === 1) { $this->instance_name = config('app.name', 'Mailwolt');
$this->validateOnly('form_domain'); $this->timezone = Setting::get('timezone', 'Europe/Berlin');
$this->validateOnly('form_timezone'); $this->locale = Setting::get('locale', 'de');
} elseif ($this->step === 2) { $this->ui_domain = Setting::get('ui_domain', '');
$this->validate([ $this->mail_domain = Setting::get('mail_domain', '');
'form_admin_name' => 'required|string|min:3', $this->webmail_domain = Setting::get('webmail_domain', '');
'form_admin_email' => 'required|email',
'form_admin_password' => 'required|string|min:8|same:form_admin_password_confirmation',
]);
} }
$this->step = min($this->step + 1, 3); 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 prevStep() public function back(): void
{ {
$this->step = max($this->step - 1, 1); $this->step = max($this->step - 1, 1);
} }
public function finish() public function finish(): mixed
{ {
// Step 3 Validierung (nur wenn sofort erstellen) // Schritt-3-Validierung nochmals sicherstellen
if ($this->form_cert_create_now) { $this->validate([
$this->validateOnly('form_cert_email'); 'admin_name' => 'required|string|min:2|max:64',
} 'admin_email' => 'required|email|max:190',
'admin_password' => 'required|string|min:10|same:admin_password_confirmation',
// 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,
]); ]);
// 2) Admin anlegen/aktualisieren // Settings speichern
$user = User::query() Setting::setMany([
->where('email', $this->form_admin_email) 'locale' => $this->locale,
->when($this->form_admin_username, fn($q) => $q->orWhere('username', $this->form_admin_username) 'timezone' => $this->timezone,
) 'ui_domain' => $this->ui_domain,
->first(); 'mail_domain' => $this->mail_domain,
'webmail_domain' => $this->webmail_domain,
'setup_completed' => '1',
]);
if (!$user) { // .env aktualisieren
$user = new User(); $this->writeEnv([
$user->email = $this->form_admin_email; 'APP_NAME' => $this->instance_name,
if ($this->form_admin_username) { 'APP_HOST' => $this->ui_domain,
$user->username = $this->form_admin_username; 'APP_URL' => 'https://' . $this->ui_domain,
} 'MTA_SUB' => explode('.', $this->mail_domain)[0] ?? '',
} else { 'WEBMAIL_DOMAIN' => $this->webmail_domain,
// vorhandene Email/Username harmonisieren ]);
$user->email = $this->form_admin_email;
if ($this->form_admin_username) {
$user->username = $this->form_admin_username;
}
}
$user->name = $this->form_admin_name; // Admin anlegen oder aktualisieren
$user->is_admin = true; $user = User::where('email', $this->admin_email)->first() ?? new User();
$user->password = Hash::make($this->form_admin_password); $user->name = $this->admin_name;
$user->required_change_password = true; $user->email = $this->admin_email;
$user->password = Hash::make($this->admin_password);
$user->role = 'admin';
$user->save(); $user->save();
// 3) Zertifikat jetzt ausstellen (optional) return redirect()->route('login')->with('setup_done', true);
$taskKey = 'issue-cert:' . $this->form_domain;
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'
],
]
);
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: $this->form_cert_email,
taskKey: $taskKey,
useLetsEncrypt: true
);
session()->flash('task_key', $taskKey);
session()->flash('banner_ok', 'Lets 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…');
} }
return redirect()->route('dashboard'); 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() public function render()
{ {
return view('livewire.setup.wizard'); $timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
return view('livewire.setup.wizard', compact('timezones'));
} }
} }

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Einrichtung · Mailwolt</title>
@vite(['resources/css/app.css'])
@livewireStyles
</head>
<body style="margin:0;background:var(--mw-bg);color:var(--mw-t1);font-family:-apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;font-size:13.5px;-webkit-font-smoothing:antialiased;min-height:100vh;display:flex;align-items:center;justify-content:center;
background-image:radial-gradient(900px 600px at 15% 0%,rgba(99,102,241,.07),transparent),radial-gradient(700px 500px at 85% 100%,rgba(59,130,246,.05),transparent)">
{{ $slot }}
@livewireScripts
@stack('scripts')
</body>
</html>

View File

@ -1,34 +1,184 @@
<div class="grid items-center"> <div style="width:100%;max-width:520px;padding:24px 16px">
<livewire:ping-button />
<div class="glass-card w-full max-w-2xl mx-auto p-8 space-y-8"> {{-- ═══ Logo ═══ --}}
<div wire:key="wizard-step-{{ $step }}"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:32px;justify-content:center">
@if ($step === 1) <div style="width:32px;height:32px;background:linear-gradient(135deg,#6366f1,#4f46e5);border-radius:8px;display:flex;align-items:center;justify-content:center;box-shadow:0 0 16px rgba(99,102,241,.4)">
@include('livewire.setup.step-domain') <svg width="16" height="16" viewBox="0 0 18 18" fill="none">
@elseif ($step === 2) <path d="M9 2L15.5 5.5v7L9 16 2.5 12.5v-7L9 2Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
@include('livewire.setup.step-admin') <path d="M9 6L12 7.5v3L9 12 6 10.5v-3L9 6Z" fill="white" opacity=".9"/>
@elseif ($step === 3) </svg>
@include('livewire.setup.step-certificate') </div>
@endif <div>
<div style="font-size:14px;font-weight:700;letter-spacing:.3px;color:var(--mw-t1)">MAILWOLT</div>
<div style="font-size:11px;color:var(--mw-t4);margin-top:-1px">Ersteinrichtung</div>
</div>
</div> </div>
<div class="flex items-center justify-between pt-2"> {{-- ═══ Schritt-Indikator ═══ --}}
@if ($step > 1) <div style="display:flex;align-items:center;gap:0;margin-bottom:28px">
<button type="button" wire:click="prevStep" class="btn-primary/ghost px-4 py-2"> @foreach([1 => 'System', 2 => 'Domains', 3 => 'Admin', 4 => 'Fertig'] as $n => $label)
Zurück <div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:5px">
</button> <div style="width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:600;transition:all .2s;
{{ $step > $n
? 'background:rgba(99,102,241,.25);border:1.5px solid #6366f1;color:#a5b4fc'
: ($step === $n
? 'background:rgba(99,102,241,.15);border:1.5px solid #6366f1;color:#c7d2fe'
: 'background:transparent;border:1.5px solid var(--mw-b2);color:var(--mw-t5)') }}">
@if($step > $n)
<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>
@else @else
<span></span> {{ $n }}
@endif
</div>
<span style="font-size:10.5px;color:{{ $step === $n ? 'var(--mw-t3)' : 'var(--mw-t5)' }}">{{ $label }}</span>
</div>
@if($n < 4)
<div style="flex:1;height:1px;background:{{ $step > $n ? 'rgba(99,102,241,.4)' : 'var(--mw-b2)' }};margin-bottom:18px;max-width:40px"></div>
@endif
@endforeach
</div>
{{-- ═══ Karte ═══ --}}
<div style="background:var(--mw-bg2);border:1px solid var(--mw-b2);border-radius:14px;padding:28px 24px">
{{-- ── Schritt 1: System ── --}}
@if($step === 1)
<div style="margin-bottom:20px">
<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>
<input type="text" wire:model="instance_name" class="mw-modal-input" placeholder="Mailwolt">
@error('instance_name') <div class="mw-modal-error">{{ $message }}</div> @enderror
</div>
<div>
<label class="mw-modal-label">Sprache</label>
<select wire:model="locale" class="mw-modal-input">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</div>
<div>
<label class="mw-modal-label">Zeitzone</label>
<select wire:model="timezone" class="mw-modal-input">
@foreach($timezones as $tz)
<option value="{{ $tz }}" @selected($timezone === $tz)>{{ $tz }}</option>
@endforeach
</select>
</div>
</div>
{{-- ── Schritt 2: Domains ── --}}
@elseif($step === 2)
<div style="margin-bottom:20px">
<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>
<input type="text" wire:model="ui_domain" class="mw-modal-input" placeholder="mail.example.com">
@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>
<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>
<div>
<label class="mw-modal-label">Webmail Domain</label>
<input type="text" wire:model="webmail_domain" class="mw-modal-input" placeholder="webmail.example.com">
@error('webmail_domain') <div class="mw-modal-error">{{ $message }}</div> @enderror
</div>
</div>
{{-- ── Schritt 3: Admin ── --}}
@elseif($step === 3)
<div style="margin-bottom:20px">
<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>
<input type="text" wire:model="admin_name" class="mw-modal-input" placeholder="Max Mustermann">
@error('admin_name') <div class="mw-modal-error">{{ $message }}</div> @enderror
</div>
<div>
<label class="mw-modal-label">E-Mail</label>
<input type="email" wire:model="admin_email" class="mw-modal-input" placeholder="admin@example.com">
@error('admin_email') <div class="mw-modal-error">{{ $message }}</div> @enderror
</div>
<div>
<label class="mw-modal-label">Passwort <span style="color:var(--mw-t4);font-weight:400">(min. 10 Zeichen)</span></label>
<input type="password" wire:model="admin_password" class="mw-modal-input">
@error('admin_password') <div class="mw-modal-error">{{ $message }}</div> @enderror
</div>
<div>
<label class="mw-modal-label">Passwort bestätigen</label>
<input type="password" wire:model="admin_password_confirmation" class="mw-modal-input">
</div>
</div>
{{-- ── Schritt 4: Zusammenfassung ── --}}
@elseif($step === 4)
<div style="margin-bottom:20px">
<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],
['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)
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--mw-bg3);border-radius:7px;border:1px solid var(--mw-b2)">
<span style="font-size:12px;color:var(--mw-t4)">{{ $row['label'] }}</span>
<span style="font-size:12px;color:var(--mw-t2);font-family:{{ in_array($row['label'], ['UI Domain','Mail Domain','Webmail Domain']) ? 'monospace' : 'inherit' }}">{{ $row['value'] ?: '—' }}</span>
</div>
@endforeach
</div>
@endif @endif
@if ($step < 3) </div>
<button type="button" wire:click="nextStep" class="btn-primary px-6 py-2">
{{-- ═══ Navigation ═══ --}}
<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">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Zurück
</button>
@endif
@if($step < 4)
<button wire:click="next" wire:loading.attr="disabled" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content;margin-left:auto">
<svg wire:loading.remove wire:target="next" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg wire:loading wire:target="next" width="12" height="12" viewBox="0 0 12 12" fill="none" style="animation:spin .7s linear infinite"><path d="M10 6A4 4 0 1 1 6 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Weiter Weiter
</button> </button>
@else @else
<button type="button" wire:click="finish" class="btn-primary px-6 py-2"> <button wire:click="finish" wire:loading.attr="disabled" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content;margin-left:auto;background:linear-gradient(135deg,rgba(99,102,241,.25),rgba(79,70,229,.2));border-color:rgba(99,102,241,.5)">
Fertig <svg wire:loading.remove wire:target="finish" 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>
<svg wire:loading wire:target="finish" width="12" height="12" viewBox="0 0 12 12" fill="none" style="animation:spin .7s linear infinite"><path d="M10 6A4 4 0 1 1 6 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Einrichtung abschließen
</button> </button>
@endif @endif
</div> </div>
</div>
</div> </div>

View File

@ -109,6 +109,6 @@ Route::middleware('guest.only')->group(function () {
}); });
Route::middleware('auth')->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');
}); });