diff --git a/Xboard/plugins/UserOnlineDevices/README.md b/Xboard/plugins/UserOnlineDevices/README.md index 3584a4f..f00a5be 100644 --- a/Xboard/plugins/UserOnlineDevices/README.md +++ b/Xboard/plugins/UserOnlineDevices/README.md @@ -14,6 +14,7 @@ This plugin adds a user-facing online device dashboard for Xboard. - `GET /api/v1/user-online-devices/summary` - `GET /api/v1/user-online-devices/panel-url` +- `GET /userstatus` - `GET /user-online-devices/userstatus` - `GET /user-online-devices/panel/{user}` (temporary signed URL) @@ -22,4 +23,4 @@ This plugin adds a user-facing online device dashboard for Xboard. - 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 a standalone user-side page instead of patching the compiled SPA bundle directly. - The "online device" count is effectively the number of unique online IPs reported by nodes. -- The easiest user entry is now `/user-online-devices/userstatus`, which reads the logged-in browser token and shows the current user's own status. +- The easiest user entry is now `/userstatus`, which reads the current Xboard login token and shows the current user's own status. diff --git a/Xboard/plugins/UserOnlineDevices/resources/views/userstatus.blade.php b/Xboard/plugins/UserOnlineDevices/resources/views/userstatus.blade.php index 4ff95fb..f2523bf 100644 --- a/Xboard/plugins/UserOnlineDevices/resources/views/userstatus.blade.php +++ b/Xboard/plugins/UserOnlineDevices/resources/views/userstatus.blade.php @@ -291,98 +291,95 @@ subscribe: '/api/v1/user/getSubscribe' }; const REFRESH_MS = 30000; + const TOKEN_KEYS = ['access_token', 'AccessToken', 'ACCESS_TOKEN']; + let lastAuthorization = ''; - function collectStringCandidates(value, bucket) { - if (!value || typeof value !== 'string') return; - - const trimmed = value.trim().replace(/^"|"$/g, ''); - if (!trimmed) return; - - bucket.add(trimmed); - - if (trimmed.startsWith('Bearer ')) { - bucket.add(trimmed.slice(7).trim()); - } - - if (trimmed[0] === '{' || trimmed[0] === '[') { - try { - collectFromUnknown(JSON.parse(trimmed), bucket); - } catch (error) { - } - } + function isObject(value) { + return value && typeof value === 'object' && !Array.isArray(value); } - function collectFromUnknown(input, bucket) { - if (!input) return; + function normalizeToken(candidate) { + if (typeof candidate !== 'string') return ''; + + const trimmed = candidate.trim().replace(/^"|"$/g, ''); + if (!trimmed) return ''; + + const raw = trimmed.replace(/^Bearer\s+/i, '').trim(); + if (raw.length < 20 || /\s/.test(raw)) return ''; + + return 'Bearer ' + raw; + } + + function extractTokenFromParsedValue(input) { + if (!input) return ''; if (typeof input === 'string') { - collectStringCandidates(input, bucket); - return; + return normalizeToken(input); } if (Array.isArray(input)) { - input.forEach(function (item) { - collectFromUnknown(item, bucket); - }); - return; - } - - if (typeof input === 'object') { - Object.keys(input).forEach(function (key) { - collectFromUnknown(input[key], bucket); - }); - } - } - - function collectTokensFromStorage(storage, bucket) { - if (!storage) return; - - for (let i = 0; i < storage.length; i += 1) { - const key = storage.key(i); - if (!key) continue; - collectStringCandidates(storage.getItem(key), bucket); - } - } - - function buildTokenCandidates() { - const bucket = new Set(); - collectTokensFromStorage(window.localStorage, bucket); - collectTokensFromStorage(window.sessionStorage, bucket); - - return Array.from(bucket) - .map(function (token) { - return token.startsWith('Bearer ') ? token : 'Bearer ' + token; - }) - .filter(function (token) { - const raw = token.replace(/^Bearer\s+/i, '').trim(); - return raw.length >= 20 && !/\s/.test(raw); - }); - } - - async function pickAuthorization() { - const candidates = buildTokenCandidates(); - - for (const authorization of candidates) { - try { - const response = await fetch(ENDPOINTS.info, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Authorization': authorization - }, - credentials: 'same-origin' - }); - - if (response.ok) { - return authorization; - } - } catch (error) { + for (const item of input) { + const token = extractTokenFromParsedValue(item); + if (token) return token; } + return ''; + } + + if (!isObject(input)) { + return ''; + } + + if (typeof input.expired === 'boolean' && input.expired) { + return ''; + } + + const expiresAt = Number(input.expires_at || input.expiresAt || input.expire_at || input.expireAt || 0); + if (expiresAt && expiresAt < Math.floor(Date.now() / 1000)) { + return ''; + } + + const directKeys = ['value', 'token', 'access_token', 'accessToken', 'data']; + for (const key of directKeys) { + const token = extractTokenFromParsedValue(input[key]); + if (token) return token; } return ''; } + function parseStoredToken(rawValue) { + if (!rawValue || typeof rawValue !== 'string') return ''; + + const direct = normalizeToken(rawValue); + if (direct) return direct; + + const trimmed = rawValue.trim(); + if (trimmed[0] !== '{' && trimmed[0] !== '[') { + return ''; + } + + try { + return extractTokenFromParsedValue(JSON.parse(trimmed)); + } catch (error) { + return ''; + } + } + + function readTokenFromStorage(storage) { + if (!storage) return ''; + + for (const key of TOKEN_KEYS) { + const token = parseStoredToken(storage.getItem(key)); + if (token) return token; + } + + return ''; + } + + function getAuthorization() { + return readTokenFromStorage(window.localStorage) || readTokenFromStorage(window.sessionStorage); + } + function formatTime(timestamp) { if (!timestamp) return '-'; const date = new Date(timestamp * 1000); @@ -414,6 +411,19 @@ box.textContent = ''; } + function resetView() { + document.getElementById('user-email').textContent = '-'; + document.getElementById('user-id').textContent = 'Please log in first'; + document.getElementById('online-count').textContent = '-'; + document.getElementById('last-online').textContent = '-'; + document.getElementById('plan-name').textContent = '-'; + document.getElementById('traffic-note').textContent = '-'; + document.getElementById('refresh-badge').textContent = 'Auto Refresh'; + document.getElementById('session-badge').textContent = 'Checking'; + document.getElementById('ip-list').innerHTML = '