feat(security): Sicherheitsseite zeigt dieses Gerät und die letzten Anmeldungen
Die Liste „Aktive Sessions“ las die sessions-Tabelle, Live speichert Sessions in Redis — sie war immer leer. Jetzt: „Dieses Gerät“ (seit wann angemeldet) und die letzten fünf Anmeldungen mit Anmeldeweg aus dem Aktivitätsverlauf, ohne IP; Kachel „Anmeldungen in 30 Tagen“ (Roadmap 4.1). Changelog: Verbessert Public: Die Sicherheitsseite zeigt Ihre letzten Anmeldungen und wie Sie sich jeweils angemeldet haben. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
53e3b270e0
commit
29e68330ca
2 changed files with 93 additions and 26 deletions
|
|
@ -1,15 +1,16 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Admin\UserImpersonation;
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\SecurityChange;
|
||||
use App\Livewire\Concerns\ConfirmsIdentity;
|
||||
use App\Mail\EmailChangeConfirmation;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\PasswordChanger;
|
||||
use App\Services\Auth\SecurityNotifier;
|
||||
use App\Services\Auth\SessionRevocation;
|
||||
use App\Services\Auth\StrongAuthentication;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
|
@ -309,11 +310,23 @@ new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends
|
|||
'recoveryCodes' => $recoveryCodes,
|
||||
'securityProofKind' => $this->securityProofKind($user),
|
||||
'passkeys' => $user->passkeys()->latest()->get(['id', 'name', 'credential', 'last_used_at', 'created_at']),
|
||||
'sessions' => DB::table('sessions')
|
||||
// Sessions liegen in Redis und lassen sich nicht je Konto aufzählen;
|
||||
// die Liste zeigt deshalb das aktuelle Gerät und die letzten
|
||||
// Anmeldungen aus dem Aktivitätsverlauf (ohne IP).
|
||||
'currentSessionSince' => ($loggedInAtMs = (int) session(SessionRevocation::SESSION_KEY, 0)) > 0
|
||||
? \Carbon\Carbon::createFromTimestampMs($loggedInAtMs)
|
||||
: null,
|
||||
'recentLogins' => ActivityEvent::query()
|
||||
->where('user_id', $user->id)
|
||||
->orderByDesc('last_activity')
|
||||
->where('type', ActivityType::UserLoggedIn)
|
||||
->orderByDesc('occurred_at')
|
||||
->limit(5)
|
||||
->get(['id', 'ip_address', 'user_agent', 'last_activity']),
|
||||
->get(['occurred_at', 'meta']),
|
||||
'loginsLast30Days' => ActivityEvent::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('type', ActivityType::UserLoggedIn)
|
||||
->where('occurred_at', '>=', now()->subDays(30))
|
||||
->count(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
|
@ -405,11 +418,11 @@ new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends
|
|||
|
||||
<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') }}
|
||||
{{ __('Anmeldungen') }}
|
||||
</div>
|
||||
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">{{ $sessions->count() }}</div>
|
||||
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">{{ $loginsLast30Days }}</div>
|
||||
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Aus den aktuellen Web-Sessions') }}
|
||||
{{ __('In den letzten 30 Tagen') }}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
|
@ -608,9 +621,9 @@ new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends
|
|||
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Aktive Sessions') }}</span>
|
||||
<span class="section-eyebrow">{{ __('Geräte und Anmeldungen') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $sessions->count() }} {{ __('Einträge') }}
|
||||
{{ __('Letzte :count Anmeldungen', ['count' => $recentLogins->count()]) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 px-5 pb-2 pt-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
|
|
@ -625,32 +638,50 @@ new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends
|
|||
</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)]">
|
||||
{{ __('Dieses Gerät') }}
|
||||
</div>
|
||||
<div class="mt-0.5 text-[11.5px] text-[color:var(--color-ink-3)] truncate">
|
||||
{{ Str::limit(request()->userAgent() ?: __('Browser unbekannt'), 120) }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge ok">
|
||||
@if ($currentSessionSince)
|
||||
{{ __('angemeldet :since', ['since' => $currentSessionSince->diffForHumans()]) }}
|
||||
@else
|
||||
{{ __('angemeldet') }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@forelse ($recentLogins as $login)
|
||||
<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') }}
|
||||
{{ $login->occurred_at->format('d.m.Y H:i') }}
|
||||
</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 class="mt-0.5 text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ match ($login->meta['method'] ?? 'password') {
|
||||
'magic-link' => __('Anmeldung per Magic-Link'),
|
||||
'google' => __('Anmeldung mit Google'),
|
||||
'passkey' => __('Anmeldung mit Passkey'),
|
||||
'impersonation' => __('Zugriff durch den Support'),
|
||||
default => __('Anmeldung mit Passwort'),
|
||||
} }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge hub">
|
||||
{{ \Carbon\Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }}
|
||||
</span>
|
||||
<span class="badge hub">{{ $login->occurred_at->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 class="px-5 py-6 text-center text-[12px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Noch keine weiteren Anmeldungen protokolliert.') }}
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<p class="px-5 pb-4 pt-2 text-[11.5px] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Laufende Anmeldungen auf anderen Geräten werden nicht einzeln angezeigt. Erkennen Sie eine Anmeldung nicht, ändern Sie Ihr Passwort und melden Sie die anderen Geräte ab.') }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue