email = (string) auth()->user()->email; } public function updatePassword(): void { try { $validated = $this->validate([ 'current_password' => ['required', 'string', 'current_password'], 'password' => ['required', 'string', Password::defaults(), 'confirmed'], ]); } catch (ValidationException $e) { $this->reset('current_password', 'password', 'password_confirmation'); throw $e; } Auth::user()->forceFill([ 'password' => Hash::make($validated['password']), ])->save(); $this->reset('current_password', 'password', 'password_confirmation'); session()->flash('security-status', __('Passwort aktualisiert.')); } /** * Startet eine E-Mail-Änderung: Die neue Adresse wird nur als * `pending_email` vorgemerkt und erhält einen signierten Bestätigungslink. * Die alte Adresse bleibt bis zur Bestätigung aktiv — so sperrt sich * niemand mit einer vertippten Adresse aus dem Konto aus. */ public function updateEmail(): void { $validated = $this->validate([ 'email' => [ 'required', 'email', 'max:190', Rule::unique(User::class, 'email')->ignore(auth()->id()), ], ]); /** @var User $user */ $user = auth()->user(); $this->assertSecurityProof($user); if (strcasecmp($validated['email'], $user->email) === 0) { throw ValidationException::withMessages([ 'email' => __('Das ist bereits Ihre aktuelle E-Mail-Adresse.'), ]); } $user->forceFill(['pending_email' => $validated['email']])->save(); $expiresAt = now()->addHour(); $confirmUrl = URL::temporarySignedRoute('email-change.confirm', $expiresAt, [ 'user' => $user->id, 'hash' => sha1($validated['email']), ]); Mail::to($validated['email'])->send(new EmailChangeConfirmation( $user, $validated['email'], $confirmUrl, $expiresAt->format('H:i') )); session()->flash('security-status', __('Bestätigungslink an :email gesendet. Die Änderung wird erst nach Klick auf den Link wirksam — bis dahin bleibt Ihre bisherige Adresse aktiv.', ['email' => $validated['email']])); } public function cancelPendingEmailChange(): void { auth()->user()->forceFill(['pending_email' => null])->save(); session()->flash('security-status', __('Die ausstehende E-Mail-Änderung wurde verworfen.')); } public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable): void { $enable(auth()->user()); $this->showRecoveryCodes = false; session()->flash('security-status', __('Scannen Sie den QR-Code mit Ihrer Authenticator-App und bestätigen Sie die Einrichtung mit einem aktuellen Code.')); } /** * Bestätigt die 2FA-Einrichtung mit einem TOTP-Code. Erst danach ist * `two_factor_confirmed_at` gesetzt und der Login fordert die Challenge an. */ public function confirmTwoFactorAuthentication(ConfirmTwoFactorAuthentication $confirm): void { $this->validate(['twoFactorCode' => ['required', 'string']]); try { $confirm(auth()->user(), $this->twoFactorCode); } catch (ValidationException) { throw ValidationException::withMessages([ 'twoFactorCode' => __('Der eingegebene Code ist ungültig. Bitte versuchen Sie es mit einem aktuellen Code erneut.'), ]); } $this->reset('twoFactorCode'); $this->showRecoveryCodes = true; session()->flash('security-status', __('Zwei-Faktor-Authentifizierung ist jetzt aktiv. Speichern Sie Ihre Wiederherstellungs-Codes — sie werden nur einmal angezeigt.')); } public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable): void { /** @var User $user */ $user = auth()->user(); // Eine noch unbestätigte Einrichtung abzubrechen braucht keinen // Nachweis – aktiv ist die 2FA erst nach dem bestätigten App-Code. if ($user->hasEnabledTwoFactorAuthentication()) { $this->assertSecondFactor($user); } $disable($user); $this->reset('twoFactorCode'); $this->showRecoveryCodes = false; session()->flash('security-status', __('Zwei-Faktor-Authentifizierung deaktiviert.')); } public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate): void { /** @var User $user */ $user = auth()->user(); $this->assertSecondFactor($user); $generate($user); $this->showRecoveryCodes = true; session()->flash('security-status', __('Neue Wiederherstellungs-Codes erzeugt. Die alten Codes sind ab sofort ungültig.')); } /** * Nachweis je nach Kontoart: 2FA aktiv → zweiter Faktor; Passwort gesetzt * → Passwort; reines Magic-Link-Konto ohne beides → kein weiterer Nachweis * (der Zugang selbst ist dann bereits der Postfach-Nachweis, und die neue * Adresse muss ihren Bestätigungslink ohnehin einlösen). */ private function assertSecurityProof(User $user): void { if ($user->hasEnabledTwoFactorAuthentication()) { $this->assertSecondFactor($user); return; } if ((string) $user->password === '') { return; } try { $this->validate(['securityPassword' => ['required', 'string', 'current_password']]); } finally { $this->reset('securityPassword'); } } /** * Prüft `securityCode` als TOTP (Fortify-Provider, replay-geschützt) oder * als Wiederherstellungs-Code (wird dabei verbraucht). Fünf Fehlversuche * pro Minute, damit eine Session die sechs Stellen nicht durchprobiert. */ private function assertSecondFactor(User $user): void { $this->validate(['securityCode' => ['required', 'string']]); $code = trim($this->securityCode); $this->reset('securityCode'); $throttleKey = 'security-proof:'.$user->getKey(); if (RateLimiter::tooManyAttempts($throttleKey, 5)) { throw ValidationException::withMessages([ 'securityCode' => __('Zu viele Fehlversuche. Bitte warten Sie eine Minute.'), ]); } $secret = decrypt($user->two_factor_secret); if (app(TwoFactorAuthenticationProvider::class)->verify($secret, $code)) { RateLimiter::clear($throttleKey); return; } if (in_array($code, $user->recoveryCodes() ?? [], true)) { $user->replaceRecoveryCode($code); RateLimiter::clear($throttleKey); return; } RateLimiter::hit($throttleKey, 60); throw ValidationException::withMessages([ 'securityCode' => __('Der Code ist ungültig. Geben Sie einen aktuellen Code aus Ihrer Authenticator-App oder einen Wiederherstellungs-Code ein.'), ]); } public function with(): array { /** @var User $user */ $user = auth()->user(); $user->refresh(); $twoFactorPending = ! is_null($user->two_factor_secret ?? null) && is_null($user->two_factor_confirmed_at ?? null); $twoFactorConfirmed = ! is_null($user->two_factor_confirmed_at ?? null); $qrUrl = null; $recoveryCodes = []; if (! is_null($user->two_factor_secret ?? null) && Features::enabled(Features::twoFactorAuthentication())) { try { // QR-Code nur während der Einrichtung; Recovery-Codes nur // direkt nach Bestätigung/Neu-Erzeugung (einmalige Anzeige). $qrUrl = $twoFactorPending ? $user->twoFactorQrCodeSvg() : null; $recoveryCodes = ($twoFactorConfirmed && $this->showRecoveryCodes) ? (json_decode(decrypt($user->two_factor_recovery_codes), true) ?: []) : []; } catch (\Throwable) { $qrUrl = null; $recoveryCodes = []; } } return [ 'user' => $user, 'twoFactorPending' => $twoFactorPending, 'twoFactorConfirmed' => $twoFactorConfirmed, 'twoFactorQrSvg' => $qrUrl, 'recoveryCodes' => $recoveryCodes, 'sessions' => DB::table('sessions') ->where('user_id', $user->id) ->orderByDesc('last_activity') ->limit(5) ->get(['id', 'ip_address', 'user_agent', 'last_activity']), ]; } }; ?>
{{-- ============== PAGE HEADER ============== --}} @if (session('security-status'))
{{ session('security-status') }}
@endif @if (session('security-error'))
{{ session('security-error') }}
@endif {{-- ============== KPI-Reihe ============== --}}
{{ __('E-Mail') }}
{{ $user->email }}
@if ($user->email_verified_at) {{ __('Bestätigt') }} @else {{ __('Nicht bestätigt') }} @endif
{{ __('Zwei-Faktor') }}
@if ($twoFactorConfirmed) {{ __('Aktiv') }} @elseif ($twoFactorPending) {{ __('Einrichtung ausstehend') }} @else {{ __('Nicht aktiv') }} @endif
@if ($twoFactorConfirmed) {{ __('Zusatzschutz aktiv') }} @elseif ($twoFactorPending) {{ __('Bestätigung erforderlich') }} @else {{ __('Empfohlen') }} @endif
{{ __('Letzter Login') }}
{{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Unbekannt') }}
{{ $user->last_login_ip ?: __('Keine IP gespeichert') }}
{{ __('Aktive Sessions') }}
{{ $sessions->count() }}
{{ __('Aus den aktuellen Web-Sessions') }}
{{ __('Passwort ändern') }}
{{ __('Passwort speichern') }}
{{ __('E-Mail-Adresse ändern') }}
@if ($user->pending_email)
{{ __('Ausstehende Änderung zu:') }} {{ $user->pending_email }}

{{ __('Der Bestätigungslink wurde an die neue Adresse gesendet. Bis zur Bestätigung bleibt die bisherige Adresse aktiv.') }}

{{ __('Änderung verwerfen') }}
@endif @if ($twoFactorConfirmed) @elseif ((string) $user->password !== '') @endif

{{ __('Sie erhalten einen Bestätigungslink an die neue Adresse. Erst nach dem Klick wird die Änderung wirksam — Ihre bisherige Adresse bleibt bis dahin aktiv.') }}

{{ __('Bestätigungslink senden') }}
{{ __('Zwei-Faktor-Authentifizierung') }} @if ($twoFactorConfirmed) {{ __('Aktiv') }} @elseif ($twoFactorPending) {{ __('Bestätigung erforderlich') }} @endif
@if (! $twoFactorPending && ! $twoFactorConfirmed) {{-- Zustand 1: nicht eingerichtet --}}

{{ __('Schützen Sie Ihren Account zusätzlich mit einer Authenticator-App (TOTP).') }}

{{ __('Zwei-Faktor-Authentifizierung aktivieren') }} @elseif ($twoFactorPending) {{-- Zustand 2: Secret erzeugt, Bestätigung mit App-Code steht aus. Erst die Bestätigung aktiviert die Login-Abfrage. --}}
{{-- QR-Code: bg-white ist BEWUSST konstant in beiden Modi — QR-Codes brauchen schwarz-auf-weiß für zuverlässiges Scannen. --}}
{!! $twoFactorQrSvg !!}

{{ __('Scannen Sie den QR-Code mit Ihrer Authenticator-App (z. B. 1Password, Google Authenticator) und geben Sie anschließend den angezeigten 6-stelligen Code ein. Erst danach ist die Zwei-Faktor-Authentifizierung aktiv.') }}

{{ __('Einrichtung bestätigen') }} {{ __('Abbrechen') }}
@else {{-- Zustand 3: aktiv. Recovery-Codes nur einmalig direkt nach Bestätigung bzw. Neu-Erzeugung sichtbar. --}}

{{ __('Die Zwei-Faktor-Authentifizierung ist aktiv. Beim Login wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.') }}

@if (! empty($recoveryCodes))
{{ __('Wiederherstellungs-Codes') }}

{{ __('Speichern Sie diese Codes jetzt an einem sicheren Ort — sie werden nur dieses eine Mal angezeigt. Jeder Code ersetzt einmalig Ihre Authenticator-App, falls Sie den Zugriff darauf verlieren.') }}

    @foreach ($recoveryCodes as $code)
  • {{ $code }}
  • @endforeach
@endif

{{ __('Zum Deaktivieren oder für neue Wiederherstellungs-Codes geben Sie einen aktuellen Code aus Ihrer Authenticator-App oder einen Wiederherstellungs-Code ein.') }}

{{ __('Neue Wiederherstellungs-Codes erzeugen') }} {{ __('Zwei-Faktor deaktivieren') }}
@endif
{{ __('Aktive Sessions') }} {{ $sessions->count() }} {{ __('Einträge') }}

{{ __('Hier sehen Sie die letzten bekannten Web-Sessions Ihres Kontos. Abmelden erfolgt aktuell über das Nutzer-Menü.') }}

@forelse ($sessions as $session)
{{ $session->ip_address ?: __('IP unbekannt') }}
{{ Str::limit($session->user_agent ?: __('User-Agent unbekannt'), 120) }}
{{ \Carbon\Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }}
@empty
{{ __('Keine Sessions gefunden') }}

{{ __('Sobald Sessions protokolliert werden, erscheinen sie hier.') }}

@endforelse