295 lines
12 KiB
PHP
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>
|