From 26fffef653ecefb58f9ba1cd1c8c11db1d4b2a1d Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Tue, 7 Apr 2026 18:06:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + plugins/.gitignore | 6 - .../UserOnlineDevicesController.php | 273 ++---- plugins/UserOnlineDevices/Plugin.php | 25 +- plugins/UserOnlineDevices/README.md | 32 +- plugins/UserOnlineDevices/config.json | 30 +- .../resources/views/admin-index.blade.php | 381 +++++++++ .../resources/views/admin-users.blade.php | 793 ------------------ .../resources/views/panel.blade.php | 352 -------- .../resources/views/userstatus.blade.php | 550 ------------ plugins/UserOnlineDevices/routes/api.php | 21 +- plugins/UserOnlineDevices/routes/web.php | 18 +- 12 files changed, 489 insertions(+), 1996 deletions(-) create mode 100644 .gitignore create mode 100644 plugins/UserOnlineDevices/resources/views/admin-index.blade.php delete mode 100644 plugins/UserOnlineDevices/resources/views/admin-users.blade.php delete mode 100644 plugins/UserOnlineDevices/resources/views/panel.blade.php delete mode 100644 plugins/UserOnlineDevices/resources/views/userstatus.blade.php 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 @@ + + + + + + + User Online Devices + + + + +
+
+
Xboard Admin Plugin
+

All Users Online IP Monitor

+
+ This plugin page is rendered on the server side for administrators. It reads live device state from Xboard nodes and shows each user's current online IP count and active IP list without relying on browser-stored admin tokens. +
+
+ +
+
+
Users On Current Page
+
{{ $summary['page_users'] }}
+
Rows currently displayed after filtering
+
+
+
Users With Online IP
+
{{ $summary['users_with_online_ip'] }}
+
Users on this page whose live IP count is greater than zero
+
+
+
Total Online IP Count
+
{{ $summary['total_online_ips'] }}
+
Sum of all active IPs on the current page
+
+
+
Current Page
+
{{ $summary['current_page'] }}
+
Page {{ $users->currentPage() }} of {{ $users->lastPage() }}
+
+
+ +
+ + + + Reset + Back Admin +
+ +
+
+ + + + + + + + + + + + + + @forelse ($users as $user) + + + + + + + + + + @empty + + + + @endforelse + +
IDEmailSubscriptionOnline IP CountOnline IP ListLast OnlineCreated
{{ $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.
+
+
+ + +
+
+ + + diff --git a/plugins/UserOnlineDevices/resources/views/admin-users.blade.php b/plugins/UserOnlineDevices/resources/views/admin-users.blade.php deleted file mode 100644 index 008c4e3..0000000 --- a/plugins/UserOnlineDevices/resources/views/admin-users.blade.php +++ /dev/null @@ -1,793 +0,0 @@ - - - - - - - Admin Online IP Monitor - - - - -
-
-
Xboard Admin Monitor
-

All Users Online IP Count

-
- This page is intended for administrators. It shows the current online IP quantity of all users reported by Xboard nodes, and supports keyword search by user ID or email. -
-
- - - -
-
-
Users On Current Page
-
0
-
How many user rows are currently loaded
-
-
-
Users With Online IP
-
0
-
Users on this page whose online IP count is greater than zero
-
-
-
Total Online IP Count
-
0
-
Waiting for data
-
-
- -
- - - - - -
- -
-
- - - - - - - - - - - - -
IDEmailOnline IP CountOnline IP ListLast OnlineCreated
-
- - -
-
- - - - - diff --git a/plugins/UserOnlineDevices/resources/views/panel.blade.php b/plugins/UserOnlineDevices/resources/views/panel.blade.php deleted file mode 100644 index 2a5c9ea..0000000 --- a/plugins/UserOnlineDevices/resources/views/panel.blade.php +++ /dev/null @@ -1,352 +0,0 @@ - - - - - - - 在线设备与在线 IP - - - - -
-
-
Xboard Plugin Dashboard
-

在线设备与在线 IP

-
- 当前页面由 User Online Devices 插件生成,会自动刷新并显示该账号最近上报到 Xboard 的在线 IP 列表。 -
-
- -
-
-
账号
-
{{ $payload['user']['email'] }}
-
用户 ID: {{ $payload['user']['id'] }}
-
-
-
在线设备数
-
{{ $payload['user']['online_count'] }}
-
按实时在线 IP 去重统计
-
-
-
最后活跃
-
{{ $payload['user']['last_online_at'] ? date('Y-m-d H:i:s', $payload['user']['last_online_at']) : '暂无记录' }}
-
数据来源于 Xboard 设备状态服务
-
-
- -
-
-
-

在线 IP 列表

-
每 {{ $refreshInterval }} 秒刷新
-
-
- -
- -
-
-

登录会话

-
{{ $payload['meta']['show_active_sessions'] ? '已开启' : '已关闭' }}
-
-
-
-
-
- - - - - diff --git a/plugins/UserOnlineDevices/resources/views/userstatus.blade.php b/plugins/UserOnlineDevices/resources/views/userstatus.blade.php deleted file mode 100644 index 6dea1f2..0000000 --- a/plugins/UserOnlineDevices/resources/views/userstatus.blade.php +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - - User Status - - - - -
-
-
Xboard User Status
-

User Status

-
- This page is provided by the User Online Devices plugin. After login, it reads your current token from browser storage and shows your own online IP list, online count and account status. -
-
- - - -
-
-
User
-
-
-
Please log in first
-
-
-
Online IP Count
-
-
-
Unique real-time IP count reported by Xboard nodes
-
-
-
Last Online
-
-
-
Last device state update written by Xboard
-
-
-
Subscription
-
-
-
-
-
-
- -
-
-
-
Online IP List
-
Auto Refresh
-
-
-
- Current Xboard device state is counted by unique IP. If one device changes network, it may appear as a different online IP. -
-
- -
- -
- - -
-
- - - - - diff --git a/plugins/UserOnlineDevices/routes/api.php b/plugins/UserOnlineDevices/routes/api.php index 67dd5d3..45fa8e8 100644 --- a/plugins/UserOnlineDevices/routes/api.php +++ b/plugins/UserOnlineDevices/routes/api.php @@ -1,22 +1,3 @@ 'api/v1/user-online-devices', - 'middleware' => 'user', -], function () { - Route::get('/summary', [UserOnlineDevicesController::class, 'summary']); - Route::get('/panel-url', [UserOnlineDevicesController::class, 'panelUrl']); -}); - -Route::group([ - 'prefix' => 'api/v2/' . $securePath . '/user-online-devices', - 'middleware' => 'admin', -], function () { - Route::get('/users', [UserOnlineDevicesController::class, 'adminUsers']) - ->name('user-online-devices.admin-users-data'); -}); +// This plugin intentionally uses a server-rendered admin page only. diff --git a/plugins/UserOnlineDevices/routes/web.php b/plugins/UserOnlineDevices/routes/web.php index f51d952..50e1618 100644 --- a/plugins/UserOnlineDevices/routes/web.php +++ b/plugins/UserOnlineDevices/routes/web.php @@ -5,15 +5,9 @@ use Plugin\UserOnlineDevices\Controllers\UserOnlineDevicesController; $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); -Route::get('/' . $securePath . '/user-online-devices', [UserOnlineDevicesController::class, 'adminUsersPage']) - ->name('user-online-devices.admin-users'); - -Route::get('/user-online-devices/panel/{user}', [UserOnlineDevicesController::class, 'panel']) - ->whereNumber('user') - ->middleware('signed') - ->name('user-online-devices.panel'); - -Route::get('/user-online-devices/snapshot/{user}', [UserOnlineDevicesController::class, 'snapshot']) - ->whereNumber('user') - ->middleware('signed') - ->name('user-online-devices.snapshot'); +Route::group([ + 'prefix' => $securePath, + 'middleware' => ['admin'], +], function () { + Route::get('/user-online-devices', [UserOnlineDevicesController::class, 'index']); +});