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; } app(PasswordChanger::class)->change(Auth::user(), $validated['password'], session()->driver()); $this->reset('current_password', 'password', 'password_confirmation'); session()->flash('security-status', __('Passwort aktualisiert. Alle anderen angemeldeten Geräte wurden abgemeldet.')); } /** * 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.')); } /** * Erstmalige Einrichtung nur mit frischem Nachweis (Sicherheitsaudit * 07.10.2026, S-06): Wer nur eine fremde Session hat, soll keinen eigenen * Authenticator registrieren und den Inhaber damit aussperren können. */ public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable): void { /** @var User $user */ $user = auth()->user(); $this->assertSecurityProof($user); $enable($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; app(SecurityNotifier::class)->notify(auth()->user(), SecurityChange::TwoFactorEnabled); // Der gerade bestätigte Code belegt den zweiten Faktor auch für den // Admin-Bereich (Entscheidung 5). app(StrongAuthentication::class)->mark(auth()->user(), 'totp'); 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. $wasEnabled = $user->hasEnabledTwoFactorAuthentication(); if ($wasEnabled) { $this->assertSecondFactor($user); } $disable($user); $this->reset('twoFactorCode'); $this->showRecoveryCodes = false; if ($wasEnabled) { app(SecurityNotifier::class)->notify($user, SecurityChange::TwoFactorDisabled); } 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.')); } /** * Startet das Hinzufügen eines Passkeys (Entscheidung 5, 07.10.2026): * erst der frische Identitätsnachweis, dann die Browser-Zeremonie gegen * Fortifys Registrierungsrouten (EnsurePasskeyManagementIsConfirmed prüft * den Nachweis dort erneut). */ public function startPasskeyRegistration(): void { $this->ensureNotImpersonating(); $validated = $this->validate(['passkeyName' => ['required', 'string', 'max:60']], [], [ 'passkeyName' => __('Name des Passkeys'), ]); $this->assertSecurityProof(auth()->user()); $this->dispatch('passkey-register', name: $validated['passkeyName']); } public function passkeyRegistered(): void { $this->reset('passkeyName'); session()->flash('security-status', __('Passkey hinzugefügt. Sie können sich ab sofort damit anmelden.')); } public function deletePasskey(int $passkeyId, DeletePasskey $delete): void { $this->ensureNotImpersonating(); /** @var User $user */ $user = auth()->user(); $this->assertSecurityProof($user); $passkey = $user->passkeys()->whereKey($passkeyId)->firstOrFail(); $delete($user, $passkey); session()->flash('security-status', __('Passkey entfernt.')); } private function ensureNotImpersonating(): void { abort_if(app(UserImpersonation::class)->isActive(), 403, __('Während der Benutzer-Impersonation können keine Passkeys verwaltet werden.')); } 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, 'securityProofKind' => $this->securityProofKind($user), 'passkeys' => $user->passkeys()->latest()->get(['id', 'name', 'credential', 'last_used_at', 'created_at']), 'sessions' => DB::table('sessions') ->where('user_id', $user->id) ->orderByDesc('last_activity') ->limit(5) ->get(['id', 'ip_address', 'user_agent', 'last_activity']), ]; } }; ?>
{{ __('Passwort, E-Mail und Zwei-Faktor-Authentifizierung verwalten.') }}
{{ __('Mit einem Passkey melden Sie sich per Fingerabdruck, Gesicht oder Geräte-PIN an — ohne Passwort und ohne Code aus der App. Passkeys gelten zugleich als zweiter Faktor und funktionieren nur auf der echten pressekonto-Seite.') }}
@if ($passkeys->isNotEmpty()){{ __('Schützen Sie Ihren Account zusätzlich mit einer Authenticator-App (TOTP).') }}
@elseif ($twoFactorPending) {{-- Zustand 2: Secret erzeugt, Bestätigung mit App-Code steht aus. Erst die Bestätigung aktiviert die Login-Abfrage. --}}{{ __('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.') }}
{{ __('Die Zwei-Faktor-Authentifizierung ist aktiv. Beim Login wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.') }}
@if (! empty($recoveryCodes)){{ __('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.') }}
{{ __('Zum Deaktivieren oder für neue Wiederherstellungs-Codes geben Sie einen aktuellen Code aus Ihrer Authenticator-App oder einen Wiederherstellungs-Code ein.') }}
{{ __('Hier sehen Sie die letzten bekannten Web-Sessions Ihres Kontos. Abmelden erfolgt aktuell über das Nutzer-Menü.') }}
{{ __('Sobald Sessions protokolliert werden, erscheinen sie hier.') }}