修复TOKEN错误的问题
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 = '<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>
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user