修复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/summary`
|
||||||
- `GET /api/v1/user-online-devices/panel-url`
|
- `GET /api/v1/user-online-devices/panel-url`
|
||||||
|
- `GET /userstatus`
|
||||||
- `GET /user-online-devices/userstatus`
|
- `GET /user-online-devices/userstatus`
|
||||||
- `GET /user-online-devices/panel/{user}` (temporary signed URL)
|
- `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.
|
- 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.
|
- 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 "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'
|
subscribe: '/api/v1/user/getSubscribe'
|
||||||
};
|
};
|
||||||
const REFRESH_MS = 30000;
|
const REFRESH_MS = 30000;
|
||||||
|
const TOKEN_KEYS = ['access_token', 'AccessToken', 'ACCESS_TOKEN'];
|
||||||
|
let lastAuthorization = '';
|
||||||
|
|
||||||
function collectStringCandidates(value, bucket) {
|
function isObject(value) {
|
||||||
if (!value || typeof value !== 'string') return;
|
return value && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
|
||||||
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 collectFromUnknown(input, bucket) {
|
function normalizeToken(candidate) {
|
||||||
if (!input) return;
|
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') {
|
if (typeof input === 'string') {
|
||||||
collectStringCandidates(input, bucket);
|
return normalizeToken(input);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
input.forEach(function (item) {
|
for (const item of input) {
|
||||||
collectFromUnknown(item, bucket);
|
const token = extractTokenFromParsedValue(item);
|
||||||
});
|
if (token) return token;
|
||||||
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) {
|
|
||||||
}
|
}
|
||||||
|
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 '';
|
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) {
|
function formatTime(timestamp) {
|
||||||
if (!timestamp) return '-';
|
if (!timestamp) return '-';
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp * 1000);
|
||||||
@@ -414,6 +411,19 @@
|
|||||||
box.textContent = '';
|
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) {
|
async function getJson(url, authorization) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -493,9 +503,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPageData() {
|
async function loadPageData() {
|
||||||
const authorization = await pickAuthorization();
|
const authorization = getAuthorization();
|
||||||
|
if (authorization !== lastAuthorization) {
|
||||||
|
resetView();
|
||||||
|
lastAuthorization = authorization;
|
||||||
|
}
|
||||||
|
|
||||||
if (!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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,6 +527,7 @@
|
|||||||
renderInfo(results[1]);
|
renderInfo(results[1]);
|
||||||
renderSubscribe(results[2]);
|
renderSubscribe(results[2]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
resetView();
|
||||||
setError('Failed to load your user status. Please confirm the plugin is enabled and your login is still valid.');
|
setError('Failed to load your user status. Please confirm the plugin is enabled and your login is still valid.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,6 +542,11 @@
|
|||||||
|
|
||||||
loadPageData();
|
loadPageData();
|
||||||
window.setInterval(loadPageData, REFRESH_MS);
|
window.setInterval(loadPageData, REFRESH_MS);
|
||||||
|
window.addEventListener('storage', function (event) {
|
||||||
|
if (!event.key || TOKEN_KEYS.indexOf(event.key) !== -1) {
|
||||||
|
loadPageData();
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Plugin\UserOnlineDevices\Controllers\UserOnlineDevicesController;
|
use Plugin\UserOnlineDevices\Controllers\UserOnlineDevicesController;
|
||||||
|
|
||||||
|
Route::get('/userstatus', [UserOnlineDevicesController::class, 'userStatus'])
|
||||||
|
->name('user-online-devices.userstatus.short');
|
||||||
|
|
||||||
Route::group([
|
Route::group([
|
||||||
'prefix' => 'user-online-devices',
|
'prefix' => 'user-online-devices',
|
||||||
], function () {
|
], function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user