*/ public function forUser(User $user): array { $currentId = Session::getId(); return DB::table('sessions') ->where('user_id', $user->id) ->orderByDesc('last_activity') ->get(['id', 'ip_address', 'user_agent', 'last_activity']) ->map(fn ($row) => [ 'id' => $row->id, 'ip' => $row->ip_address, 'userAgentShort' => $this->shortenUserAgent($row->user_agent), 'lastActivity' => Carbon::createFromTimestamp((int) $row->last_activity), 'isCurrent' => $row->id === $currentId, ]) ->all(); } /** * Sign the user out of every device EXCEPT the current one. The current * session stays valid (nothing rotated for self), so the operator keeps * working uninterrupted. */ public function logoutOtherDevices(User $user): void { DB::table('sessions') ->where('user_id', $user->id) ->where('id', '!=', Session::getId()) ->delete(); } /** * Kill ALL of this user's sessions and rotate their remember_token, so a * stolen remember-me cookie can no longer resurrect a session. Used by the * (separate) multi-user admin feature; not wired to a per-row button here. */ public function logoutUserEverywhere(User $user): void { DB::table('sessions')->where('user_id', $user->id)->delete(); $user->forceFill(['remember_token' => Str::random(60)])->save(); } /** * Global logout: truncate every session and rotate every user's * remember_token. Destructive — logs out the current operator too. The * caller (Livewire) is responsible for the R5 confirm, audit, and the * redirect to login that must follow. */ public function logoutEveryone(): void { DB::table('sessions')->truncate(); User::query()->eachById(function (User $user) { $user->forceFill(['remember_token' => Str::random(60)])->save(); }); } /** * Collapse a raw User-Agent header into a short "Browser · OS" label for the * list. Best-effort string sniffing (no extra dependency); falls back to * "unknown" copy when the header is empty or unrecognized. */ private function shortenUserAgent(?string $userAgent): string { if ($userAgent === null || trim($userAgent) === '') { return __('sessions.ua_unknown'); } $browser = match (true) { str_contains($userAgent, 'Edg/') => 'Edge', str_contains($userAgent, 'OPR/') || str_contains($userAgent, 'Opera') => 'Opera', str_contains($userAgent, 'Firefox') => 'Firefox', str_contains($userAgent, 'Chrome') => 'Chrome', str_contains($userAgent, 'Safari') => 'Safari', default => null, }; $os = match (true) { str_contains($userAgent, 'Windows') => 'Windows', str_contains($userAgent, 'Android') => 'Android', str_contains($userAgent, 'iPhone') || str_contains($userAgent, 'iPad') => 'iOS', str_contains($userAgent, 'Mac OS') || str_contains($userAgent, 'Macintosh') => 'macOS', str_contains($userAgent, 'Linux') => 'Linux', default => null, }; $label = trim(implode(' · ', array_filter([$browser, $os]))); // Nothing matched → show a trimmed prefix of the raw header so the row is // still distinguishable, rather than a useless "unknown". return $label !== '' ? $label : Str::limit($userAgent, 40); } }