beforePluginAction()) { return $this->fail($error); } return $this->success($this->buildPayload($request->user())); } public function panelUrl(Request $request) { if ($error = $this->beforePluginAction()) { return $this->fail($error); } $user = $request->user(); $ttlMinutes = max(1, (int) $this->getConfig('signed_url_ttl_minutes', 60)); $expiresAt = now()->addMinutes($ttlMinutes); return $this->success([ 'url' => URL::temporarySignedRoute( 'user-online-devices.panel', $expiresAt, ['user' => $user->id] ), 'expires_at' => $expiresAt->timestamp, ]); } public function adminUsersPage(): View { abort_unless($this->isPluginEnabled(), 404); $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); return view('UserOnlineDevices::admin-users', [ 'apiEndpoint' => url('/api/v2/' . $securePath . '/user-online-devices/users'), 'adminHomeUrl' => url('/' . $securePath), ]); } public function adminUsers(Request $request) { if ($error = $this->beforePluginAction()) { return $this->fail($error); } $current = max(1, (int) $request->input('current', 1)); $pageSize = min(100, max(10, (int) $request->input('pageSize', 20))); $keyword = trim((string) $request->input('keyword', '')); $query = User::query() ->select(['id', 'email', 'last_online_at', 'created_at']); if ($keyword !== '') { $query->where(function ($builder) use ($keyword) { $builder->where('email', 'like', '%' . $keyword . '%'); if (is_numeric($keyword)) { $builder->orWhere('id', (int) $keyword); } }); } $users = $query ->orderByDesc('last_online_at') ->orderByDesc('id') ->paginate($pageSize, ['*'], 'page', $current); return $this->success($this->buildAdminUsersPayload($users)); } public function panel(Request $request, int $user): View { abort_unless($this->isPluginEnabled(), 404); $targetUser = User::query()->findOrFail($user); $payload = $this->buildPayload($targetUser); $ttlMinutes = max(1, (int) $this->getConfig('signed_url_ttl_minutes', 60)); return view('UserOnlineDevices::panel', [ 'payload' => $payload, 'snapshotUrl' => URL::temporarySignedRoute( 'user-online-devices.snapshot', now()->addMinutes($ttlMinutes), ['user' => $targetUser->id] ), 'refreshInterval' => max(5, (int) $this->getConfig('refresh_interval_seconds', 15)), ]); } public function snapshot(Request $request, int $user) { abort_unless($this->isPluginEnabled(), 404); $targetUser = User::query()->findOrFail($user); return response()->json([ 'data' => $this->buildPayload($targetUser), ]); } private function buildPayload(User $user): array { $devices = $this->deviceStateService->getUsersDevices([$user->id]); $ips = collect($devices[$user->id] ?? []) ->filter(fn ($ip) => is_string($ip) && $ip !== '') ->unique() ->values(); return [ 'user' => [ 'id' => $user->id, 'email' => $user->email, 'online_count' => $ips->count(), 'last_online_at' => $user->last_online_at, ], 'online_devices' => $this->formatOnlineDevices($ips), 'active_sessions' => $this->shouldShowActiveSessions() ? $this->formatActiveSessions($user) : [], 'meta' => [ 'generated_at' => time(), 'refresh_interval_seconds' => max(5, (int) $this->getConfig('refresh_interval_seconds', 15)), 'mask_ip' => (bool) $this->getConfig('mask_ip', false), 'show_active_sessions' => $this->shouldShowActiveSessions(), ], ]; } private function buildAdminUsersPayload(LengthAwarePaginator $users): array { $items = $users->getCollection(); $devicesByUser = $this->deviceStateService->getUsersDevices($items->pluck('id')->all()); $users->setCollection($items->map(function (User $user) use ($devicesByUser) { $ips = collect($devicesByUser[$user->id] ?? []) ->filter(fn ($ip) => is_string($ip) && $ip !== '') ->unique() ->values(); return [ 'id' => $user->id, 'email' => $user->email, 'online_count' => $ips->count(), 'online_devices' => $this->formatOnlineDevices($ips), 'last_online_at' => $user->last_online_at?->timestamp, 'created_at' => $user->created_at?->timestamp, ]; })); return [ 'list' => $users->items(), 'pagination' => [ 'current' => $users->currentPage(), 'pageSize' => $users->perPage(), 'total' => $users->total(), 'lastPage' => $users->lastPage(), ], 'meta' => [ 'generated_at' => time(), 'refresh_interval_seconds' => max(5, (int) $this->getConfig('refresh_interval_seconds', 15)), 'mask_ip' => (bool) $this->getConfig('mask_ip', false), ], ]; } private function formatOnlineDevices(Collection $ips): array { return $ips->values()->map(function (string $ip, int $index) { return [ 'name' => 'Device ' . ($index + 1), 'ip' => $this->maskIp($ip), 'raw_ip' => $ip, ]; })->all(); } private function formatActiveSessions(User $user): array { return $user->tokens() ->where(function ($query) { $query->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->orderByDesc('last_used_at') ->get(['id', 'name', 'last_used_at', 'created_at', 'expires_at']) ->map(function ($token) { return [ 'id' => $token->id, 'device' => $token->name ?: 'Unknown Session', 'last_used_at' => $token->last_used_at?->timestamp, 'created_at' => $token->created_at?->timestamp, 'expires_at' => $token->expires_at?->timestamp, ]; }) ->all(); } private function shouldShowActiveSessions(): bool { return (bool) $this->getConfig('show_active_sessions', true); } private function maskIp(string $ip): string { if (!(bool) $this->getConfig('mask_ip', false)) { return $ip; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $parts = explode('.', $ip); $parts[3] = '*'; return implode('.', $parts); } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $parts = explode(':', $ip); $count = count($parts); if ($count > 2) { $parts[$count - 1] = '****'; $parts[$count - 2] = '****'; } return implode(':', $parts); } return $ip; } }