From 8cca428d89a2ce3e95b8af7cd413e863a6a0cf7c Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sat, 18 Apr 2026 21:55:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=8C=96UI=20JS=20CONFIG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/admin/app.js | 52 +- frontend/admin/main.js | 20 +- frontend/theme/Nebula/assets/app.js | 812 +++++++++++------------ frontend/theme/Nebula/assets/enhancer.js | 146 ++-- internal/handler/common.go | 6 +- 5 files changed, 518 insertions(+), 518 deletions(-) diff --git a/frontend/admin/app.js b/frontend/admin/app.js index c325c15..9ab72fb 100644 --- a/frontend/admin/app.js +++ b/frontend/admin/app.js @@ -45,20 +45,20 @@ }; const ROUTE_META = { - overview: { title: "总览", description: "查看收入、用户和流量概况。" }, - "dashboard-node": { title: "节点状态", description: "查看节点在线状态、负载和推送情况。" }, - "node-manage": { title: "节点管理", description: "管理服务节点、可见性以及父子节点关系。" }, - "node-group": { title: "权限组", description: "管理节点权限组和用户分组映射。" }, - "node-route": { title: "路由规则", description: "维护节点路由匹配规则。" }, - "plan-manage": { title: "套餐管理", description: "维护套餐、流量和价格配置。" }, - "order-manage": { title: "订单管理", description: "处理待支付和已支付订单。" }, - "coupon-manage": { title: "优惠券", description: "创建和维护优惠券信息。" }, - "user-manage": { title: "用户管理", description: "查看用户订阅、流量和封禁状态。" }, - "ticket-manage": { title: "工单中心", description: "查看用户工单和处理状态。" }, - realname: { title: "实名认证", description: "审核实名记录和同步状态。" }, - "user-online-devices": { title: "在线设备", description: "查看用户在线 IP 和设备分布。" }, - "user-ipv6-subscription": { title: "IPv6 子账号", description: "管理 IPv6 阴影账号与密码同步。" }, - "system-config": { title: "系统设置", description: "编辑站点、订阅和安全参数。" } + overview: { title: '总览', description: '查看收入、用户和流量概况。' }, + 'dashboard-node': { title: '节点状态', description: '查看节点在线状态、负载和推送情况。' }, + 'node-manage': { title: '节点管理', description: '管理服务节点、可见性以及父子节点关系。' }, + 'node-group': { title: '权限组', description: '管理节点权限组和用户分组映射。' }, + 'node-route': { title: '路由规则', description: '维护节点路由匹配规则。' }, + 'plan-manage': { title: '套餐管理', description: '维护套餐、流量和价格配置。' }, + 'order-manage': { title: '订单管理', description: '处理待支付和已支付订单。' }, + 'coupon-manage': { title: '优惠券', description: '创建和维护优惠券信息。' }, + 'user-manage': { title: '用户管理', description: '查看用户订阅、流量和封禁状态。' }, + 'ticket-manage': { title: '工单中心', description: '查看用户工单和处理状态。' }, + realname: { title: '实名认证', description: '审核实名记录和同步状态。' }, + 'user-online-devices': { title: '在线设备', description: '查看用户在线 IP 和设备分布。' }, + 'user-ipv6-subscription': { title: 'IPv6 子账号', description: '管理 IPv6 阴影账号与密码同步。' }, + 'system-config': { title: '系统设置', description: '编辑站点、订阅和安全参数。' } }; state.route = normalizeRoute(readRoute()); @@ -66,14 +66,14 @@ boot(); async function boot() { - window.addEventListener("hashchange", async function () { + window.addEventListener('hashchange', async function () { state.route = normalizeRoute(readRoute()); state.modal = null; await hydrateRoute(); }); - root.addEventListener("click", onClick); - root.addEventListener("submit", onSubmit); + root.addEventListener('click', onClick); + root.addEventListener('submit', onSubmit); // Initialize shell and modal containers root.innerHTML = '
'; @@ -94,7 +94,7 @@ async function loadBootstrap() { try { setBusy(true); - const loginCheck = unwrap(await request("/api/v1/user/checkLogin", { method: "GET" })); + const loginCheck = unwrap(await request('/api/v1/user/checkLogin', { method: 'GET' })); if (!loginCheck || !loginCheck.is_admin) { clearSession(); return; @@ -102,15 +102,15 @@ state.user = loginCheck; const [config, system] = await Promise.all([ - request(cfg.api.adminConfig, { method: "GET" }), - request(cfg.api.systemStatus, { method: "GET" }) + request(cfg.api.adminConfig, { method: 'GET' }), + request(cfg.api.systemStatus, { method: 'GET' }) ]); state.config = unwrap(config) || {}; state.system = unwrap(system) || {}; } catch (error) { - console.error("bootstrap failed", error); + console.error('bootstrap failed', error); clearSession(); - show(error.message || "管理端初始化失败", "error"); + show(error.message || '管理端初始化失败', 'error'); } finally { setBusy(false); render(); @@ -127,23 +127,23 @@ setBusy(true); const page = getCurrentPage(); - if (state.route === "overview") { + if (state.route === 'overview') { state.dashboard = unwrap(await request(cfg.api.dashboardSummary)); - } else if (state.route === "dashboard-node") { + } else if (state.route === 'dashboard-node') { const [dashboard, nodes] = await Promise.all([ request(cfg.api.dashboardSummary), request(cfg.api.serverNodes) ]); state.dashboard = unwrap(dashboard) || {}; state.nodes = toArray(unwrap(nodes)); - } else if (state.route === "node-manage") { + } else if (state.route === 'node-manage') { const [nodes, groups] = await Promise.all([ request(cfg.api.serverNodes), request(cfg.api.serverGroups) ]); state.nodes = toArray(unwrap(nodes)); state.groups = toArray(unwrap(groups)); - } else if (state.route === "node-group") { + } else if (state.route === 'node-group') { state.groups = toArray(unwrap(await request(cfg.api.serverGroups))); } else if (state.route === "node-route") { state.routes = toArray(unwrap(await request(cfg.api.serverRoutes))); diff --git a/frontend/admin/main.js b/frontend/admin/main.js index a38caf0..bb589e8 100644 --- a/frontend/admin/main.js +++ b/frontend/admin/main.js @@ -1,11 +1,11 @@ const settings = window.settings || {}; const assetNonce = window.__ADMIN_ASSET_NONCE__ || String(Date.now()); -const securePath = String(settings.secure_path || "admin").replace(/^\/+/, ""); +const securePath = String(settings.secure_path || 'admin').replace(/^\/+/, ''); const adminBase = `/api/v2/${securePath}`; window.ADMIN_APP_CONFIG = { - title: settings.title || "XBoard Admin", - version: settings.version || "1.0.0", + title: settings.title || 'XBoard Admin', + version: settings.version || '1.0.0', securePath, baseUrl: settings.base_url || window.location.origin, api: { @@ -27,28 +27,28 @@ window.ADMIN_APP_CONFIG = { }, }; -document.documentElement.dataset.adminExecutionMode = "main-app"; +document.documentElement.dataset.adminExecutionMode = 'main-app'; function showBootError(error) { - console.error("Failed to boot admin app", error); - const root = document.getElementById("admin-app"); + console.error('Failed to boot admin app', error); + const root = document.getElementById('admin-app'); if (root) { root.innerHTML = `
Admin app failed to load.
${String( - error && error.message ? error.message : error || "Unknown error", + error && error.message ? error.message : error || 'Unknown error', )}
`; } } -window.addEventListener("error", (event) => { +window.addEventListener('error', (event) => { if (!event || !event.error) { return; } showBootError(event.error); }); -const script = document.createElement("script"); +const script = document.createElement('script'); script.src = `/admin-assets/app.js?v=${encodeURIComponent(assetNonce)}`; script.defer = true; -script.onerror = () => showBootError(new Error("Failed to load /admin-assets/app.js")); +script.onerror = () => showBootError(new Error('Failed to load /admin-assets/app.js')); document.body.appendChild(script); diff --git a/frontend/theme/Nebula/assets/app.js b/frontend/theme/Nebula/assets/app.js index b035346..5e54fab 100644 --- a/frontend/theme/Nebula/assets/app.js +++ b/frontend/theme/Nebula/assets/app.js @@ -1,22 +1,22 @@ (function () { "use strict"; - var theme = window.NEBULA_THEME || {}; - var app = document.getElementById("app"); - var loader = document.getElementById("nebula-loader"); - var themeMediaQuery = getThemeMediaQuery(); + const theme = window.NEBULA_THEME || {}; + const app = document.getElementById('app'); + const loader = document.getElementById('nebula-loader'); + const themeMediaQuery = getThemeMediaQuery(); if (!app) { return; } - var state = { - mode: "login", + const state = { + mode: 'login', loading: true, - message: "", - messageType: "", + message: '', + messageType: '', themePreference: getStoredThemePreference(), - themeMode: "dark", + themeMode: 'dark', uplinkMbps: null, currentRoute: getCurrentRoute(), nodePage: 1, @@ -41,7 +41,7 @@ realNameVerification: null, appConfig: null, isSidebarOpen: false, - serverSearch: "", + serverSearch: '', cachedSubNodes: null, ipv6CachedSubNodes: null }; @@ -55,13 +55,13 @@ window.setInterval(loadUplinkMetric, 5000); try { - var verify = getVerifyToken(); + const verify = getVerifyToken(); if (verify) { - var verifyResponse = await fetchJson("/api/v1/passport/auth/token2Login?verify=" + encodeURIComponent(verify), { - method: "GET", + const verifyResponse = await fetchJson('/api/v1/passport/auth/token2Login?verify=' + encodeURIComponent(verify), { + method: 'GET', auth: false }); - var verifyPayload = unwrap(verifyResponse); + const verifyPayload = unwrap(verifyResponse); if (verifyPayload && verifyPayload.auth_data) { saveToken(verifyPayload.auth_data); clearVerifyToken(); @@ -101,16 +101,16 @@ } try { - var results = await Promise.all([ - fetchJson("/api/v1/user/info", { method: "GET" }), - fetchJson("/api/v1/user/getSubscribe", { method: "GET" }), - fetchJson("/api/v1/user/getStat", { method: "GET" }), - fetchJson("/api/v1/user/server/fetch", { method: "GET" }), - fetchJson("/api/v1/user/ticket/fetch", { method: "GET" }), + const results = await Promise.all([ + fetchJson('/api/v1/user/info', { method: 'GET' }), + fetchJson('/api/v1/user/getSubscribe', { method: 'GET' }), + fetchJson('/api/v1/user/getStat', { method: 'GET' }), + fetchJson('/api/v1/user/server/fetch', { method: 'GET' }), + fetchJson('/api/v1/user/ticket/fetch', { method: 'GET' }), fetchSessionOverview(), - fetchJson("/api/v1/user/comm/config", { method: "GET" }), + fetchJson('/api/v1/user/comm/config', { method: 'GET' }), fetchRealNameVerificationStatus(), - fetchJson("/api/v1/user/user-add-ipv6-subscription/check", { method: "GET" }) + fetchJson('/api/v1/user/user-add-ipv6-subscription/check', { method: 'GET' }) ]); state.user = unwrap(results[0]); @@ -126,10 +126,10 @@ if (state.ipv6AuthToken) { try { - var ipv6Results = await Promise.all([ - fetchJson("/api/v1/user/info", { method: "GET", ipv6: true }), - fetchJson("/api/v1/user/getSubscribe", { method: "GET", ipv6: true }), - fetchJson("/api/v1/user/server/fetch", { method: "GET", ipv6: true }), + const ipv6Results = await Promise.all([ + fetchJson('/api/v1/user/info', { method: 'GET', ipv6: true }), + fetchJson('/api/v1/user/getSubscribe', { method: 'GET', ipv6: true }), + fetchJson('/api/v1/user/server/fetch', { method: 'GET', ipv6: true }), fetchSessionOverview(true) ]); state.ipv6User = unwrap(ipv6Results[0]); @@ -138,9 +138,9 @@ state.ipv6SessionOverview = ipv6Results[3]; } catch (e) { if (handleIpv6AuthFailure(e)) { - console.warn("Nebula: cleared stale IPv6 session after unauthorized response"); + console.warn('Nebula: cleared stale IPv6 session after unauthorized response'); } - console.error("Failed to load IPv6 dashboard data", e); + console.error('Failed to load IPv6 dashboard data', e); } } @@ -150,41 +150,41 @@ } catch (error) { state.loading = false; if (error.status === 401 || error.status === 403) { - var hadSession = Boolean(state.authToken); + const hadSession = Boolean(state.authToken); clearToken(); resetDashboard(); - showMessage(hadSession ? "Session expired. Please sign in again." : "", hadSession ? "error" : ""); + showMessage(hadSession ? 'Session expired. Please sign in again.' : '', hadSession ? 'error' : ''); return false; } - showMessage(error.message || "Failed to load dashboard", "error"); + showMessage(error.message || 'Failed to load dashboard', 'error'); return false; } } async function fetchSessionOverview(isIpv6) { try { - var response = await fetchJson("/api/v1/user/user-online-devices/get-ip", { method: "GET", ipv6: !!isIpv6 }); - var ipPayload = unwrap(response) || {}; + const response = await fetchJson('/api/v1/user/user-online-devices/get-ip', { method: 'GET', ipv6: !!isIpv6 }); + const ipPayload = unwrap(response) || {}; - var reportedIps = ipPayload.ips || []; + const reportedIps = ipPayload.ips || []; if (!isIpv6) { state.reportedIps = reportedIps; } - - var overview = ipPayload.session_overview || { sessions: [], online_ips: [] }; + + const overview = ipPayload.session_overview || { sessions: [], online_ips: [] }; if (!isIpv6) { overview.online_ips = state.reportedIps; } - + return buildSessionOverview(overview.sessions, overview, !!isIpv6); } catch (error) { if (isIpv6 && handleIpv6AuthFailure(error)) { return null; } - console.error("Nebula: Failed to synchronize online status", error); + console.error('Nebula: Failed to synchronize online status', error); // Fallback for sessions if plugin fails try { - var sessionsFallback = unwrap(await fetchJson("/api/v1/user/getActiveSession", { method: "GET", ipv6: !!isIpv6 })) || []; + const sessionsFallback = unwrap(await fetchJson('/api/v1/user/getActiveSession', { method: 'GET', ipv6: !!isIpv6 })) || []; return buildSessionOverview(sessionsFallback, {}, !!isIpv6); } catch (e) { if (isIpv6 && handleIpv6AuthFailure(e)) { @@ -823,43 +823,43 @@ } // Trojan - if (link.indexOf("trojan://") === 0) { - var body = link.split("#")[0].slice(9); - var main = body.split("?")[0]; - var query = body.split("?")[1] || ""; - var parts = main.split("@"); - var password = parts[0]; - var serverParts = (parts[1] || "").split(":"); + if (link.indexOf('trojan://') === 0) { + const body = link.split('#')[0].slice(9); + const main = body.split('?')[0]; + const query = body.split('?')[1] || ''; + const parts = main.split('@'); + const password = parts[0]; + const serverParts = (parts[1] || '').split(':'); - var params = {}; - query.split("&").forEach(function (pair) { - var p = pair.split("="); - params[p[0]] = decodeURIComponent(p[1] || ""); + const params = {}; + query.split('&').forEach((pair) => { + const p = pair.split('='); + params[p[0]] = decodeURIComponent(p[1] || ''); }); - var yaml = [ - "- name: \"" + (remark || "Trojan Node") + "\"", - " type: trojan", - " server: " + serverParts[0], - " port: " + (parseInt(serverParts[1]) || 443), - " password: " + password, - " udp: true", - " sni: " + (params.sni || params.peer || ""), - " skip-cert-verify: true" + const yaml = [ + '- name: "' + (remark || 'Trojan Node') + '"', + ' type: trojan', + ' server: ' + serverParts[0], + ' port: ' + (parseInt(serverParts[1]) || 443), + ' password: ' + password, + ' udp: true', + ' sni: ' + (params.sni || params.peer || ''), + ' skip-cert-verify: true' ]; - if (params.type === "ws") { - yaml.push(" ws-opts:"); - yaml.push(" path: " + (params.path || "/")); - if (params.host) yaml.push(" headers:"); - if (params.host) yaml.push(" Host: " + params.host); + if (params.type === 'ws') { + yaml.push(' ws-opts:'); + yaml.push(' path: ' + (params.path || '/')); + if (params.host) yaml.push(' headers:'); + if (params.host) yaml.push(' Host: ' + params.host); } - if (params.type === "grpc") { - yaml.push(" grpc-opts:"); - yaml.push(" grpc-service-name: " + (params.serviceName || "")); + if (params.type === 'grpc') { + yaml.push(' grpc-opts:'); + yaml.push(' grpc-service-name: ' + (params.serviceName || '')); } - return yaml.join("\n"); + return yaml.join('\n'); } return link; @@ -867,27 +867,27 @@ function utf8Base64Decode(str) { try { - var b64 = str.trim().replace(/-/g, "+").replace(/_/g, "/"); - while (b64.length % 4 !== 0) b64 += "="; - - var binary = atob(b64); + let b64 = str.trim().replace(/-/g, '+').replace(/_/g, '/'); + while (b64.length % 4 !== 0) b64 += '='; + + const binary = atob(b64); try { - if (typeof TextDecoder !== "undefined") { - var bytes = new Uint8Array(binary.length); - for (var i = 0; i < binary.length; i++) { + if (typeof TextDecoder !== 'undefined') { + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } - return new TextDecoder("utf-8").decode(bytes); + return new TextDecoder('utf-8').decode(bytes); } - - return decodeURIComponent(binary.split("").map(function (c) { - return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); - }).join("")); + + return decodeURIComponent(binary.split('').map((c) => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); } catch (e) { return binary; } } catch (e) { - throw new Error("Invalid base64 content: " + e.message); + throw new Error('Invalid base64 content: ' + e.message); } } @@ -912,24 +912,24 @@ if (state.authToken) { return; } - var baseUrl = getMetricsBaseUrl(); + const baseUrl = getMetricsBaseUrl(); if (!baseUrl) { return; } try { - var response = await fetch(baseUrl + "/api/metrics/overview", { - method: "GET", + const response = await fetch(baseUrl + '/api/metrics/overview', { + method: 'GET', headers: { - "X-Requested-With": "XMLHttpRequest" + 'X-Requested-With': 'XMLHttpRequest' } }); - var payload = await response.json(); - var network = (payload && payload.network) || {}; - var tx = Number(network.tx || 0); - var total = tx; + const payload = await response.json(); + const network = (payload && payload.network) || {}; + const tx = Number(network.tx || 0); + const total = tx; if (total > 0) { state.uplinkMbps = (total * 8) / 1000000; - var el = document.getElementById("nebula-hero-metric-stack"); + const el = document.getElementById('nebula-hero-metric-stack'); if (el) { el.innerHTML = renderUplinkMetricStack(); } else if (!state.authToken || !state.user) { @@ -942,37 +942,37 @@ } function onSubmit(event) { - var form = event.target; - if (!form.matches("[data-form]")) { + const form = event.target; + if (!form.matches('[data-form]')) { return; } event.preventDefault(); - var formType = form.getAttribute("data-form"); - var data = Object.fromEntries(new FormData(form).entries()); - var submitButton = form.querySelector("[type='submit']"); + const formType = form.getAttribute('data-form'); + const data = Object.fromEntries(new FormData(form).entries()); + const submitButton = form.querySelector('[type="submit"]'); if (submitButton) { submitButton.disabled = true; } - if (formType === "create-ticket") { - fetchJson("/api/v1/user/ticket/save", { - method: "POST", + if (formType === 'create-ticket') { + fetchJson('/api/v1/user/ticket/save', { + method: 'POST', body: { - subject: data.subject || "", - level: data.level || "0", - message: data.message || "" + subject: data.subject || '', + level: data.level || '0', + message: data.message || '' } - }).then(function () { - showMessage("工单已创建", "success"); + }).then(() => { + showMessage('工单已创建', 'success'); state.selectedTicketId = null; state.selectedTicket = null; return loadDashboard(true); - }).then(render).catch(function (error) { - showMessage(error.message || "工单创建失败", "error"); + }).then(render).catch((error) => { + showMessage(error.message || '工单创建失败', 'error'); render(); - }).finally(function () { + }).finally(() => { if (submitButton) { submitButton.disabled = false; } @@ -980,28 +980,28 @@ return; } - if (formType === "real-name-verification") { - fetchJson("/api/v1/user/real-name-verification/submit", { - method: "POST", + if (formType === 'real-name-verification') { + fetchJson('/api/v1/user/real-name-verification/submit', { + method: 'POST', body: { - real_name: data.real_name || "", - identity_no: data.identity_no || "" + real_name: data.real_name || '', + identity_no: data.identity_no || '' } - }).then(function (response) { - var payload = unwrap(response) || {}; - var verification = payload.verification || payload; + }).then((response) => { + const payload = unwrap(response) || {}; + const verification = payload.verification || payload; state.realNameVerification = verification; showMessage( - verification && verification.status === "approved" - ? "实名认证已通过。" - : "认证信息已提交,请等待审核。", - "success" + verification && verification.status === 'approved' + ? '实名认证已通过。' + : '认证信息已提交,请等待审核。', + 'success' ); return loadDashboard(true); - }).then(render).catch(function (error) { - showMessage(error.message || "认证提交失败", "error"); + }).then(render).catch((error) => { + showMessage(error.message || '认证提交失败', 'error'); render(); - }).finally(function () { + }).finally(() => { if (submitButton) { submitButton.disabled = false; } @@ -1009,24 +1009,24 @@ return; } - if (formType === "change-password") { + if (formType === 'change-password') { if (data.new_password !== data.confirm_password) { - showMessage("两次输入的新密码不一致", "error"); + showMessage('两次输入的新密码不一致', 'error'); if (submitButton) submitButton.disabled = false; return; } - fetchJson("/api/v1/user/changePassword", { - method: "POST", + fetchJson('/api/v1/user/changePassword', { + method: 'POST', body: { - old_password: data.old_password || "", - new_password: data.new_password || "" + old_password: data.old_password || '', + new_password: data.new_password || '' } - }).then(function () { - showMessage("密码已修改,建议在 IPv6 节点同步新密码", "success"); + }).then(() => { + showMessage('密码已修改,建议在 IPv6 节点同步新密码', 'success'); form.reset(); - }).catch(function (error) { - showMessage(error.message || "密码修改失败", "error"); - }).finally(function () { + }).catch((error) => { + showMessage(error.message || '密码修改失败', 'error'); + }).finally(() => { if (submitButton) { submitButton.disabled = false; } @@ -1034,22 +1034,22 @@ return; } - if (formType === "forget-password") { - fetchJson("/api/v1/passport/auth/forget", { - method: "POST", + if (formType === 'forget-password') { + fetchJson('/api/v1/passport/auth/forget', { + method: 'POST', auth: false, body: { email: data.email, email_code: data.email_code, password: data.password } - }).then(function () { - showMessage("密码已重置,请使用新密码登录", "success"); - state.mode = "login"; + }).then(() => { + showMessage('密码已重置,请使用新密码登录', 'success'); + state.mode = 'login'; render(); - }).catch(function (error) { - showMessage(error.message || "重置失败", "error"); - }).finally(function () { + }).catch((error) => { + showMessage(error.message || '重置失败', 'error'); + }).finally(() => { if (submitButton) { submitButton.disabled = false; } @@ -1057,48 +1057,48 @@ return; } - fetchJson(formType === "register" ? "/api/v1/passport/auth/register" : "/api/v1/passport/auth/login", { - method: "POST", + fetchJson(formType === 'register' ? '/api/v1/passport/auth/register' : '/api/v1/passport/auth/login', { + method: 'POST', auth: false, body: data - }).then(function (response) { - var payload = unwrap(response); + }).then((response) => { + const payload = unwrap(response); if (!payload || !payload.auth_data) { - throw new Error("Authentication succeeded but token payload is missing"); + throw new Error('Authentication succeeded but token payload is missing'); } saveToken(payload.auth_data); state.authToken = getStoredToken(); - showMessage(formType === "register" ? "Account created" : "Signed in", "success"); - return loadDashboard(true).then(function () { - if (formType !== "login" || !data.email || !data.password || state.ipv6AuthToken) { + showMessage(formType === 'register' ? 'Account created' : 'Signed in', 'success'); + return loadDashboard(true).then(() => { + if (formType !== 'login' || !data.email || !data.password || state.ipv6AuthToken) { return null; } if (!(state.ipv6Eligibility && state.ipv6Eligibility.is_active)) { return null; } - var ipv6Email = data.email.replace("@", "-ipv6@"); - return fetchJson("/api/v1/passport/auth/login", { - method: "POST", + const ipv6Email = data.email.replace('@', '-ipv6@'); + return fetchJson('/api/v1/passport/auth/login', { + method: 'POST', auth: false, body: Object.assign({}, data, { email: ipv6Email }) - }).then(function (ipv6Response) { - var ipv6Payload = unwrap(ipv6Response); + }).then((ipv6Response) => { + const ipv6Payload = unwrap(ipv6Response); if (ipv6Payload && ipv6Payload.auth_data) { saveIpv6Token(ipv6Payload.auth_data); state.ipv6AuthToken = getStoredIpv6Token(); } - }).catch(function () { + }).catch(() => { return null; - }).then(function () { + }).then(() => { return loadDashboard(true); }); }); - }).then(render).catch(function (error) { - showMessage(error.message || "Unable to continue", "error"); + }).then(render).catch((error) => { + showMessage(error.message || 'Unable to continue', 'error'); render(); - }).finally(function () { + }).finally(() => { if (submitButton) { submitButton.disabled = false; } @@ -1107,23 +1107,23 @@ function handleResetSecurity(actionEl, isIpv6) { showNebulaModal({ - title: "安全提醒", - content: "确定要重置订阅令牌吗?重置令牌后,原本的订阅地址将失效,你需要重新获取并配置订阅。", - confirmText: "确认重置", - onConfirm: function () { + title: '安全提醒', + content: '确定要重置订阅令牌吗?重置令牌后,原本的订阅地址将失效,你需要重新获取并配置订阅。', + confirmText: '确认重置', + onConfirm: () => { actionEl.disabled = true; - fetchJson("/api/v1/user/resetSecurity", { method: "GET", ipv6: !!isIpv6 }).then(function (response) { - var subscribeUrl = unwrap(response); - var sub = isIpv6 ? state.ipv6Subscribe : state.subscribe; + fetchJson('/api/v1/user/resetSecurity', { method: 'GET', ipv6: !!isIpv6 }).then((response) => { + const subscribeUrl = unwrap(response); + const sub = isIpv6 ? state.ipv6Subscribe : state.subscribe; if (sub && subscribeUrl) { sub.subscribe_url = subscribeUrl; } - showMessage((isIpv6 ? "IPv6 " : "") + "订阅令牌已重置", "success"); + showMessage((isIpv6 ? 'IPv6 ' : '') + '订阅令牌已重置', 'success'); render(); - }).catch(function (error) { - showMessage(error.message || "重置失败", "error"); + }).catch((error) => { + showMessage(error.message || '重置失败', 'error'); render(); - }).finally(function () { + }).finally(() => { actionEl.disabled = false; }); } @@ -1131,68 +1131,68 @@ } function handleRemoveSession(actionEl) { - var sessionId = actionEl.getAttribute("data-session-id"); + const sessionId = actionEl.getAttribute('data-session-id'); if (!sessionId) { return; } actionEl.disabled = true; - fetchJson("/api/v1/user/removeActiveSession", { - method: "POST", + fetchJson('/api/v1/user/removeActiveSession', { + method: 'POST', body: { session_id: sessionId } - }).then(function () { - showMessage("Session revoked", "success"); + }).then(() => { + showMessage('Session revoked', 'success'); return loadDashboard(true); - }).then(render).catch(function (error) { - showMessage(error.message || "Unable to revoke session", "error"); + }).then(render).catch((error) => { + showMessage(error.message || 'Unable to revoke session', 'error'); render(); - }).finally(function () { + }).finally(() => { actionEl.disabled = false; }); } function handleRemoveOtherSessions(actionEl) { - var sessions = ((state.sessionOverview && state.sessionOverview.sessions) || []).filter(function (session) { + const sessions = ((state.sessionOverview && state.sessionOverview.sessions) || []).filter((session) => { return !session.is_current; }); if (!sessions.length) { - showMessage("No other sessions to sign out.", "success"); + showMessage('No other sessions to sign out.', 'success'); render(); return; } actionEl.disabled = true; - Promise.all(sessions.map(function (session) { - return fetchJson("/api/v1/user/removeActiveSession", { - method: "POST", + Promise.all(sessions.map((session) => { + return fetchJson('/api/v1/user/removeActiveSession', { + method: 'POST', body: { session_id: session.id } }); - })).then(function () { - showMessage("Signed out all other sessions.", "success"); + })).then(() => { + showMessage('Signed out all other sessions.', 'success'); return loadDashboard(true); - }).then(render).catch(function (error) { - showMessage(error.message || "Unable to sign out other sessions", "error"); + }).then(render).catch((error) => { + showMessage(error.message || 'Unable to sign out other sessions', 'error'); render(); - }).finally(function () { + }).finally(() => { actionEl.disabled = false; }); } function handleOpenTicketDetail(actionEl) { - var ticketId = actionEl.getAttribute("data-ticket-id"); + const ticketId = actionEl.getAttribute('data-ticket-id'); if (!ticketId) { return; } actionEl.disabled = true; - fetchJson("/api/v1/user/ticket/fetch?id=" + encodeURIComponent(ticketId), { method: "GET" }).then(function (response) { + fetchJson('/api/v1/user/ticket/fetch?id=' + encodeURIComponent(ticketId), { method: 'GET' }).then((response) => { state.selectedTicketId = ticketId; state.selectedTicket = unwrap(response); render(); - }).catch(function (error) { - showMessage(error.message || "工单详情加载失败", "error"); + }).catch((error) => { + showMessage(error.message || '工单详情加载失败', 'error'); render(); - }).finally(function () { + }).finally(() => { actionEl.disabled = false; }); } @@ -1219,8 +1219,8 @@ return; } - var isLoggedIn = !!(state.authToken && state.user); - var targetLayout = isLoggedIn ? "dashboard" : "login"; + const isLoggedIn = !!(state.authToken && state.user); + const targetLayout = isLoggedIn ? 'dashboard' : 'login'; // If layout type changed (e.g. Login -> Dashboard), do a full shell render if (currentLayout !== targetLayout) { @@ -1232,21 +1232,21 @@ // If already in Dashboard, only update the content area and sidebar state if (isLoggedIn) { - var usedTraffic = ((state.subscribe && state.subscribe.u) || 0) + ((state.subscribe && state.subscribe.d) || 0); - var totalTraffic = (state.subscribe && state.subscribe.transfer_enable) || 0; - var remainingTraffic = Math.max(totalTraffic - usedTraffic, 0); - var percent = totalTraffic > 0 ? Math.min(100, Math.round((usedTraffic / totalTraffic) * 100)) : 0; - var stats = Array.isArray(state.stats) ? state.stats : [0, 0, 0]; - var overview = state.sessionOverview || {}; + const usedTraffic = ((state.subscribe && state.subscribe.u) || 0) + ((state.subscribe && state.subscribe.d) || 0); + const totalTraffic = (state.subscribe && state.subscribe.transfer_enable) || 0; + const remainingTraffic = Math.max(totalTraffic - usedTraffic, 0); + const percent = totalTraffic > 0 ? Math.min(100, Math.round((usedTraffic / totalTraffic) * 100)) : 0; + const stats = Array.isArray(state.stats) ? state.stats : [0, 0, 0]; + const overview = state.sessionOverview || {}; - var contentArea = document.querySelector(".dashboard-main"); + const contentArea = document.querySelector('.dashboard-main'); if (contentArea) { contentArea.innerHTML = renderCurrentRoute(remainingTraffic, usedTraffic, totalTraffic, percent, stats, overview); } - var sidebarContainer = document.querySelector(".dashboard-shell"); + const sidebarContainer = document.querySelector('.dashboard-shell'); if (sidebarContainer) { - var sidebar = sidebarContainer.querySelector(".dashboard-sidebar"); + const sidebar = sidebarContainer.querySelector('.dashboard-sidebar'); if (sidebar) { sidebar.innerHTML = renderSidebar(remainingTraffic, usedTraffic, totalTraffic, percent, overview, true); } @@ -1292,12 +1292,12 @@ } function renderDashboard() { - var usedTraffic = ((state.subscribe && state.subscribe.u) || 0) + ((state.subscribe && state.subscribe.d) || 0); - var totalTraffic = (state.subscribe && state.subscribe.transfer_enable) || 0; - var remainingTraffic = Math.max(totalTraffic - usedTraffic, 0); - var percent = totalTraffic > 0 ? Math.min(100, Math.round((usedTraffic / totalTraffic) * 100)) : 0; - var stats = Array.isArray(state.stats) ? state.stats : [0, 0, 0]; - var overview = state.sessionOverview || {}; + const usedTraffic = ((state.subscribe && state.subscribe.u) || 0) + ((state.subscribe && state.subscribe.d) || 0); + const totalTraffic = (state.subscribe && state.subscribe.transfer_enable) || 0; + const remainingTraffic = Math.max(totalTraffic - usedTraffic, 0); + const percent = totalTraffic > 0 ? Math.min(100, Math.round((usedTraffic / totalTraffic) * 100)) : 0; + const stats = Array.isArray(state.stats) ? state.stats : [0, 0, 0]; + const overview = state.sessionOverview || {}; return renderDashboardByRoute(remainingTraffic, usedTraffic, totalTraffic, percent, stats, overview); } @@ -1385,14 +1385,14 @@ ].join(""); } - var html = [ + const html = [ '
', - renderTrafficOverviewCard(remainingTraffic, usedTraffic, totalTraffic, percent, "span-12 section-card--overview"), - renderIpv6TrafficOverviewCard("span-12"), - renderAccessSnapshotCard(overview, "span-12 section-card--overview"), - renderRealNameVerificationOverviewCard("span-12 section-card--overview"), + renderTrafficOverviewCard(remainingTraffic, usedTraffic, totalTraffic, percent, 'span-12 section-card--overview'), + renderIpv6TrafficOverviewCard('span-12'), + renderAccessSnapshotCard(overview, 'span-12 section-card--overview'), + renderRealNameVerificationOverviewCard('span-12 section-card--overview'), '
' - ].join(""); + ].join(''); return html; } @@ -1426,19 +1426,19 @@ } function renderAccessSnapshotCard(overview, extraClass) { - extraClass = extraClass || "span-7"; - var ipv6Overview = state.ipv6SessionOverview; - var limitDisplay = formatLimit(overview.device_limit) + (ipv6Overview ? " / " + formatLimit(ipv6Overview.device_limit) : ""); + extraClass = extraClass || 'span-7'; + const ipv6Overview = state.ipv6SessionOverview; + const limitDisplay = formatLimit(overview.device_limit) + (ipv6Overview ? ' / ' + formatLimit(ipv6Overview.device_limit) : ''); return [ '
', '
访问概览

安全状态

', '
', - kpiBox("设备数限制(IPv4/IPv6)", limitDisplay), - kpiBox("余额", formatMoney((state.user && state.user.balance) || 0, state.appConfig && state.appConfig.currency_symbol)), - kpiBox("到期时间", formatDate(state.subscribe && state.subscribe.expired_at)), + kpiBox('设备数限制(IPv4/IPv6)', limitDisplay), + kpiBox('余额', formatMoney((state.user && state.user.balance) || 0, state.appConfig && state.appConfig.currency_symbol)), + kpiBox('到期时间', formatDate(state.subscribe && state.subscribe.expired_at)), '
', '
' - ].join(""); + ].join(''); } function renderIpv6TrafficOverviewCard(extraClass) { @@ -1477,21 +1477,21 @@ } function renderSidebar(remainingTraffic, usedTraffic, totalTraffic, percent, overview, innerOnly) { - var content = [ + let content = [ '' - ].join(""); - content = content.replace(/', - "", - '
', - !isIpv6 ? kpiBox("套餐", escapeHtml((sub && sub.plan && sub.plan.name) || "暂无套餐")) : "", - kpiBox("限速", formatSpeed(sub && sub.speed_limit)), - kpiBox("重置日", String((sub && sub.reset_day) || "-")), - !isIpv6 ? kpiBox("邮箱", '' + escapeHtml((user && user.email) || "-") + "") : "", - "
", - ipv6FooterHtml, - "" - ].join(""); + '', + '', + '
', + !isIpv6 ? kpiBox('套餐', escapeHtml((sub && sub.plan && sub.plan.name) || '暂无套餐')) : '', + kpiBox('限速', formatSpeed(sub && sub.speed_limit)), + kpiBox('重置日', String((sub && sub.reset_day) || '-')), + !isIpv6 ? kpiBox('邮箱', '' + escapeHtml((user && user.email) || '-') + '') : '', + '
', + ipv6FooterHtml, + '' + ].join(''); return html; } @@ -1839,8 +1839,8 @@ } function renderTicketStatus(status) { - var isOpen = Number(status) === 0; - return '' + (isOpen ? "处理中" : "已关闭") + ""; + const isOpen = Number(status) === 0; + return '' + (isOpen ? '处理中' : '已关闭') + ''; } function getTicketLevelLabel(level) { @@ -1875,7 +1875,7 @@ } function getNodePageSize() { - var width = window.innerWidth || document.documentElement.clientWidth || 1280; + const width = window.innerWidth || document.documentElement.clientWidth || 1280; if (width <= 860) { return 2; } @@ -1914,26 +1914,26 @@ if (Array.isArray(tags)) { return tags.filter(Boolean); } - if (typeof tags === "string") { - var normalized = tags.trim(); - if (!normalized || normalized === "[]" || normalized === "null" || normalized === '""') { + if (typeof tags === 'string') { + let normalized = tags.trim(); + if (!normalized || normalized === '[]' || normalized === 'null' || normalized === '""') { return []; } - if ((normalized.charAt(0) === "[" && normalized.charAt(normalized.length - 1) === "]") || - (normalized.charAt(0) === "{" && normalized.charAt(normalized.length - 1) === "}")) { + if ((normalized.charAt(0) === '[' && normalized.charAt(normalized.length - 1) === ']') || + (normalized.charAt(0) === '{' && normalized.charAt(normalized.length - 1) === '}')) { try { - var parsed = JSON.parse(normalized); + const parsed = JSON.parse(normalized); if (Array.isArray(parsed)) { - return parsed.map(function (tag) { + return parsed.map((tag) => { return String(tag).trim(); }).filter(Boolean); } } catch (error) { - normalized = normalized.replace(/^\[/, "").replace(/\]$/, ""); + normalized = normalized.replace(/^\[/, '').replace(/\]$/, ''); } } - return normalized.split(/[|,\/]/).map(function (tag) { - return tag.trim().replace(/^["'\[]+|["'\]]+$/g, ""); + return normalized.split(/[|,\/]/).map((tag) => { + return tag.trim().replace(/^["'\[]+|["'\]]+$/g, ''); }).filter(Boolean); } return []; @@ -1945,29 +1945,29 @@ } function buildSessionOverview(list, extra, isIpv6) { - var ipMap = {}; - var deviceMap = {}; - var lastOnlineAt = null; + const ipMap = {}; + const deviceMap = {}; + let lastOnlineAt = null; - var normalizedSessions = list.map(function (session) { - var lastUsedAt = toTimestamp(session.last_used_at); - var createdAt = toTimestamp(session.created_at); - var expiresAt = toTimestamp(session.expires_at); - var sessionName = session.name || session.device_name || session.user_agent || ("Session #" + (session.id || "")); - var sessionIp = firstNonEmpty([ + const normalizedSessions = list.map((session) => { + const lastUsedAt = toTimestamp(session.last_used_at); + const createdAt = toTimestamp(session.created_at); + const expiresAt = toTimestamp(session.expires_at); + const sessionName = session.name || session.device_name || session.user_agent || ('Session #' + (session.id || '')); + const sessionIp = firstNonEmpty([ session.ip, session.ip_address, session.last_used_ip, session.login_ip, session.current_ip ]); - var deviceKey = firstNonEmpty([ + const deviceKey = firstNonEmpty([ session.device_name, session.user_agent, session.client_type, session.os, sessionName - ]) || ("device-" + Math.random().toString(36).slice(2)); + ]) || ('device-' + Math.random().toString(36).slice(2)); if (sessionIp) { ipMap[sessionIp] = true; @@ -1984,7 +1984,7 @@ name: sessionName, abilities: session.abilities, ip: sessionIp, - user_agent: session.user_agent || "", + user_agent: session.user_agent || '', last_used_at: lastUsedAt, created_at: createdAt, expires_at: expiresAt, @@ -1992,7 +1992,7 @@ }; }); - var sub = isIpv6 ? state.ipv6Subscribe : state.subscribe; + const sub = isIpv6 ? state.ipv6Subscribe : state.subscribe; return { online_ip_count: Object.keys(ipMap).length, @@ -2018,15 +2018,15 @@ } function renderBrandMark() { - var logoUrl = getThemeLogo(); + const logoUrl = getThemeLogo(); if (logoUrl) { - return '' + escapeHtml(theme.title || '; + return '' + escapeHtml(theme.title || 'Nebula') + ' logo'; } return ''; } function renderThemeToggle() { - var isLight = state.themeMode === "light"; + const isLight = state.themeMode === 'light'; return [ '' - ].join(""); + ].join(''); } function renderRecordFooter() { - var records = []; + const records = []; if (theme.config && theme.config.icpNo) { records.push('' + escapeHtml(theme.config.icpNo) + ''); } @@ -2047,7 +2047,7 @@ records.push('' + escapeHtml(theme.config.psbNo) + ''); } if (!records.length) { - return ""; + return ''; } return ''; } @@ -2056,16 +2056,16 @@ return [ '
', '

Welcome To

', - '

' + escapeHtml(getAuthPanelTitle()) + "

", + '

' + escapeHtml(getAuthPanelTitle()) + '

', '
' - ].join(""); + ].join(''); } function getAuthPanelTitle() { - if (state.mode === "register") { - return (theme.config && theme.config.registerTitle) || "Create your access."; + if (state.mode === 'register') { + return (theme.config && theme.config.registerTitle) || 'Create your access.'; } - return (theme.config && theme.config.welcomeTarget) || (theme && theme.title) || "Your Space"; + return (theme.config && theme.config.welcomeTarget) || (theme && theme.title) || 'Your Space'; } function onRouteChange() { @@ -2076,9 +2076,9 @@ } function setCurrentRoute(route) { - var nextRoute = isValidRoute(route) ? route : "overview"; + const nextRoute = isValidRoute(route) ? route : 'overview'; state.currentRoute = nextRoute; - window.location.hash = "#/" + nextRoute; + window.location.hash = '#/' + nextRoute; render(); } @@ -2095,19 +2095,19 @@ } function getCurrentRoute() { - var hash = String(window.location.hash || ""); - var route = hash.indexOf("#/") === 0 ? hash.slice(2) : ""; - return isValidRoute(route) ? route : "overview"; + const hash = String(window.location.hash || ''); + const route = hash.indexOf('#/') === 0 ? hash.slice(2) : ''; + return isValidRoute(route) ? route : 'overview'; } function getStoredThemePreference() { - var stored = ""; + let stored = ''; try { - stored = window.localStorage.getItem("nebula_theme_preference") || ""; + stored = window.localStorage.getItem('nebula_theme_preference') || ''; } catch (error) { - stored = ""; + stored = ''; } - if (stored === "light" || stored === "dark" || stored === "system") { + if (stored === 'light' || stored === 'dark' || stored === 'system') { return stored; } return getDefaultThemePreference(); @@ -2165,26 +2165,26 @@ } function renderRealNameVerificationOverviewCard(extraClass) { - var verification = getRealNameVerificationState(); + const verification = getRealNameVerificationState(); if (!verification.enabled) { - return ""; + return ''; } - var submitted = verification.submitted_at ? formatDate(verification.submitted_at) : "-"; - var reviewed = verification.reviewed_at ? formatDate(verification.reviewed_at) : "-"; + const submitted = verification.submitted_at ? formatDate(verification.submitted_at) : '-'; + const reviewed = verification.reviewed_at ? formatDate(verification.reviewed_at) : '-'; - var html = [ - '
', + const html = [ + '
', '
实名认证

认证信息

', '
', - kpiBox("认证状态", escapeHtml(verification.status_label || "未认证")), - kpiBox("真实姓名", escapeHtml(verification.real_name || "-")), - kpiBox("证件号码", escapeHtml(verification.identity_no_masked || "-")), + kpiBox('认证状态', escapeHtml(verification.status_label || '未认证')), + kpiBox('真实姓名', escapeHtml(verification.real_name || '-')), + kpiBox('证件号码', escapeHtml(verification.identity_no_masked || '-')), '
', - verification.reviewed_at ? '' : "", - verification.reject_reason ? '
审核备注: ' + escapeHtml(verification.reject_reason) + '
' : "", + verification.reviewed_at ? '' : '', + verification.reject_reason ? '
审核备注: ' + escapeHtml(verification.reject_reason) + '
' : '', '
' - ].join(""); + ].join(''); } function renderRealNameVerificationPanel() { @@ -2260,50 +2260,50 @@ } function getThemeLogo() { - var config = theme.config || {}; - if (state.themeMode === "light") { - return config.lightLogoUrl || config.darkLogoUrl || theme.logo || ""; + const config = theme.config || {}; + if (state.themeMode === 'light') { + return config.lightLogoUrl || config.darkLogoUrl || theme.logo || ''; } - return config.darkLogoUrl || theme.logo || config.lightLogoUrl || ""; + return config.darkLogoUrl || theme.logo || config.lightLogoUrl || ''; } function updateThemeColorMeta() { - var meta = document.querySelector('meta[name="theme-color"]'); + const meta = document.querySelector('meta[name="theme-color"]'); if (meta) { - meta.setAttribute("content", state.themeMode === "light" ? "#eef4fb" : "#08101c"); + meta.setAttribute('content', state.themeMode === 'light' ? '#eef4fb' : '#08101c'); } } function getDefaultThemePreference() { - var configMode = theme && theme.config ? theme.config.defaultThemeMode : ""; - if (configMode === "light" || configMode === "dark" || configMode === "system") { + const configMode = theme && theme.config ? theme.config.defaultThemeMode : ''; + if (configMode === 'light' || configMode === 'dark' || configMode === 'system') { return configMode; } - return "system"; + return 'system'; } function resolveThemeMode(preference) { - if (preference === "light" || preference === "dark") { + if (preference === 'light' || preference === 'dark') { return preference; } - return isSystemDarkMode() ? "dark" : "light"; + return isSystemDarkMode() ? 'dark' : 'light'; } function bindSystemThemeListener() { if (!themeMediaQuery) { return; } - var onThemeChange = function () { - if (state.themePreference === "system") { + const onThemeChange = () => { + if (state.themePreference === 'system') { applyThemeMode(); render(); } }; - if (typeof themeMediaQuery.addEventListener === "function") { - themeMediaQuery.addEventListener("change", onThemeChange); + if (typeof themeMediaQuery.addEventListener === 'function') { + themeMediaQuery.addEventListener('change', onThemeChange); return; } - if (typeof themeMediaQuery.addListener === "function") { + if (typeof themeMediaQuery.addListener === 'function') { themeMediaQuery.addListener(onThemeChange); } } @@ -2320,8 +2320,8 @@ } function getMetricsBaseUrl() { - var baseUrl = theme && theme.config ? theme.config.metricsBaseUrl : ""; - return String(baseUrl || "").replace(/\/+$/, ""); + const baseUrl = theme && theme.config ? theme.config.metricsBaseUrl : ''; + return String(baseUrl || '').replace(/\/+$/, ''); } function getUplinkHeadline() { @@ -2398,26 +2398,26 @@ } function getStoredToken() { - var candidates = []; - collectStorageValues(window.localStorage, ["access_token", "auth_data", "__nebula_auth_data__", "token", "auth_token"], candidates); - collectStorageValues(window.sessionStorage, ["access_token", "auth_data", "__nebula_auth_data__", "token", "auth_token"], candidates); - for (var i = 0; i < candidates.length; i += 1) { - var token = extractToken(candidates[i]); + const candidates = []; + collectStorageValues(window.localStorage, ['access_token', 'auth_data', '__nebula_auth_data__', 'token', 'auth_token'], candidates); + collectStorageValues(window.sessionStorage, ['access_token', 'auth_data', '__nebula_auth_data__', 'token', 'auth_token'], candidates); + for (let i = 0; i < candidates.length; i += 1) { + const token = extractToken(candidates[i]); if (token) { return token; } } - return ""; + return ''; } function saveToken(token) { - var normalized = normalizeAuthHeader(token); - window.localStorage.setItem("access_token", normalized); - window.localStorage.setItem("auth_data", normalized); - window.localStorage.setItem("__nebula_auth_data__", normalized); - window.sessionStorage.setItem("access_token", normalized); - window.sessionStorage.setItem("auth_data", normalized); - window.sessionStorage.setItem("__nebula_auth_data__", normalized); + const normalized = normalizeAuthHeader(token); + window.localStorage.setItem('access_token', normalized); + window.localStorage.setItem('auth_data', normalized); + window.localStorage.setItem('__nebula_auth_data__', normalized); + window.sessionStorage.setItem('access_token', normalized); + window.sessionStorage.setItem('auth_data', normalized); + window.sessionStorage.setItem('__nebula_auth_data__', normalized); } function clearToken() { @@ -2493,49 +2493,49 @@ function applyCustomBackground() { if (!theme.config || !theme.config.backgroundUrl) { - document.documentElement.style.removeProperty("--nebula-bg-image"); + document.documentElement.style.removeProperty('--nebula-bg-image'); return; } - document.documentElement.style.setProperty("--nebula-bg-image", 'url("' + String(theme.config.backgroundUrl).replace(/"/g, '\\"') + '")'); + document.documentElement.style.setProperty('--nebula-bg-image', 'url("' + String(theme.config.backgroundUrl).replace(/"/g, '\\"') + '")'); } function getVerifyToken() { - var url = new URL(window.location.href); - var directVerify = url.searchParams.get("verify"); + const url = new URL(window.location.href); + const directVerify = url.searchParams.get('verify'); if (directVerify) { return directVerify; } - if (window.location.hash.indexOf("?") !== -1) { - return new URLSearchParams(window.location.hash.split("?")[1] || "").get("verify"); + if (window.location.hash.indexOf('?') !== -1) { + return new URLSearchParams(window.location.hash.split('?')[1] || '').get('verify'); } - return ""; + return ''; } function clearVerifyToken() { - var url = new URL(window.location.href); - url.searchParams.delete("verify"); - if (window.location.hash.indexOf("?") !== -1) { - window.history.replaceState({}, "", url.pathname + url.search + window.location.hash.split("?")[0]); + const url = new URL(window.location.href); + url.searchParams.delete('verify'); + if (window.location.hash.indexOf('?') !== -1) { + window.history.replaceState({}, '', url.pathname + url.search + window.location.hash.split('?')[0]); return; } - window.history.replaceState({}, "", url.pathname + url.search); + window.history.replaceState({}, '', url.pathname + url.search); } function showMessage(message, type) { - var container = document.getElementById("nebula-toasts"); + const container = document.getElementById('nebula-toasts'); if (!container) return; - + if (!message) { - container.innerHTML = ""; + container.innerHTML = ''; return; } - container.innerHTML = '
' + escapeHtml(message) + "
"; - + container.innerHTML = '
' + escapeHtml(message) + '
'; + // Auto clear after 4s if (state.messageTimeout) clearTimeout(state.messageTimeout); - state.messageTimeout = window.setTimeout(function() { - container.innerHTML = ""; + state.messageTimeout = window.setTimeout(() => { + container.innerHTML = ''; }, 4000); } @@ -2556,7 +2556,7 @@ if (!storage) { return; } - for (var i = 0; i < keys.length; i += 1) { + for (let i = 0; i < keys.length; i += 1) { pushCandidate(storage.getItem(keys[i]), target); } } @@ -2570,65 +2570,65 @@ function extractToken(value, depth) { depth = depth || 0; - if (depth > 4 || value === null || typeof value === "undefined") { - return ""; + if (depth > 4 || value === null || typeof value === 'undefined') { + return ''; } - if (typeof value === "string") { - var trimmed = value.trim(); + if (typeof value === 'string') { + const trimmed = value.trim(); if (!trimmed) { - return ""; + return ''; } - if (trimmed.indexOf("Bearer ") === 0) { + if (trimmed.indexOf('Bearer ') === 0) { return trimmed; } if (/^[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+(\.[A-Za-z0-9\-_.+/=]+)?$/.test(trimmed)) { - return "Bearer " + trimmed; + return 'Bearer ' + trimmed; } - if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf("{") === -1) { - return "Bearer " + trimmed; + if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf('{') === -1) { + return 'Bearer ' + trimmed; } - if ((trimmed.charAt(0) === "{" && trimmed.charAt(trimmed.length - 1) === "}") || - (trimmed.charAt(0) === "[" && trimmed.charAt(trimmed.length - 1) === "]")) { + if ((trimmed.charAt(0) === '{' && trimmed.charAt(trimmed.length - 1) === '}') || + (trimmed.charAt(0) === '[' && trimmed.charAt(trimmed.length - 1) === ']')) { try { return extractToken(JSON.parse(trimmed), depth + 1); } catch (error) { - return ""; + return ''; } } - return ""; + return ''; } if (Array.isArray(value)) { - for (var i = 0; i < value.length; i += 1) { - var arrayToken = extractToken(value[i], depth + 1); + for (let i = 0; i < value.length; i += 1) { + const arrayToken = extractToken(value[i], depth + 1); if (arrayToken) { return arrayToken; } } - return ""; + return ''; } - if (typeof value === "object") { - var directKeys = ["access_token", "auth_data", "token", "Authorization", "authorization"]; - for (var j = 0; j < directKeys.length; j += 1) { + if (typeof value === 'object') { + const directKeys = ['access_token', 'auth_data', 'token', 'Authorization', 'authorization']; + for (let j = 0; j < directKeys.length; j += 1) { if (Object.prototype.hasOwnProperty.call(value, directKeys[j])) { - var directToken = extractToken(value[directKeys[j]], depth + 1); + const directToken = extractToken(value[directKeys[j]], depth + 1); if (directToken) { return directToken; } } } } - return ""; + return ''; } function normalizeAuthHeader(token) { - var trimmed = String(token || "").trim(); + const trimmed = String(token || '').trim(); if (!trimmed) { - return ""; + return ''; } - if (trimmed.indexOf("Bearer ") === 0) { + if (trimmed.indexOf('Bearer ') === 0) { return trimmed; } - return "Bearer " + trimmed; + return 'Bearer ' + trimmed; } function copyText(value, successMessage) { @@ -2645,25 +2645,25 @@ } function formatTraffic(value) { - var units = ["B", "KB", "MB", "GB", "TB"]; - var size = Number(value || 0); - var index = 0; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = Number(value || 0); + let index = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } - return size.toFixed(size >= 10 || index === 0 ? 0 : 1) + " " + units[index]; + return size.toFixed(size >= 10 || index === 0 ? 0 : 1) + ' ' + units[index]; } function formatMoney(value, symbol) { - return (symbol || "$") + (Number(value || 0) / 100).toFixed(2); + return (symbol || '$') + (Number(value || 0) / 100).toFixed(2); } function formatSpeed(value) { if (!value) { - return "不限"; + return '不限'; } - return value + " Mbps"; + return value + ' Mbps'; } function formatLimit(value) { @@ -2685,28 +2685,28 @@ if (!value) { return null; } - if (typeof value === "number") { + if (typeof value === 'number') { return value; } - var parsed = Date.parse(value); + const parsed = Date.parse(value); return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000); } function truncate(text, maxLength) { - var str = String(text || ""); + const str = String(text || ''); if (str.length <= maxLength) { return str; } - return str.slice(0, maxLength - 1) + "..."; + return str.slice(0, maxLength - 1) + '...'; } function firstNonEmpty(values) { - for (var i = 0; i < values.length; i += 1) { + for (let i = 0; i < values.length; i += 1) { if (values[i]) { return values[i]; } } - return ""; + return ''; } function escapeHtml(value) { diff --git a/frontend/theme/Nebula/assets/enhancer.js b/frontend/theme/Nebula/assets/enhancer.js index 755e154..538764a 100644 --- a/frontend/theme/Nebula/assets/enhancer.js +++ b/frontend/theme/Nebula/assets/enhancer.js @@ -1,9 +1,9 @@ (function () { "use strict"; - var theme = window.NEBULA_THEME || {}; - var loader = document.getElementById("nebula-loader"); - var appRoot = document.getElementById("app"); + const theme = window.NEBULA_THEME || {}; + const loader = document.getElementById('nebula-loader'); + const appRoot = document.getElementById('app'); applyCustomBackground(); mountShell(); @@ -15,12 +15,12 @@ return; } - var shell = document.createElement("div"); - shell.className = "app-shell nebula-app-shell"; - shell.id = "nebula-app-shell"; + const shell = document.createElement('div'); + shell.className = 'app-shell nebula-app-shell'; + shell.id = 'nebula-app-shell'; shell.innerHTML = renderShellChrome(); - var parent = appRoot.parentNode; + const parent = appRoot.parentNode; parent.insertBefore(shell, appRoot); var appStage = shell.querySelector(".nebula-app-stage"); @@ -30,10 +30,10 @@ } function mountMonitor() { - var rail = document.getElementById("nebula-side-rail"); - var panel = document.createElement("aside"); - panel.className = "nebula-monitor"; - panel.id = "nebula-monitor"; + const rail = document.getElementById('nebula-side-rail'); + const panel = document.createElement('aside'); + panel.className = 'nebula-monitor'; + panel.id = 'nebula-monitor'; panel.innerHTML = renderLoadingPanel(); if (rail) { @@ -42,18 +42,18 @@ document.body.appendChild(panel); } - panel.addEventListener("click", function (event) { - var actionEl = event.target.closest("[data-nebula-action]"); + panel.addEventListener('click', (event) => { + const actionEl = event.target.closest('[data-nebula-action]'); if (!actionEl) { return; } - var action = actionEl.getAttribute("data-nebula-action"); - if (action === "refresh") { + const action = actionEl.getAttribute('data-nebula-action'); + if (action === 'refresh') { loadDeviceOverview(panel); } - if (action === "toggle") { - panel.classList.toggle("is-collapsed"); + if (action === 'toggle') { + panel.classList.toggle('is-collapsed'); } }); @@ -69,10 +69,10 @@ } try { - var response = await fetch("/api/v1/user/user-online-devices/get-ip", { - method: "GET", + const response = await fetch('/api/v1/user/user-online-devices/get-ip', { + method: 'GET', headers: getRequestHeaders(), - credentials: "same-origin" + credentials: 'same-origin' }); if (response.status === 401 || response.status === 403) { @@ -81,17 +81,17 @@ } if (!response.ok) { - throw new Error("Unable to load device overview"); + throw new Error('Unable to load device overview'); } - var resJson = await response.json(); - var data = resJson && resJson.data ? resJson.data : {}; - console.log("[Nebula] Device overview data:", data); // Diagnostic log - var overview = data.session_overview || data; - + const resJson = await response.json(); + const data = resJson && resJson.data ? resJson.data : {}; + console.log('[Nebula] Device overview data:', data); // Diagnostic log + const overview = data.session_overview || data; + panel.innerHTML = renderPanel(overview); } catch (error) { - panel.innerHTML = renderErrorPanel(error.message || "Unable to load device overview"); + panel.innerHTML = renderErrorPanel(error.message || 'Unable to load device overview'); } } @@ -174,43 +174,43 @@ } function renderPanel(data) { - var ips = Array.isArray(data.online_ips) ? data.online_ips : []; - var sessions = Array.isArray(data.sessions) ? data.sessions.slice(0, 4) : []; + const ips = Array.isArray(data.online_ips) ? data.online_ips : []; + const sessions = Array.isArray(data.sessions) ? data.sessions.slice(0, 4) : []; return [ '
', - '

Live Access Monitor

' + escapeHtml((theme.config && theme.config.slogan) || "Current IP and session visibility") + '
', + '

Live Access Monitor

' + escapeHtml((theme.config && theme.config.slogan) || 'Current IP and session visibility') + '
', '
', '
', '
', '
', '
', - metric(String(data.online_ip_count || 0), "Current IPs"), - metric(String(data.online_device_count || 0), "Online devices"), - metric(String(data.active_session_count || 0), "Stored sessions"), - metric(formatLimit(data.device_limit), "Device limit"), + metric(String(data.online_ip_count || 0), 'Current IPs'), + metric(String(data.online_device_count || 0), 'Online devices'), + metric(String(data.active_session_count || 0), 'Stored sessions'), + metric(formatLimit(data.device_limit), 'Device limit'), '
', '
Last online: ' + formatDate(data.last_online_at) + '
', '
', '
', '
Online IP addresses
', - ips.length ? '
' + ips.map(function (ip) { + ips.length ? '
' + ips.map((ip) => { return '
' + escapeHtml(ip) + '
'; - }).join("") + '
' : '
No IP data reported yet.
', + }).join('') : '
No IP data reported yet.
', '
', '
', '
Recent sessions
', - sessions.length ? '
' + sessions.map(function (session) { + sessions.length ? '
' + sessions.map((session) => { return [ '
', - '' + escapeHtml(session.name || ("Session #" + session.id)) + (session.is_current ? " · current" : "") + '', + '' + escapeHtml(session.name || ('Session #' + session.id)) + (session.is_current ? ' · current' : '') + '', '
Last used: ' + formatDate(session.last_used_at) + '
', '
' - ].join(""); - }).join("") + '
' : '
No session records available.
', + ].join(''); + }).join('') : '
No session records available.
', '
', '
' - ].join(""); + ].join(''); } function metric(value, label) { @@ -218,11 +218,11 @@ } function getRequestHeaders() { - var headers = { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest" + const headers = { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' }; - var token = getStoredToken(); + const token = getStoredToken(); if (token) { headers.Authorization = token; } @@ -275,21 +275,21 @@ } function getStoredToken() { - var candidates = []; - var directKeys = ["access_token", "auth_data", "__nebula_auth_data__", "token", "auth_token"]; + const candidates = []; + const directKeys = ['access_token', 'auth_data', '__nebula_auth_data__', 'token', 'auth_token']; collectStorageValues(window.localStorage, directKeys, candidates); collectStorageValues(window.sessionStorage, directKeys, candidates); collectCookieValues(candidates); collectGlobalValues(candidates); - for (var i = 0; i < candidates.length; i += 1) { - var token = extractToken(candidates[i]); + for (let i = 0; i < candidates.length; i += 1) { + const token = extractToken(candidates[i]); if (token) { return token; } } - return ""; + return ''; } function collectStorageValues(storage, keys, target) { @@ -306,15 +306,15 @@ if (!document.cookie) { return; } - var cookies = document.cookie.split(";"); - for (var i = 0; i < cookies.length; i += 1) { - var part = cookies[i].split("="); + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i += 1) { + const part = cookies[i].split('='); if (part.length < 2) { continue; } - var key = String(part[0] || "").trim(); - if (key.indexOf("token") !== -1 || key.indexOf("auth") !== -1) { - pushCandidate(decodeURIComponent(part.slice(1).join("=")), target); + const key = String(part[0] || '').trim(); + if (key.indexOf('token') !== -1 || key.indexOf('auth') !== -1) { + pushCandidate(decodeURIComponent(part.slice(1).join('=')), target); } } } @@ -337,29 +337,29 @@ return ""; } - if (typeof value === "string") { - var trimmed = value.trim(); + if (typeof value === 'string') { + const trimmed = value.trim(); if (!trimmed) { - return ""; + return ''; } - if (trimmed.indexOf("Bearer ") === 0) { + if (trimmed.indexOf('Bearer ') === 0) { return trimmed; } if (/^[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+(\.[A-Za-z0-9\-_.+/=]+)?$/.test(trimmed)) { - return "Bearer " + trimmed; + return 'Bearer ' + trimmed; } - if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf("{") === -1) { - return "Bearer " + trimmed; + if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf('{') === -1) { + return 'Bearer ' + trimmed; } - if ((trimmed.charAt(0) === "{" && trimmed.charAt(trimmed.length - 1) === "}") || - (trimmed.charAt(0) === "[" && trimmed.charAt(trimmed.length - 1) === "]")) { + if ((trimmed.charAt(0) === '{' && trimmed.charAt(trimmed.length - 1) === '}') || + (trimmed.charAt(0) === '[' && trimmed.charAt(trimmed.length - 1) === ']')) { try { return extractToken(JSON.parse(trimmed), (depth || 0) + 1); } catch (error) { - return ""; + return ''; } } - return ""; + return ''; } if (Array.isArray(value)) { @@ -372,22 +372,22 @@ return ""; } - if (typeof value === "object") { - var keys = ["access_token", "auth_data", "token", "Authorization", "authorization"]; - for (var j = 0; j < keys.length; j += 1) { + if (typeof value === 'object') { + const keys = ['access_token', 'auth_data', 'token', 'Authorization', 'authorization']; + for (let j = 0; j < keys.length; j += 1) { if (Object.prototype.hasOwnProperty.call(value, keys[j])) { - var directToken = extractToken(value[keys[j]], (depth || 0) + 1); + const directToken = extractToken(value[keys[j]], (depth || 0) + 1); if (directToken) { return directToken; } } } - for (var key in value) { + for (const key in value) { if (!Object.prototype.hasOwnProperty.call(value, key)) { continue; } - var token = extractToken(value[key], (depth || 0) + 1); + const token = extractToken(value[key], (depth || 0) + 1); if (token) { return token; } diff --git a/internal/handler/common.go b/internal/handler/common.go index 45e0587..2cb29a2 100644 --- a/internal/handler/common.go +++ b/internal/handler/common.go @@ -270,14 +270,14 @@ func formatTimeValue(value *time.Time) string { func getFetchParams(c *gin.Context) map[string]string { params := make(map[string]string) - + // 1. Get from Query parameters for k, v := range c.Request.URL.Query() { if len(v) > 0 { params[k] = v[0] } } - + // 2. Override with JSON body if applicable (for POST) if c.Request.Method == http.MethodPost { var body map[string]any @@ -288,6 +288,6 @@ func getFetchParams(c *gin.Context) map[string]string { } } } - + return params }