Rechtebechebung für User mit Sudorechte

main
boban 2025-10-19 22:38:46 +02:00
parent 28129cb989
commit f8934b7a9a
9 changed files with 203 additions and 120 deletions

View File

@ -3,16 +3,33 @@
namespace App\Livewire\Auth; namespace App\Livewire\Auth;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class LoginForm extends Component class LoginForm extends Component
{ {
public ?array $banner = [];
public bool $showBanner = false;
public string $name = ''; public string $name = '';
public string $password = ''; public string $password = '';
public ?string $error = null; public ?string $error = null;
public bool $show = false; public bool $show = false;
public function mount(): void
{
// Flash nur EINMAL ziehen
$flash = session()->pull('login_banner');
if ($flash) {
$this->banner = $flash;
$this->showBanner = true;
}
}
public function dismissBanner(): void
{
$this->showBanner = false;
}
public function login() public function login()
{ {
@ -28,7 +45,7 @@ class LoginForm extends Component
if (Auth::attempt([$field => $this->name, 'password' => $this->password], true)) { if (Auth::attempt([$field => $this->name, 'password' => $this->password], true)) {
request()->session()->regenerate(); request()->session()->regenerate();
return redirect()->intended(route('setup.wizard')) ; return redirect()->intended(route('ui.dashboard'));
} }
$this->error = 'Ungültige Zugangsdaten.'; $this->error = 'Ungültige Zugangsdaten.';

View File

@ -58,6 +58,13 @@ class SignupForm extends Component
// nach erstem User: Signup deaktivieren // nach erstem User: Signup deaktivieren
if ($isFirstUser) { if ($isFirstUser) {
Setting::set('system.signup_enabled', 0); // Redis + DB Setting::set('system.signup_enabled', 0); // Redis + DB
return redirect()
->route('login')
->with('login_banner', [
'type' => 'success',
'title' => 'Registrierung abgeschlossen',
'message' => 'Dein Konto wurde erfolgreich erstellt. Du kannst dich jetzt anmelden.',
]);
} }
}); });
}); });

View File

@ -20,6 +20,8 @@ class DomainDnsModal extends ModalComponent
public array $static = []; public array $static = [];
/** @var array<int,array<string,string|int|null>> */ /** @var array<int,array<string,string|int|null>> */
public array $dynamic = []; public array $dynamic = [];
/** @var array<int,array<string,string|int|null>> */
public array $optional = [];
public static function modalMaxWidth(): string public static function modalMaxWidth(): string
{ {
@ -37,6 +39,7 @@ class DomainDnsModal extends ModalComponent
'TXT' => 'bg-violet-500/20 text-violet-300', 'TXT' => 'bg-violet-500/20 text-violet-300',
'SRV' => 'bg-rose-500/20 text-rose-300', 'SRV' => 'bg-rose-500/20 text-rose-300',
'TLSA' => 'bg-red-500/20 text-red-300', 'TLSA' => 'bg-red-500/20 text-red-300',
'OPTIONAL' => 'bg-gray-500/20 text-gray-300',
]; ];
$d = Domain::findOrFail($domainId); $d = Domain::findOrFail($domainId);
@ -59,7 +62,6 @@ class DomainDnsModal extends ModalComponent
// --- Statische Infrastruktur (für alle Domains gleich) --- // --- Statische Infrastruktur (für alle Domains gleich) ---
$this->static = [ $this->static = [
['type' => 'MX', 'name' => $base, 'value' => "10 {$mailServerFqdn}."],
['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4], ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4],
['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $tlsa->host], ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $tlsa->host],
]; ];
@ -78,7 +80,7 @@ class DomainDnsModal extends ModalComponent
} }
// --- Domain-spezifisch --- // --- Domain-spezifisch ---
$spf = "v=spf1 a mx ip4:{$ipv4} ip6:{$ipv6} ~all"; $spf = "v=spf1 ip4:{$ipv4} ip6:{$ipv6} mx -all";
$dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100"; $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
$dkim = DB::table('dkim_keys') $dkim = DB::table('dkim_keys')
@ -90,19 +92,24 @@ class DomainDnsModal extends ModalComponent
: ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p='); : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p=');
$this->dynamic = [ $this->dynamic = [
['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."],
['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."], ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."],
['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."], ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."],
// SRV Records für Autodiscover und Maildienste
['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."],
// TXT Records // TXT Records
['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'], ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'],
['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc, 'helpLabel' => 'DMARC Assistant', 'helpUrl' => 'https://www.kitterman.com/dmarc/assistant.html'], ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc, 'helpLabel' => 'DMARC Assistant', 'helpUrl' => 'https://www.kitterman.com/dmarc/assistant.html'],
['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt, 'helpLabel' => 'DKIM Inspector', 'helpUrl' => 'https://dkimvalidator.com/'], ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt, 'helpLabel' => 'DKIM Inspector', 'helpUrl' => 'https://dkimvalidator.com/'],
];
$this->optional = [
// SRV Records für Autodiscover und Maildienste
['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."],
]; ];
} }

View File

@ -2,11 +2,8 @@
namespace App\Observers; namespace App\Observers;
use App\Jobs\InstallDkimKey;
use App\Jobs\RemoveDkimKey;
use App\Models\DkimKey;
use App\Models\Domain; use App\Models\Domain;
use App\Services\DkimService; use Illuminate\Support\Facades\Log;
class DomainObserver class DomainObserver
{ {
@ -16,76 +13,54 @@ class DomainObserver
*/ */
public function created(Domain $domain): void public function created(Domain $domain): void
{ {
if ($domain->is_server) { if ($domain->is_server) return;
return;
}
$selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1'); $selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1');
$bits = (int) config('mailpool.defaults.dkim_bits', 2048); $bits = (int) config('mailpool.defaults.dkim_bits', 2048);
$res = app(\App\Services\DkimService::class) // Service erledigt: Key generieren, DB (upsert) pflegen, Helper ausführen, OpenDKIM reloaden
->generateForDomain($domain, $bits, $selector); app(\App\Services\DkimService::class)->generateForDomain($domain, $bits, $selector);
$dk = \App\Models\DkimKey::create([ // DNS-Records: aktiven Key aus DB lesen und provisionieren
'domain_id' => $domain->id, $active = $domain->dkimKeys()->where('is_active', true)->latest()->first();
'selector' => $res['selector'], if ($active) {
'private_key_pem' => $res['private_pem'], app(\App\Services\DnsRecordService::class)->provision(
'public_key_txt' => preg_replace('/^v=DKIM1; k=rsa; p=/', '', $res['dns_txt']), $domain,
'is_active' => true, $active->selector,
]); "v=DKIM1; k=rsa; p={$active->public_key_txt}",
[
// Helper aufrufen (Pfad aus $res['priv_path']!) 'spf_tail' => \App\Models\Setting::get('mailpool.spf_tail', '~all'),
dispatch(new \App\Jobs\InstallDkimKey( 'spf_extra' => \App\Models\Setting::get('mailpool.spf_extra', []),
domainId: $domain->id, 'dmarc_policy' => \App\Models\Setting::get('mailpool.dmarc_policy', 'none'),
dkimKeyId: $dk->id, 'rua' => \App\Models\Setting::get('mailpool.rua', null),
privPath: $res['priv_path'], ]
dnsTxtContent: $res['dns_txt'], );
)); }
// DNS-Records gleich anlegen/aktualisieren
app(\App\Services\DnsRecordService::class)->provision(
$domain,
$dk->selector,
"v=DKIM1; k=rsa; p={$dk->public_key_txt}",
[
'spf_tail' => \App\Models\Setting::get('mailpool.spf_tail', '~all'),
'spf_extra' => \App\Models\Setting::get('mailpool.spf_extra', []),
'dmarc_policy' => \App\Models\Setting::get('mailpool.dmarc_policy', 'none'),
'rua' => \App\Models\Setting::get('mailpool.rua', null),
]
);
} }
// public function created(Domain $domain): void // public function created(Domain $domain): void
// { // {
// // Standardwerte aus Config oder .env // if ($domain->is_server) {
// $selector = config('mailwolt.dkim.selector', 'mwl1'); // return;
// $bits = (int) config('mailwolt.dkim.bits', 2048); // }
// //
// // Keypair erzeugen // $selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1');
// $res = app(DkimService::class)->generateForDomain( // $bits = (int) config('mailpool.defaults.dkim_bits', 2048);
// domainId: $domain, //
// bits: $bits, // $res = app(\App\Services\DkimService::class)
// selector: $selector // ->generateForDomain($domain, $bits, $selector);
//
// // DNS-Records gleich anlegen/aktualisieren
// app(\App\Services\DnsRecordService::class)->provision(
// $domain,
// $dk->selector,
// "v=DKIM1; k=rsa; p={$dk->public_key_txt}",
// [
// 'spf_tail' => \App\Models\Setting::get('mailpool.spf_tail', '~all'),
// 'spf_extra' => \App\Models\Setting::get('mailpool.spf_extra', []),
// 'dmarc_policy' => \App\Models\Setting::get('mailpool.dmarc_policy', 'none'),
// 'rua' => \App\Models\Setting::get('mailpool.rua', null),
// ]
// ); // );
//
// // In dkim_keys speichern
// $dk = DkimKey::create([
// 'domain_id' => $domain->id,
// 'selector' => $res['selector'],
// 'private_key_pem' => $res['private_pem'],
// 'public_key_txt' => preg_replace('/^v=DKIM1; k=rsa; p=/', '', $res['dns_txt']),
// 'is_active' => true,
// ]);
//
// // Helper-Job zum Installieren starten
// InstallDkimKey::dispatch(
// domainId: $domain->id,
// dkimKeyId: $dk->id,
// privPath: $res['priv_path'],
// dnsTxtContent: $res['dns_txt']
// )->afterCommit();
// } // }
/** /**
@ -93,12 +68,28 @@ class DomainObserver
*/ */
public function deleted(Domain $domain): void public function deleted(Domain $domain): void
{ {
// Falls SoftDeletes im Spiel, willst du evtl. forceDeleted spiegeln (s.u.) try {
foreach ($domain->dkimKeys()->get() as $dk) { /** @var \App\Services\DkimService $svc */
RemoveDkimKey::dispatch( $svc = app(\App\Services\DkimService::class);
domainId: $domain->id,
selector: $dk->selector // Entferne DKIM aus OpenDKIM Config
)->afterCommit(); $svc->removeForDomain($domain);
// Optionale lokale Dateien löschen
$path = storage_path("app/private/dkim/{$domain->domain}");
if (is_dir($path)) {
\Illuminate\Support\Facades\File::deleteDirectory($path);
}
// Reload OpenDKIM
\Illuminate\Support\Facades\Process::run(['sudo','-n','/usr/bin/systemctl','reload','opendkim']);
Log::info("Domain deleted + DKIM cleaned", ['domain' => $domain->domain]);
} catch (\Throwable $e) {
Log::error("Domain delete cleanup failed", [
'domain' => $domain->domain,
'error' => $e->getMessage(),
]);
} }
} }

View File

@ -22,7 +22,7 @@ class DkimService
$dirKey = $this->dirKeyFor($domain); $dirKey = $this->dirKeyFor($domain);
$selKey = preg_replace('/[^A-Za-z0-9._-]/', '_', substr($selector, 0, 32)); // schlicht & stabil $selKey = preg_replace('/[^A-Za-z0-9._-]/', '_', substr($selector, 0, 32)); // schlicht & stabil
// Disk "local" zeigt bei dir auf storage/app/private (siehe Kommentar in deinem Code) // Disk "local" zeigt bei dir auf storage/app/private (siehe Kommentar in deinem Code)
$disk = Storage::disk('local'); $disk = Storage::disk('local');
$baseRel = "dkim/{$dirKey}"; $baseRel = "dkim/{$dirKey}";
@ -159,36 +159,58 @@ class DkimService
return $san; return $san;
} }
protected function safeKey($value, int $max = 64): string public function removeForDomain(Domain|string $domain): void
{ {
if (is_object($value)) { $domainName = $domain instanceof Domain ? $domain->domain : $domain;
if (isset($value->id)) $value = $value->id; $keyTable = '/etc/opendkim/KeyTable';
elseif (method_exists($value, 'getKey')) $value = $value->getKey(); $signTable = '/etc/opendkim/SigningTable';
else $value = json_encode($value); $keyDir = "/etc/opendkim/keys/{$domainName}";
// Tabellen bereinigen
foreach ([$keyTable, $signTable] as $file) {
if (is_file($file)) {
$lines = file($file, FILE_IGNORE_NEW_LINES);
$filtered = array_filter($lines, fn($l) => !str_contains($l, $domainName));
file_put_contents($file, implode(PHP_EOL, $filtered) . PHP_EOL);
}
} }
$raw = (string) $value;
$san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw); // Key-Verzeichnis löschen
if ($san === '' ) $san = 'unknown'; if (is_dir($keyDir)) {
if (strlen($san) > $max) { \Illuminate\Support\Facades\File::deleteDirectory($keyDir);
$san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12);
} }
return $san;
} }
protected static function extractPublicKeyBase64(string $pem): string // protected function safeKey($value, int $max = 64): string
{ // {
// Hole den Body zwischen den Headern (multiline, dotall) // if (is_object($value)) {
if (!preg_match('/^-+BEGIN PUBLIC KEY-+\r?\n(.+?)\r?\n-+END PUBLIC KEY-+\s*$/ms', $pem, $m)) { // if (isset($value->id)) $value = $value->id;
throw new \RuntimeException('DKIM: Ungültiges Public-Key-PEM (Header/Footers nicht gefunden).'); // elseif (method_exists($value, 'getKey')) $value = $value->getKey();
} // else $value = json_encode($value);
// }
// Whitespace entfernen → reines Base64 // $raw = (string) $value;
$b64 = preg_replace('/\s+/', '', $m[1]); // $san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw);
// if ($san === '' ) $san = 'unknown';
if ($b64 === '' || base64_decode($b64, true) === false) { // if (strlen($san) > $max) {
throw new \RuntimeException('DKIM: Public Key Base64 ist leer/ungültig.'); // $san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12);
} // }
// return $san;
return $b64; // }
} //
// protected static function extractPublicKeyBase64(string $pem): string
// {
// // Hole den Body zwischen den Headern (multiline, dotall)
// if (!preg_match('/^-+BEGIN PUBLIC KEY-+\r?\n(.+?)\r?\n-+END PUBLIC KEY-+\s*$/ms', $pem, $m)) {
// throw new \RuntimeException('DKIM: Ungültiges Public-Key-PEM (Header/Footers nicht gefunden).');
// }
//
// // Whitespace entfernen → reines Base64
// $b64 = preg_replace('/\s+/', '', $m[1]);
//
// if ($b64 === '' || base64_decode($b64, true) === false) {
// throw new \RuntimeException('DKIM: Public Key Base64 ist leer/ungültig.');
// }
//
// return $b64;
// }
} }

View File

@ -1,17 +1,32 @@
<div class="w-full #min-h-[86vh] grid place-items-center px-4 <div class="w-full #min-h-[86vh] grid place-items-center
bg-[radial-gradient(1200px_600px_at_10%_-10%,rgba(59,130,246,.08),transparent), bg-[radial-gradient(1200px_600px_at_10%_-10%,rgba(59,130,246,.08),transparent),
radial-gradient(900px_500px_at_90%_0%,rgba(99,102,241,.06),transparent)]"> radial-gradient(900px_500px_at_90%_0%,rgba(99,102,241,.06),transparent)]">
{{-- Banner (dismissbar) --}}
@if($showBanner && $banner)
<div class="max-w-[520px] mb-5 rounded-2xl border border-emerald-400/30 text-emerald-300 bg-emerald-500/10 p-4 #md:p-5 shadow backdrop-blur"
role="status" aria-live="polite">
<div class="flex items-start gap-3">
<div class="shrink-0 mt-0.5">
<i class="ph ph-check-circle text-emerald-100 text-xl"></i>
</div>
<div class="flex-1">
<div class="font-semibold text-emerald-100">
{{ $banner['title'] ?? 'Erfolgreich registriert' }}
</div>
<div class="mt-0.5 text-sm text-emerald-200">
{{ $banner['message'] ?? 'Dein Konto ist bereit. Melde dich jetzt an.' }}
</div>
</div>
</div>
</div>
@endif
<div class="nx-card w-full max-w-[520px]"> <div class="nx-card w-full max-w-[520px]">
<div class="flex items-center justify-between mb-5"> <div class="flex items-center justify-between mb-5">
<span class="nx-chip">Erster Login</span> <span class="nx-chip">Erster Login</span>
<i class="ph ph-lock-simple text-white/60"></i> <i class="ph ph-lock-simple text-white/60"></i>
</div> </div>
<p class="nx-subtle mb-7">
Melde dich mit dem einmaligen Bootstrap-Konto an, um den Setup-Wizard zu starten.
</p>
{{-- globale Fehlermeldung --}} {{-- globale Fehlermeldung --}}
@if($error) @if($error)
<div class="nx-alert mb-6"> <div class="nx-alert mb-6">

View File

@ -27,7 +27,7 @@
@if($systemDomain) @if($systemDomain)
<div class="rounded-xl border border-white/10 bg-white/[0.04] px-3 py-3"> <div class="rounded-xl border border-white/10 bg-white/[0.04] px-3 py-3">
<div class="grid grid-cols-1 #md:grid-cols-4 items-center gap-3"> <div class="grid grid-cols-1 md:grid-cols-4 items-center gap-3">
<div class=" flex items-center gap-1 text-white/90 font-medium truncate"> <div class=" flex items-center gap-1 text-white/90 font-medium truncate">
<span class="relative flex size-2.5 mx-1"> <span class="relative flex size-2.5 mx-1">
@if($systemDomain->is_active) @if($systemDomain->is_active)

View File

@ -34,7 +34,7 @@
<div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60"> <div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">
<div class="flex items-center justify-between px-4 py-2 text-[12px]"> <div class="flex items-center justify-between px-4 py-2 text-[12px]">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200 {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span> <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
<span class="text-slate-200">{{ $r['name'] }}</span> <span class="text-slate-200">{{ $r['name'] }}</span>
</div> </div>
<div class="flex items-center gap-2 text-slate-300/70"> <div class="flex items-center gap-2 text-slate-300/70">
@ -52,6 +52,30 @@
</div> </div>
</div> </div>
@endforeach @endforeach
@foreach ($optional as $r)
<div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
<span class="text-slate-200">{{ $r['name'] }}</span>
</div>
{{ $recordColors[$r['type']] }}
<div class="flex items-center gap-2 text-slate-300/70">
<x-button.copy-btn :text="$r['value']" />
</div>
</div>
<div class="px-4 pb-3">
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
@if(!empty($r['helpUrl']))
<a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">
<i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}
</a>
@endif
</div>
</div>
@endforeach
</div> </div>
</section> </section>

View File

@ -80,10 +80,10 @@ Route::get('/login', [LoginController::class, 'show'])->name('login');
Route::get('/signup', [\App\Http\Controllers\Auth\SignUpController::class, 'show' ])->middleware('signup.open')->name('signup'); Route::get('/signup', [\App\Http\Controllers\Auth\SignUpController::class, 'show' ])->middleware('signup.open')->name('signup');
Route::post('/logout', [\App\Http\Controllers\Auth\LoginController::class, 'logout'])->name('logout'); Route::post('/logout', [\App\Http\Controllers\Auth\LoginController::class, 'logout'])->name('logout');
Route::middleware(['auth', 'ensure.setup'])->group(function () { //Route::middleware(['auth', 'ensure.setup'])->group(function () {
// Route::get('/dashboard', Dashboard::class)->name('dashboard'); //// Route::get('/dashboard', Dashboard::class)->name('dashboard');
Route::get('/setup', [SetupWizard::class, 'show'])->name('setup.wizard'); // Route::get('/setup', [SetupWizard::class, 'show'])->name('setup.wizard');
}); //});