修复TOKEN错误的问题

This commit is contained in:
CN-JS-HuiBai
2026-04-07 17:19:59 +08:00
parent 9599cc766d
commit 704d7df73e
3 changed files with 106 additions and 81 deletions

View File

@@ -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.

View File

@@ -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());
function isObject(value) {
return value && typeof value === 'object' && !Array.isArray(value);
}
if (trimmed[0] === '{' || trimmed[0] === '[') {
try {
collectFromUnknown(JSON.parse(trimmed), bucket);
} catch (error) {
}
}
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 collectFromUnknown(input, bucket) {
if (!input) return;
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;
for (const item of input) {
const token = extractTokenFromParsedValue(item);
if (token) return token;
}
return '';
}
if (typeof input === 'object') {
Object.keys(input).forEach(function (key) {
collectFromUnknown(input[key], bucket);
});
}
if (!isObject(input)) {
return '';
}
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);
}
if (typeof input.expired === 'boolean' && input.expired) {
return '';
}
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);
});
const expiresAt = Number(input.expires_at || input.expiresAt || input.expire_at || input.expireAt || 0);
if (expiresAt && expiresAt < Math.floor(Date.now() / 1000)) {
return '';
}
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) {
}
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 = '<div class="empty">No online IP detected currently.</div>';
document.getElementById('session-list').innerHTML = '<div class="empty">No active session entries available.</div>';
}
async function getJson(url, authorization) {
const response = await fetch(url, {
method: 'GET',
@@ -493,9 +503,14 @@
}
async function loadPageData() {
const authorization = await pickAuthorization();
const authorization = getAuthorization();
if (authorization !== lastAuthorization) {
resetView();
lastAuthorization = authorization;
}
if (!authorization) {
setError('No usable login token was found in browser storage. Please open the Xboard homepage first, complete login there, then refresh this page.');
setError('No usable login token was found in Xboard browser storage. Please open the Xboard homepage first, complete login there, then refresh this page.');
return;
}
@@ -512,6 +527,7 @@
renderInfo(results[1]);
renderSubscribe(results[2]);
} catch (error) {
resetView();
setError('Failed to load your user status. Please confirm the plugin is enabled and your login is still valid.');
}
}
@@ -526,6 +542,11 @@
loadPageData();
window.setInterval(loadPageData, REFRESH_MS);
window.addEventListener('storage', function (event) {
if (!event.key || TOKEN_KEYS.indexOf(event.key) !== -1) {
loadPageData();
}
});
})();
</script>
</body>

View File

@@ -3,6 +3,9 @@
use Illuminate\Support\Facades\Route;
use Plugin\UserOnlineDevices\Controllers\UserOnlineDevicesController;
Route::get('/userstatus', [UserOnlineDevicesController::class, 'userStatus'])
->name('user-online-devices.userstatus.short');
Route::group([
'prefix' => 'user-online-devices',
], function () {