diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d669dfb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +reference/ +Thumbs.db +.DS_Store +*.zip diff --git a/plugins/.gitignore b/plugins/.gitignore index c73c1df..52a0795 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -1,10 +1,4 @@ * !.gitignore -!AlipayF2f/ -!Btcpay -!Coinbase -!Epay -!Mgate -!Telegram !UserOnlineDevices/ !UserOnlineDevices/** diff --git a/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php b/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php index 7d390ef..7480606 100644 --- a/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php +++ b/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php @@ -5,11 +5,10 @@ namespace Plugin\UserOnlineDevices\Controllers; use App\Http\Controllers\PluginController; use App\Models\User; use App\Services\DeviceStateService; +use DateTimeInterface; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\URL; class UserOnlineDevicesController extends PluginController { @@ -18,231 +17,107 @@ class UserOnlineDevicesController extends PluginController ) { } - public function summary(Request $request) + public function index(Request $request): View { if ($error = $this->beforePluginAction()) { - return $this->fail($error); + abort($error[0], $error[1]); } - return $this->success($this->buildPayload($request->user())); - } + $defaultPageSize = (int) $this->getConfig('default_page_size', 20); + $pageSize = (int) $request->integer('per_page', $defaultPageSize); + $pageSize = in_array($pageSize, [20, 50, 100], true) ? $pageSize : $defaultPageSize; - 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', '')); + $keyword = trim((string) $request->query('keyword', '')); $query = User::query() - ->select(['id', 'email', 'last_online_at', 'created_at']); + ->select([ + 'id', + 'email', + 'plan_id', + 'online_count', + 'last_online_at', + 'created_at', + ]) + ->with(['plan:id,name']) + ->orderByDesc('id'); if ($keyword !== '') { $query->where(function ($builder) use ($keyword) { $builder->where('email', 'like', '%' . $keyword . '%'); - if (is_numeric($keyword)) { + + if (ctype_digit($keyword)) { $builder->orWhere('id', (int) $keyword); } }); } - $users = $query - ->orderByDesc('last_online_at') - ->orderByDesc('id') - ->paginate($pageSize, ['*'], 'page', $current); + /** @var LengthAwarePaginator $paginator */ + $paginator = $query->paginate($pageSize)->appends($request->query()); - return $this->success($this->buildAdminUsersPayload($users)); - } + $userIds = $paginator->getCollection()->pluck('id')->all(); + $devicesByUser = $this->deviceStateService->getUsersDevices($userIds); - public function panel(Request $request, int $user): View - { - abort_unless($this->isPluginEnabled(), 404); + $paginator->setCollection( + $paginator->getCollection()->map(function (User $user) use ($devicesByUser) { + $ips = array_values($devicesByUser[$user->id] ?? []); + sort($ips); - $targetUser = User::query()->findOrFail($user); - $payload = $this->buildPayload($targetUser); - $ttlMinutes = max(1, (int) $this->getConfig('signed_url_ttl_minutes', 60)); + $user->online_devices = $ips; + $user->online_count_live = count($ips); + $user->subscription_name = $user->plan?->name ?: 'No subscription'; + $user->last_online_text = $this->formatDateTime($user->last_online_at); + $user->created_text = $this->formatDateTime($user->created_at); - 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)), + return $user; + }) + ); + + $pageUsers = $paginator->getCollection()->count(); + $usersWithOnlineIp = $paginator->getCollection()->filter(function (User $user) { + return ($user->online_count_live ?? 0) > 0; + })->count(); + $totalOnlineIps = $paginator->getCollection()->sum(function (User $user) { + return (int) ($user->online_count_live ?? 0); + }); + + $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); + + return view('UserOnlineDevices::admin-index', [ + 'users' => $paginator, + 'filters' => [ + 'keyword' => $keyword, + 'per_page' => $pageSize, + ], + 'summary' => [ + 'page_users' => $pageUsers, + 'users_with_online_ip' => $usersWithOnlineIp, + 'total_online_ips' => $totalOnlineIps, + 'current_page' => $paginator->currentPage(), + ], + 'adminHomeUrl' => url('/' . $securePath), ]); } - public function snapshot(Request $request, int $user) + private function formatDateTime(mixed $value): string { - 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 ($value instanceof DateTimeInterface) { + return $value->format('Y-m-d H:i:s'); } - 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] = '****'; + if (is_numeric($value)) { + $timestamp = (int) $value; + if ($timestamp > 0) { + return date('Y-m-d H:i:s', $timestamp); } - return implode(':', $parts); } - return $ip; + if (is_string($value) && trim($value) !== '') { + $timestamp = strtotime($value); + if ($timestamp !== false) { + return date('Y-m-d H:i:s', $timestamp); + } + } + + return '-'; } } diff --git a/plugins/UserOnlineDevices/Plugin.php b/plugins/UserOnlineDevices/Plugin.php index 6d772ea..8ad967d 100644 --- a/plugins/UserOnlineDevices/Plugin.php +++ b/plugins/UserOnlineDevices/Plugin.php @@ -2,35 +2,12 @@ namespace Plugin\UserOnlineDevices; -use App\Models\User; use App\Services\Plugin\AbstractPlugin; -use Illuminate\Support\Facades\URL; class Plugin extends AbstractPlugin { public function boot(): void { - $this->filter('user.subscribe.response', function ($user) { - if (!$user || empty($user['id'])) { - return $user; - } - - $ttlMinutes = max(1, (int) $this->getConfig('signed_url_ttl_minutes', 60)); - $userId = (int) $user['id']; - - $user['user_online_devices_enabled'] = true; - $user['user_online_devices_panel_url'] = URL::temporarySignedRoute( - 'user-online-devices.panel', - now()->addMinutes($ttlMinutes), - ['user' => $userId] - ); - $user['user_online_devices_snapshot_url'] = URL::temporarySignedRoute( - 'user-online-devices.snapshot', - now()->addMinutes($ttlMinutes), - ['user' => $userId] - ); - - return $user; - }); + // This plugin only exposes an admin page and does not need extra hooks. } } diff --git a/plugins/UserOnlineDevices/README.md b/plugins/UserOnlineDevices/README.md index 4abcf73..e053a83 100644 --- a/plugins/UserOnlineDevices/README.md +++ b/plugins/UserOnlineDevices/README.md @@ -1,25 +1,25 @@ -# User Online Devices Plugin +# UserOnlineDevices -This plugin provides online IP monitoring for Xboard users and administrators. +An admin-only Xboard plugin that shows all users' live online IP counts and active IP lists. ## Features -- Shows the current online device count based on Xboard's built-in `DeviceStateService` -- Provides an administrator page to inspect all users' online IP counts -- Provides an administrator API endpoint with paginated user online IP data -- Provides a temporary signed dashboard page for a single user snapshot +- Server-rendered admin page, no extra browser token parsing +- Search by user ID or email +- Page size switching +- Live online IP list from `DeviceStateService` +- Subscription name, last online time, and created time display -## Routes +## Route -- `GET /api/v1/user-online-devices/summary` -- `GET /api/v1/user-online-devices/panel-url` -- `GET /api/v2/{secure_path}/user-online-devices/users` -- `GET /{secure_path}/user-online-devices` -- `GET /user-online-devices/panel/{user}` (temporary signed URL) +After the plugin is installed and enabled, open: + +`/{secure_path}/user-online-devices` + +`secure_path` is your Xboard admin secure path. ## Notes -- This plugin reuses Xboard's existing real-time device state data from Redis. -- In current Xboard releases, plugin development is primarily backend-oriented, so this plugin ships standalone pages instead of patching the compiled SPA bundle directly. -- The "online device" count is effectively the number of unique online IPs reported by nodes. -- The administrator entry is `/{secure_path}/user-online-devices`, which reads the current admin login token and shows paginated user online IP data. +- The page is protected by Xboard's `admin` middleware +- The plugin does not expose a public user page +- Active IP data comes from node-reported device state in Redis diff --git a/plugins/UserOnlineDevices/config.json b/plugins/UserOnlineDevices/config.json index 2feea1d..2ea7c26 100644 --- a/plugins/UserOnlineDevices/config.json +++ b/plugins/UserOnlineDevices/config.json @@ -1,37 +1,19 @@ { "name": "User Online Devices", "code": "user_online_devices", - "version": "1.0.0", - "description": "Show users their current online devices and online IP list with a signed dashboard page.", + "version": "2.0.0", + "description": "Admin page for viewing all users' online IP counts and active IP lists.", "author": "OpenAI Codex", "type": "feature", "require": { "xboard": ">=1.0.0" }, "config": { - "refresh_interval_seconds": { + "default_page_size": { "type": "number", - "default": 15, - "label": "Refresh Interval", - "description": "Dashboard auto-refresh interval in seconds." - }, - "signed_url_ttl_minutes": { - "type": "number", - "default": 60, - "label": "Signed URL TTL", - "description": "Temporary dashboard link validity in minutes." - }, - "mask_ip": { - "type": "boolean", - "default": false, - "label": "Mask IP Address", - "description": "Whether to partially mask IP addresses on the dashboard." - }, - "show_active_sessions": { - "type": "boolean", - "default": true, - "label": "Show Active Sessions", - "description": "Whether to also show Sanctum login sessions." + "default": 20, + "label": "Default Page Size", + "description": "Default number of users shown per page on the admin monitor." } } } diff --git a/plugins/UserOnlineDevices/resources/views/admin-index.blade.php b/plugins/UserOnlineDevices/resources/views/admin-index.blade.php new file mode 100644 index 0000000..40680ea --- /dev/null +++ b/plugins/UserOnlineDevices/resources/views/admin-index.blade.php @@ -0,0 +1,381 @@ + + + +
+ + +| ID | +Subscription | +Online IP Count | +Online IP List | +Last Online | +Created | +|
|---|---|---|---|---|---|---|
| {{ $user->id }} | +{{ $user->email }} | +{{ $user->subscription_name }} | ++ + {{ $user->online_count_live ?? 0 }} + + | +
+ @if (!empty($user->online_devices))
+
+ @foreach ($user->online_devices as $ip)
+ {{ $ip }}
+ @endforeach
+
+ @else
+ No active IP
+ @endif
+ |
+ {{ $user->last_online_text }} | +{{ $user->created_text }} | +
|
+ No users found for the current filter.
+ |
+ ||||||
| ID | -Online IP Count | -Online IP List | -Last Online | -Created | -
|---|