presseportale/resources/views/livewire/customer/security.blade.php
Kevin Adametz 2cde7284a1 livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

584 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Mail\EmailChangeConfirmation;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Laravel\Fortify\Features;
use Laravel\Fortify\TwoFactorAuthenticationProvider;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\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 string $twoFactorCode = '';
/**
* Erneuter Nachweis vor sicherheitskritischen Aktionen (Audit F03, E3):
* bei aktiver 2FA ein aktueller App-Code oder ein Wiederherstellungs-Code,
* sonst das Passwort. Eine bestehende Session allein reicht nicht, um die
* 2FA abzuschalten, Codes neu zu erzeugen oder die Adresse zu wechseln.
*/
public string $securityCode = '';
public string $securityPassword = '';
/**
* Recovery-Codes werden nur direkt nach Bestätigung bzw. Neu-Erzeugung
* angezeigt (einmalige Sichtbarkeit), nicht bei jedem Seitenaufruf.
* Gesperrt (Audit F03): nur der Server schaltet die Anzeige frei, ein
* Client-Update der Property darf die Codes nicht entschlüsseln.
*/
#[Locked]
public bool $showRecoveryCodes = false;
public function mount(): void
{
$this->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']),
];
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Sicherheit') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Konto-Sicherheit') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Passwort, E-Mail und Zwei-Faktor-Authentifizierung verwalten.') }}
</p>
</div>
</header>
@if (session('security-status'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('security-status') }}
</div>
@endif
@if (session('security-error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-warn-soft,#fef2f2)] border-[color:var(--color-warn,#dc2626)]/30 text-[color:var(--color-warn,#b91c1c)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0" />
{{ session('security-error') }}
</div>
@endif
{{-- ============== KPI-Reihe ============== --}}
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('E-Mail') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] truncate">{{ $user->email }}</div>
<div>
@if ($user->email_verified_at)
<span class="badge ok">{{ __('Bestätigt') }}</span>
@else
<span class="badge warn">{{ __('Nicht bestätigt') }}</span>
@endif
</div>
</article>
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Zwei-Faktor') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">
@if ($twoFactorConfirmed)
{{ __('Aktiv') }}
@elseif ($twoFactorPending)
{{ __('Einrichtung ausstehend') }}
@else
{{ __('Nicht aktiv') }}
@endif
</div>
<div>
@if ($twoFactorConfirmed)
<span class="badge ok">{{ __('Zusatzschutz aktiv') }}</span>
@elseif ($twoFactorPending)
<span class="badge warn">{{ __('Bestätigung erforderlich') }}</span>
@else
<span class="badge warn">{{ __('Empfohlen') }}</span>
@endif
</div>
</article>
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Letzter Login') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">
{{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Unbekannt') }}
</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)] truncate">
{{ $user->last_login_ip ?: __('Keine IP gespeichert') }}
</div>
</article>
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Aktive Sessions') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">{{ $sessions->count() }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Aus den aktuellen Web-Sessions') }}
</div>
</article>
</section>
<div class="grid gap-6 lg:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Passwort ändern') }}</span>
</div>
<form wire:submit="updatePassword" class="p-5 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 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:button type="submit" variant="primary">{{ __('Passwort speichern') }}</flux:button>
</div>
</form>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('E-Mail-Adresse ändern') }}</span>
</div>
<form wire:submit="updateEmail" class="p-5 space-y-4">
@if ($user->pending_email)
<div class="px-3 py-2.5 rounded-[5px] border text-[12px] space-y-1.5
bg-[color:var(--color-bg-elev)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
<div class="flex items-center gap-2">
<flux:icon.clock class="size-[14px] flex-shrink-0" />
<span>{{ __('Ausstehende Änderung zu:') }} <strong class="text-[color:var(--color-ink)]">{{ $user->pending_email }}</strong></span>
</div>
<p class="m-0">{{ __('Der Bestätigungslink wurde an die neue Adresse gesendet. Bis zur Bestätigung bleibt die bisherige Adresse aktiv.') }}</p>
<flux:button size="sm" variant="subtle" wire:click="cancelPendingEmailChange">
{{ __('Änderung verwerfen') }}
</flux:button>
</div>
@endif
<flux:input wire:model="email" type="email" :label="__('Neue E-Mail-Adresse')" autocomplete="email" required />
@if ($twoFactorConfirmed)
<flux:input wire:model="securityCode" :label="__('Code aus der App oder Wiederherstellungs-Code')"
inputmode="numeric" autocomplete="one-time-code" placeholder="123456" required />
@elseif ((string) $user->password !== '')
<flux:input wire:model="securityPassword" type="password" :label="__('Aktuelles Passwort')" autocomplete="current-password" required />
@endif
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Sie erhalten einen Bestätigungslink an die neue Adresse. Erst nach dem Klick wird die Änderung wirksam — Ihre bisherige Adresse bleibt bis dahin aktiv.') }}
</p>
<div class="flex justify-end pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:button type="submit" variant="primary">{{ __('Bestätigungslink senden') }}</flux:button>
</div>
</form>
</article>
</div>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zwei-Faktor-Authentifizierung') }}</span>
@if ($twoFactorConfirmed)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@elseif ($twoFactorPending)
<span class="badge warn dot">{{ __('Bestätigung erforderlich') }}</span>
@endif
</div>
<div class="p-5">
@if (! $twoFactorPending && ! $twoFactorConfirmed)
{{-- Zustand 1: nicht eingerichtet --}}
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Schützen Sie Ihren Account zusätzlich mit einer Authenticator-App (TOTP).') }}
</p>
<flux:button class="mt-4" wire:click="enableTwoFactorAuthentication" variant="primary">
{{ __('Zwei-Faktor-Authentifizierung aktivieren') }}
</flux:button>
@elseif ($twoFactorPending)
{{-- Zustand 2: Secret erzeugt, Bestätigung mit App-Code steht aus.
Erst die Bestätigung aktiviert die Login-Abfrage. --}}
<div class="flex flex-col gap-5 lg:flex-row lg:items-start">
{{-- QR-Code: bg-white ist BEWUSST konstant in beiden Modi —
QR-Codes brauchen schwarz-auf-weiß für zuverlässiges Scannen. --}}
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-white p-4 flex-shrink-0">
{!! $twoFactorQrSvg !!}
</div>
<div class="space-y-4 min-w-0 flex-1">
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('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.') }}
</p>
<form wire:submit="confirmTwoFactorAuthentication" class="space-y-3 max-w-[280px]">
<flux:input wire:model="twoFactorCode" :label="__('Code aus der App')"
inputmode="numeric" autocomplete="one-time-code" placeholder="123456" required />
<div class="flex flex-wrap gap-2">
<flux:button type="submit" variant="primary">
{{ __('Einrichtung bestätigen') }}
</flux:button>
<flux:button wire:click="disableTwoFactorAuthentication" variant="subtle">
{{ __('Abbrechen') }}
</flux:button>
</div>
</form>
</div>
</div>
@else
{{-- Zustand 3: aktiv. Recovery-Codes nur einmalig direkt nach
Bestätigung bzw. Neu-Erzeugung sichtbar. --}}
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Die Zwei-Faktor-Authentifizierung ist aktiv. Beim Login wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.') }}
</p>
@if (! empty($recoveryCodes))
<div class="mt-4 space-y-3">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Wiederherstellungs-Codes') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('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.') }}
</p>
<ul class="grid grid-cols-2 gap-2 text-[11.5px] font-mono m-0 p-0 list-none max-w-[420px]">
@foreach ($recoveryCodes as $code)
<li class="rounded-[4px] bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)] px-2 py-1 text-[color:var(--color-ink)]">{{ $code }}</li>
@endforeach
</ul>
</div>
@endif
<div class="mt-5 pt-4 border-t border-[color:var(--color-bg-rule)] space-y-3">
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Zum Deaktivieren oder für neue Wiederherstellungs-Codes geben Sie einen aktuellen Code aus Ihrer Authenticator-App oder einen Wiederherstellungs-Code ein.') }}
</p>
<div class="max-w-[280px]">
<flux:input wire:model="securityCode" :label="__('Code aus der App oder Wiederherstellungs-Code')"
inputmode="numeric" autocomplete="one-time-code" placeholder="123456" />
</div>
<div class="flex flex-wrap gap-2">
<flux:button wire:click="regenerateRecoveryCodes" variant="filled">
{{ __('Neue Wiederherstellungs-Codes erzeugen') }}
</flux:button>
<flux:button wire:click="disableTwoFactorAuthentication" variant="danger">
{{ __('Zwei-Faktor deaktivieren') }}
</flux:button>
</div>
</div>
@endif
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktive Sessions') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $sessions->count() }} {{ __('Einträge') }}
</span>
</div>
<div class="px-5 pb-2 pt-3">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Hier sehen Sie die letzten bekannten Web-Sessions Ihres Kontos. Abmelden erfolgt aktuell über das Nutzer-Menü.') }}
</p>
</div>
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@forelse ($sessions as $session)
<div class="flex flex-col gap-2 px-5 py-3 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $session->ip_address ?: __('IP unbekannt') }}
</div>
<div class="mt-0.5 text-[11.5px] text-[color:var(--color-ink-3)] truncate">
{{ Str::limit($session->user_agent ?: __('User-Agent unbekannt'), 120) }}
</div>
</div>
<span class="badge hub">
{{ \Carbon\Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }}
</span>
</div>
@empty
<div class="flex flex-col items-center justify-center px-5 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.shield-check class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">{{ __('Keine Sessions gefunden') }}</div>
<p class="mt-1 max-w-md text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Sobald Sessions protokolliert werden, erscheinen sie hier.') }}
</p>
</div>
@endforelse
</div>
</article>
</div>