subSeconds($timeoutSeconds); // Get active Sanctum tokens with user information $onlineTokens = PersonalAccessToken::with(['tokenable.unit', 'tokenable.rank', 'tokenable.position']) ->where('tokenable_type', User::class) ->where('last_used_at', '>', $cutoffTime) ->where(function ($query) { $query->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->whereHas('tokenable', function ($query) { $query->whereNull('deleted_at'); }) ->orderBy('last_used_at', 'desc') ->get(); // Group by user to handle multiple tokens $onlineUsers = $onlineTokens->groupBy('tokenable_id')->map(function ($tokens) { $token = $tokens->first(); $user = $token->tokenable; $latestToken = $tokens->sortByDesc('last_used_at')->first(); return [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'army_number' => $user->army_number, 'image_url' => $user->image_url ? Storage::disk('public')->url($user->image_url) : null, 'status' => $user->status, 'unit_name' => $user->unit?->name, 'rank_name' => $user->rank?->name, 'position_name' => $user->position?->name, 'ip_address' => 'N/A', // Sanctum doesn't store IP by default 'user_agent' => $this->parseUserAgent('Sanctum Token'), 'last_activity' => $latestToken->last_used_at?->timestamp ?? $latestToken->created_at->timestamp, 'last_activity_human' => $latestToken->last_used_at?->diffForHumans() ?? $latestToken->created_at->diffForHumans(), 'session_count' => $tokens->count(), 'is_online' => true ]; }); return $onlineUsers->values(); }); } /** * Get online users count * * @param int $timeoutMinutes Minutes of inactivity to consider user offline * @return int */ public function getOnlineUsersCount(int $timeoutMinutes = 5): int { $cacheKey = "online_users_count_{$timeoutMinutes}"; return Cache::remember($cacheKey, 30, function () use ($timeoutMinutes) { $timeoutSeconds = $timeoutMinutes * 60; $cutoffTime = now()->subSeconds($timeoutSeconds); return PersonalAccessToken::where('tokenable_type', User::class) ->where('last_used_at', '>', $cutoffTime) ->where(function ($query) { $query->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->whereHas('tokenable', function ($query) { $query->whereNull('deleted_at'); }) ->distinct('tokenable_id') ->count('tokenable_id'); }); } /** * Get user's current session information * * @param int $userId * @return array|null */ public function getUserSessionInfo(int $userId): ?array { $token = PersonalAccessToken::where('tokenable_type', User::class) ->where('tokenable_id', $userId) ->where(function ($query) { $query->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->orderBy('last_used_at', 'desc') ->first(); if (!$token) { return null; } return [ 'session_id' => $token->id, 'ip_address' => 'N/A', // Sanctum doesn't store IP by default 'user_agent' => $this->parseUserAgent('Sanctum Token'), 'last_activity' => $token->last_used_at?->timestamp ?? $token->created_at->timestamp, 'last_activity_human' => $token->last_used_at?->diffForHumans() ?? $token->created_at->diffForHumans(), 'is_online' => $token->last_used_at && $token->last_used_at->gt(now()->subMinutes(5)) ]; } /** * Parse user agent string to extract browser and OS info * * @param string $userAgent * @return array */ private function parseUserAgent(string $userAgent): array { $browser = 'Unknown'; $os = 'Unknown'; // Simple browser detection if (strpos($userAgent, 'Chrome') !== false) { $browser = 'Chrome'; } elseif (strpos($userAgent, 'Firefox') !== false) { $browser = 'Firefox'; } elseif (strpos($userAgent, 'Safari') !== false) { $browser = 'Safari'; } elseif (strpos($userAgent, 'Edge') !== false) { $browser = 'Edge'; } // Simple OS detection if (strpos($userAgent, 'Windows') !== false) { $os = 'Windows'; } elseif (strpos($userAgent, 'Mac') !== false) { $os = 'macOS'; } elseif (strpos($userAgent, 'Linux') !== false) { $os = 'Linux'; } elseif (strpos($userAgent, 'Android') !== false) { $os = 'Android'; } elseif (strpos($userAgent, 'iOS') !== false) { $os = 'iOS'; } return [ 'browser' => $browser, 'os' => $os, 'raw' => $userAgent ]; } /** * Clear online users cache */ public function clearCache(): void { Cache::forget('online_users_5'); Cache::forget('online_users_10'); Cache::forget('online_users_15'); Cache::forget('online_users_30'); Cache::forget('online_users_count_5'); Cache::forget('online_users_count_10'); Cache::forget('online_users_count_15'); Cache::forget('online_users_count_30'); } /** * Get online users statistics * * @return array */ public function getOnlineUsersStats(): array { $stats = []; // Get stats for different timeout periods $timeouts = [5, 10, 15, 30]; foreach ($timeouts as $timeout) { $stats["online_{$timeout}min"] = $this->getOnlineUsersCount($timeout); } // Get total registered users $stats['total_users'] = User::count(); // Get users by status $stats['active_users'] = User::where('status', 'active')->count(); $stats['inactive_users'] = User::where('status', 'inactive')->count(); return $stats; } }