'Willkommen zurück', 'eyebrow' => 'Anmeldung im Publisher-Hub', 'topRightLabel' => 'Noch kein Konto?', 'topRightLinkText' => 'Konto erstellen', 'topRightLinkHref' => '/register'])] class extends Component { #[Validate('required|string|email')] public string $email = ''; #[Validate('required|string')] public string $password = ''; public bool $remember = false; // Eigene Eingabe für das Magic-Link-Modal, getrennt vom Login-Formular. public string $magicEmail = ''; /** * Handle an incoming authentication request. */ public function login(): void { $this->validate(); $this->ensureIsNotRateLimited(); if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { RateLimiter::hit($this->throttleKey()); throw ValidationException::withMessages([ 'email' => __('auth.failed'), ]); } $authenticatedUser = Auth::user(); // Unverifizierte Selbst-Registrierer (Konto angelegt, Mail noch nicht // bestätigt) gehören auf die Notice-Seite, nicht in die Panel-Logik. if ($authenticatedUser && ! $authenticatedUser->hasVerifiedEmail()) { RateLimiter::clear($this->throttleKey()); Session::regenerate(); $this->redirect(route('verification.notice', absolute: false)); return; } // Verifiziert, aber deaktiviert: Login zentral blockieren (analog zum // Magic-Link-Consume), damit auch nur-auth/verified-Routen sicher sind. if ($authenticatedUser && ! $authenticatedUser->is_active) { Auth::logout(); throw ValidationException::withMessages([ 'email' => __('Ihr Konto ist nicht aktiv. Bitte wenden Sie sich an den Support.'), ]); } $authenticatedUser?->update([ 'last_login_at' => now(), 'last_login_ip' => request()->ip(), ]); RateLimiter::clear($this->throttleKey()); Session::regenerate(); // Rollen-basierter Default-Redirect: // Admin/Editor → /dashboard, Customer → /admin/me. // Ohne navigate:true, weil das Portal ein anderes Vite-Bundle nutzt // (build/portal mit FluxUI) als das Hub-Auth-Layout (build/web). // SPA-Navigation kann den Bundle-Wechsel nicht handhaben. $defaultRoute = $authenticatedUser?->canAccessAdmin() ? route('dashboard', absolute: false) : ($authenticatedUser?->canAccessCustomer() ? route('me.dashboard', absolute: false) : '/'); $this->redirect($this->safeRedirectTarget($authenticatedUser, $defaultRoute)); } public function sendMagicLink(): void { $this->validate( ['magicEmail' => 'required|string|email'], attributes: ['magicEmail' => __('E-Mail-Adresse')], ); $this->ensureMagicLinkNotRateLimited(); RateLimiter::hit($this->magicLinkThrottleKey(), 3600); RateLimiter::hit($this->magicLinkIpThrottleKey(), 3600); $user = User::query()->where('email', $this->magicEmail)->first(); if ($user && $user->is_active) { $generated = app(MagicLinkGenerator::class)->createLoginLink($user, request()->ip()); $loginUrl = route('magic-links.consume', ['token' => $generated['plain_token']]); Mail::to($user->email)->send( new MagicLoginLink( user: $user, loginUrl: $loginUrl, expiresAt: $generated['expires_at']->format('d.m.Y H:i') ) ); } $this->reset('magicEmail'); $this->dispatch('magic-link-sent'); session()->flash('status', __('If an active account exists for this email, we sent a magic login link.')); } /** * Ensure the authentication request is not rate limited. */ protected function ensureIsNotRateLimited(): void { if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { return; } event(new Lockout(request())); $seconds = RateLimiter::availableIn($this->throttleKey()); throw ValidationException::withMessages([ 'email' => __('auth.throttle', [ 'seconds' => $seconds, 'minutes' => ceil($seconds / 60), ]), ]); } /** * Magic-Link-Versand drosseln: pro E-Mail+IP (gegen Mail-Fluten eines * Accounts und das laufende Entwerten alter Links) und zusätzlich pro IP * (gegen das Durchprobieren vieler Accounts von einer Quelle). */ protected function ensureMagicLinkNotRateLimited(): void { $withinEmail = ! RateLimiter::tooManyAttempts($this->magicLinkThrottleKey(), 3); $withinIp = ! RateLimiter::tooManyAttempts($this->magicLinkIpThrottleKey(), 15); if ($withinEmail && $withinIp) { return; } $seconds = max( RateLimiter::availableIn($this->magicLinkThrottleKey()), RateLimiter::availableIn($this->magicLinkIpThrottleKey()), ); throw ValidationException::withMessages([ 'magicEmail' => __('Zu viele Anfragen. Bitte versuchen Sie es in :minutes Minuten erneut.', [ 'minutes' => max(1, ceil($seconds / 60)), ]), ]); } /** * Übernimmt die intended-URL nur, wenn der User sie auch erreichen darf – * sonst Default. Verhindert, dass ein Customer mit intended=/admin/users * nach dem Login in der 403-Sackgasse landet. */ protected function safeRedirectTarget(?User $user, string $default): string { // Wie Laravels intended(): die gemerkte URL aus der Session ziehen und // entfernen. (In Livewire ist redirect() der Livewire-Redirector ohne // getTargetUrl(), daher direkt über die Session.) $intended = (string) session()->pull('url.intended', $default); $path = parse_url($intended, PHP_URL_PATH) ?: '/'; if ($user && ! $user->canAccessAdmin() && $this->isAdminOnlyPath($path)) { return $default; } return $intended; } /** * Reine Admin-Pfade: alles unter /admin außer dem Kundenbereich /admin/me * sowie das Admin-Dashboard /dashboard. */ protected function isAdminOnlyPath(string $path): bool { if ($path === '/dashboard' || str_starts_with($path, '/dashboard/')) { return true; } return str_starts_with($path, '/admin') && ! str_starts_with($path, '/admin/me'); } /** * Get the authentication rate limiting throttle key. */ protected function throttleKey(): string { return Str::transliterate(Str::lower($this->email).'|'.request()->ip()); } protected function magicLinkThrottleKey(): string { return 'magic-link|'.Str::transliterate(Str::lower($this->magicEmail).'|'.request()->ip()); } protected function magicLinkIpThrottleKey(): string { return 'magic-link-ip|'.request()->ip(); } }; ?>