presseportale/resources/views/livewire/customer/security.blade.php
Kevin Adametz 5b8bdf4182
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
12-05-2026 Frontend dev
2026-05-12 18:32:33 +02:00

295 lines
12 KiB
PHP

<?php
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Laravel\Fortify\Features;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends Component
{
public string $current_password = '';
public string $password = '';
public string $password_confirmation = '';
public string $email = '';
public bool $confirmedTwoFactor = false;
public function mount(): void
{
$user = auth()->user();
$this->email = (string) $user->email;
$this->confirmedTwoFactor = ! is_null($user->two_factor_confirmed_at ?? null);
}
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.'));
}
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();
$user->forceFill([
'email' => $validated['email'],
'email_verified_at' => null,
])->save();
if (Features::enabled(Features::emailVerification())) {
$user->sendEmailVerificationNotification();
}
session()->flash('security-status', __('E-Mail-Adresse aktualisiert. Bitte erneut bestätigen, falls eine Verifizierung verschickt wurde.'));
}
public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable): void
{
$enable(auth()->user());
session()->flash('security-status', __('Zwei-Faktor-Authentifizierung aktiviert. Scannen Sie den QR-Code mit Ihrer Authenticator-App.'));
}
public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable): void
{
$disable(auth()->user());
$this->confirmedTwoFactor = false;
session()->flash('security-status', __('Zwei-Faktor-Authentifizierung deaktiviert.'));
}
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate): void
{
$generate(auth()->user());
session()->flash('security-status', __('Neue Wiederherstellungs-Codes erzeugt.'));
}
public function with(): array
{
/** @var User $user */
$user = auth()->user();
$user->refresh();
$qrUrl = null;
$recoveryCodes = [];
if (! is_null($user->two_factor_secret ?? null) && Features::enabled(Features::twoFactorAuthentication())) {
try {
$qrUrl = $user->twoFactorQrCodeSvg();
$recoveryCodes = json_decode(decrypt($user->two_factor_recovery_codes), true) ?: [];
} catch (\Throwable) {
$qrUrl = null;
$recoveryCodes = [];
}
}
return [
'user' => $user,
'twoFactorEnabled' => ! is_null($user->two_factor_secret ?? null),
'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']),
];
}
}; ?>
<div class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Konto-Sicherheit') }}</flux:heading>
<flux:subheading>
{{ __('Passwort, E-Mail und Zwei-Faktor-Authentifizierung verwalten.') }}
</flux:subheading>
</flux:card>
@if(session('security-status'))
<flux:callout color="green" icon="check-circle">{{ session('security-status') }}</flux:callout>
@endif
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('E-Mail') }}</flux:text>
<flux:text weight="bold" class="mt-1 truncate">{{ $user->email }}</flux:text>
<flux:badge class="mt-3" color="{{ $user->email_verified_at ? 'green' : 'amber' }}" size="sm">
{{ $user->email_verified_at ? __('Bestätigt') : __('Nicht bestätigt') }}
</flux:badge>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Zwei-Faktor') }}</flux:text>
<flux:text weight="bold" class="mt-1">
{{ $twoFactorEnabled ? __('Aktiv') : __('Nicht aktiv') }}
</flux:text>
<flux:badge class="mt-3" color="{{ $twoFactorEnabled ? 'green' : 'zinc' }}" size="sm">
{{ $twoFactorEnabled ? __('Zusatzschutz aktiv') : __('Empfohlen') }}
</flux:badge>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Letzter Login') }}</flux:text>
<flux:text weight="bold" class="mt-1">
{{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Unbekannt') }}
</flux:text>
<flux:text class="mt-3 text-xs text-zinc-500">
{{ $user->last_login_ip ?: __('Keine IP gespeichert') }}
</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Aktive Sessions') }}</flux:text>
<flux:text weight="bold" class="mt-1">{{ $sessions->count() }}</flux:text>
<flux:text class="mt-3 text-xs text-zinc-500">
{{ __('Aus den aktuellen Web-Sessions') }}
</flux:text>
</flux:card>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('Passwort ändern') }}</flux:heading>
<form wire:submit="updatePassword" class="space-y-4">
<flux:input wire:model="current_password" type="password" :label="__('Aktuelles Passwort')" autocomplete="current-password" required />
<flux:input wire:model="password" type="password" :label="__('Neues Passwort')" autocomplete="new-password" required />
<flux:input wire:model="password_confirmation" type="password" :label="__('Neues Passwort bestätigen')" autocomplete="new-password" required />
<div class="flex justify-end">
<flux:button type="submit" variant="primary">{{ __('Passwort speichern') }}</flux:button>
</div>
</form>
</flux:card>
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('E-Mail-Adresse ändern') }}</flux:heading>
<form wire:submit="updateEmail" class="space-y-4">
<flux:input wire:model="email" type="email" :label="__('Neue E-Mail-Adresse')" autocomplete="email" required />
<flux:text class="text-xs text-zinc-500">
{{ __('Nach der Änderung kann eine erneute Bestätigung der E-Mail-Adresse erforderlich sein.') }}
</flux:text>
<div class="flex justify-end">
<flux:button type="submit" variant="primary">{{ __('E-Mail speichern') }}</flux:button>
</div>
</form>
</flux:card>
</div>
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('Zwei-Faktor-Authentifizierung') }}</flux:heading>
@if(! $twoFactorEnabled)
<flux:text class="text-sm text-zinc-500">
{{ __('Schützen Sie Ihren Account zusätzlich mit einer Authenticator-App (TOTP).') }}
</flux:text>
<flux:button class="mt-4" wire:click="enableTwoFactorAuthentication" variant="primary">
{{ __('Zwei-Faktor-Authentifizierung aktivieren') }}
</flux:button>
@else
@if($twoFactorQrSvg)
<div class="flex flex-col gap-4 lg:flex-row lg:items-start">
<div class="rounded-md border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-800">
{!! $twoFactorQrSvg !!}
</div>
<div class="space-y-3">
<flux:text class="text-sm">
{{ __('Scannen Sie den QR-Code mit Ihrer Authenticator-App (z. B. 1Password, Google Authenticator).') }}
</flux:text>
@if(! empty($recoveryCodes))
<flux:heading size="xs">{{ __('Wiederherstellungs-Codes') }}</flux:heading>
<ul class="grid grid-cols-2 gap-2 text-xs font-mono">
@foreach($recoveryCodes as $code)
<li class="rounded bg-zinc-100 px-2 py-1 dark:bg-zinc-800">{{ $code }}</li>
@endforeach
</ul>
@endif
</div>
</div>
@endif
<div class="mt-4 flex flex-wrap gap-2">
<flux:button wire:click="regenerateRecoveryCodes" variant="ghost">
{{ __('Neue Wiederherstellungs-Codes erzeugen') }}
</flux:button>
<flux:button wire:click="disableTwoFactorAuthentication" variant="danger">
{{ __('Zwei-Faktor deaktivieren') }}
</flux:button>
</div>
@endif
</flux:card>
<flux:card class="p-0">
<div class="border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Aktive Sessions') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Hier sehen Sie die letzten bekannten Web-Sessions Ihres Kontos. Abmelden erfolgt aktuell über das Nutzer-Menü.') }}
</flux:text>
</div>
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse($sessions as $session)
<div class="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<flux:text weight="semibold">
{{ $session->ip_address ?: __('IP unbekannt') }}
</flux:text>
<flux:text class="mt-1 truncate text-xs text-zinc-500">
{{ Str::limit($session->user_agent ?: __('User-Agent unbekannt'), 120) }}
</flux:text>
</div>
<flux:badge color="zinc" size="sm">
{{ \Carbon\Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }}
</flux:badge>
</div>
@empty
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<flux:icon.shield-check class="size-10 text-zinc-300" />
<flux:text weight="semibold" class="mt-3">{{ __('Keine Sessions gefunden') }}</flux:text>
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
{{ __('Sobald Sessions protokolliert werden, erscheinen sie hier.') }}
</flux:text>
</div>
@endforelse
</div>
</flux:card>
</div>