From f8934b7a9a798ba872d4fa85d6fa1fb91631bbed Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 19 Oct 2025 22:38:46 +0200 Subject: [PATCH] =?UTF-8?q?Rechtebechebung=20f=C3=BCr=20User=20mit=20Sudor?= =?UTF-8?q?echte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Livewire/Auth/LoginForm.php | 21 ++- app/Livewire/Auth/SignupForm.php | 7 + .../Ui/Domain/Modal/DomainDnsModal.php | 23 +-- app/Observers/DomainObserver.php | 131 ++++++++---------- app/Services/DkimService.php | 78 +++++++---- .../views/livewire/auth/login-form.blade.php | 27 +++- .../ui/domain/domain-dns-list.blade.php | 2 +- .../domain/modal/domain-dns-modal.blade.php | 26 +++- routes/web.php | 8 +- 9 files changed, 203 insertions(+), 120 deletions(-) diff --git a/app/Livewire/Auth/LoginForm.php b/app/Livewire/Auth/LoginForm.php index c7913ba..0041f36 100644 --- a/app/Livewire/Auth/LoginForm.php +++ b/app/Livewire/Auth/LoginForm.php @@ -3,16 +3,33 @@ namespace App\Livewire\Auth; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Component; class LoginForm extends Component { + public ?array $banner = []; + public bool $showBanner = false; + public string $name = ''; public string $password = ''; public ?string $error = null; 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() { @@ -28,7 +45,7 @@ class LoginForm extends Component if (Auth::attempt([$field => $this->name, 'password' => $this->password], true)) { request()->session()->regenerate(); - return redirect()->intended(route('setup.wizard')) ; + return redirect()->intended(route('ui.dashboard')); } $this->error = 'Ungültige Zugangsdaten.'; diff --git a/app/Livewire/Auth/SignupForm.php b/app/Livewire/Auth/SignupForm.php index fa4391a..25452f3 100644 --- a/app/Livewire/Auth/SignupForm.php +++ b/app/Livewire/Auth/SignupForm.php @@ -58,6 +58,13 @@ class SignupForm extends Component // nach erstem User: Signup deaktivieren if ($isFirstUser) { 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.', + ]); } }); }); diff --git a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php index 8fb175b..9a5c746 100644 --- a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php +++ b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php @@ -20,6 +20,8 @@ class DomainDnsModal extends ModalComponent public array $static = []; /** @var array> */ public array $dynamic = []; + /** @var array> */ + public array $optional = []; public static function modalMaxWidth(): string { @@ -37,6 +39,7 @@ class DomainDnsModal extends ModalComponent 'TXT' => 'bg-violet-500/20 text-violet-300', 'SRV' => 'bg-rose-500/20 text-rose-300', 'TLSA' => 'bg-red-500/20 text-red-300', + 'OPTIONAL' => 'bg-gray-500/20 text-gray-300', ]; $d = Domain::findOrFail($domainId); @@ -59,7 +62,6 @@ class DomainDnsModal extends ModalComponent // --- Statische Infrastruktur (für alle Domains gleich) --- $this->static = [ - ['type' => 'MX', 'name' => $base, 'value' => "10 {$mailServerFqdn}."], ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4], ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $tlsa->host], ]; @@ -78,7 +80,7 @@ class DomainDnsModal extends ModalComponent } // --- 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"; $dkim = DB::table('dkim_keys') @@ -90,19 +92,24 @@ class DomainDnsModal extends ModalComponent : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p='); $this->dynamic = [ + ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."], + ['type' => 'CNAME', 'name' => "autoconfig.$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 ['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' => $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}."], ]; } diff --git a/app/Observers/DomainObserver.php b/app/Observers/DomainObserver.php index c29af51..021ce3f 100644 --- a/app/Observers/DomainObserver.php +++ b/app/Observers/DomainObserver.php @@ -2,11 +2,8 @@ namespace App\Observers; -use App\Jobs\InstallDkimKey; -use App\Jobs\RemoveDkimKey; -use App\Models\DkimKey; use App\Models\Domain; -use App\Services\DkimService; +use Illuminate\Support\Facades\Log; class DomainObserver { @@ -16,76 +13,54 @@ class DomainObserver */ public function created(Domain $domain): void { - if ($domain->is_server) { - return; - } + if ($domain->is_server) return; $selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1'); $bits = (int) config('mailpool.defaults.dkim_bits', 2048); - $res = app(\App\Services\DkimService::class) - ->generateForDomain($domain, $bits, $selector); + // Service erledigt: Key generieren, DB (upsert) pflegen, Helper ausführen, OpenDKIM reloaden + app(\App\Services\DkimService::class)->generateForDomain($domain, $bits, $selector); - $dk = \App\Models\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 aufrufen (Pfad aus $res['priv_path']!) - dispatch(new \App\Jobs\InstallDkimKey( - domainId: $domain->id, - dkimKeyId: $dk->id, - 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), - ] - ); + // DNS-Records: aktiven Key aus DB lesen und provisionieren + $active = $domain->dkimKeys()->where('is_active', true)->latest()->first(); + if ($active) { + app(\App\Services\DnsRecordService::class)->provision( + $domain, + $active->selector, + "v=DKIM1; k=rsa; p={$active->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 // { -// // Standardwerte aus Config oder .env -// $selector = config('mailwolt.dkim.selector', 'mwl1'); -// $bits = (int) config('mailwolt.dkim.bits', 2048); +// if ($domain->is_server) { +// return; +// } // -// // Keypair erzeugen -// $res = app(DkimService::class)->generateForDomain( -// domainId: $domain, -// bits: $bits, -// selector: $selector +// $selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1'); +// $bits = (int) config('mailpool.defaults.dkim_bits', 2048); +// +// $res = app(\App\Services\DkimService::class) +// ->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 { - // Falls SoftDeletes im Spiel, willst du evtl. forceDeleted spiegeln (s.u.) - foreach ($domain->dkimKeys()->get() as $dk) { - RemoveDkimKey::dispatch( - domainId: $domain->id, - selector: $dk->selector - )->afterCommit(); + try { + /** @var \App\Services\DkimService $svc */ + $svc = app(\App\Services\DkimService::class); + + // Entferne DKIM aus OpenDKIM Config + $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(), + ]); } } diff --git a/app/Services/DkimService.php b/app/Services/DkimService.php index d63ec7a..1b81b14 100644 --- a/app/Services/DkimService.php +++ b/app/Services/DkimService.php @@ -22,7 +22,7 @@ class DkimService $dirKey = $this->dirKeyFor($domain); $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 = Storage::disk('local'); $baseRel = "dkim/{$dirKey}"; @@ -159,36 +159,58 @@ class DkimService return $san; } - protected function safeKey($value, int $max = 64): string + public function removeForDomain(Domain|string $domain): void { - if (is_object($value)) { - if (isset($value->id)) $value = $value->id; - elseif (method_exists($value, 'getKey')) $value = $value->getKey(); - else $value = json_encode($value); + $domainName = $domain instanceof Domain ? $domain->domain : $domain; + $keyTable = '/etc/opendkim/KeyTable'; + $signTable = '/etc/opendkim/SigningTable'; + $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); - if ($san === '' ) $san = 'unknown'; - if (strlen($san) > $max) { - $san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12); + + // Key-Verzeichnis löschen + if (is_dir($keyDir)) { + \Illuminate\Support\Facades\File::deleteDirectory($keyDir); } - return $san; } - 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; - } +// protected function safeKey($value, int $max = 64): string +// { +// if (is_object($value)) { +// if (isset($value->id)) $value = $value->id; +// elseif (method_exists($value, 'getKey')) $value = $value->getKey(); +// else $value = json_encode($value); +// } +// $raw = (string) $value; +// $san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw); +// if ($san === '' ) $san = 'unknown'; +// if (strlen($san) > $max) { +// $san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12); +// } +// return $san; +// } +// +// 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; +// } } diff --git a/resources/views/livewire/auth/login-form.blade.php b/resources/views/livewire/auth/login-form.blade.php index f354c00..7269453 100644 --- a/resources/views/livewire/auth/login-form.blade.php +++ b/resources/views/livewire/auth/login-form.blade.php @@ -1,17 +1,32 @@ -
+ {{-- Banner (dismissbar) --}} + @if($showBanner && $banner) +
+
+
+ +
+
+
+ {{ $banner['title'] ?? 'Erfolgreich registriert' }} +
+
+ {{ $banner['message'] ?? 'Dein Konto ist bereit. Melde dich jetzt an.' }} +
+
+
+
+ @endif +
Erster Login
- -

- Melde dich mit dem einmaligen Bootstrap-Konto an, um den Setup-Wizard zu starten. -

- {{-- globale Fehlermeldung --}} @if($error)
diff --git a/resources/views/livewire/ui/domain/domain-dns-list.blade.php b/resources/views/livewire/ui/domain/domain-dns-list.blade.php index ee5fd10..65c2f01 100644 --- a/resources/views/livewire/ui/domain/domain-dns-list.blade.php +++ b/resources/views/livewire/ui/domain/domain-dns-list.blade.php @@ -27,7 +27,7 @@ @if($systemDomain)
-
+
@if($systemDomain->is_active) diff --git a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php index d795007..f03a0fc 100644 --- a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php +++ b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php @@ -34,7 +34,7 @@
- {{ $r['type'] }} + {{ $r['type'] }} {{ $r['name'] }}
@@ -52,6 +52,30 @@
@endforeach + + @foreach ($optional as $r) +
+
+
+ {{ $r['type'] }} + {{ $r['name'] }} +
+ {{ $recordColors[$r['type']] }} +
+ +
+
+
+
{{ $r['value'] }}
+ @if(!empty($r['helpUrl'])) + + {{ $r['helpLabel'] }} + + @endif +
+
+ @endforeach
diff --git a/routes/web.php b/routes/web.php index 637fd83..6535e0b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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::post('/logout', [\App\Http\Controllers\Auth\LoginController::class, 'logout'])->name('logout'); -Route::middleware(['auth', 'ensure.setup'])->group(function () { -// Route::get('/dashboard', Dashboard::class)->name('dashboard'); - Route::get('/setup', [SetupWizard::class, 'show'])->name('setup.wizard'); -}); +//Route::middleware(['auth', 'ensure.setup'])->group(function () { +//// Route::get('/dashboard', Dashboard::class)->name('dashboard'); +// Route::get('/setup', [SetupWizard::class, 'show'])->name('setup.wizard'); +//});