From 9b1d2cabb93f4615cea0f08e64f8b89fb9687bee Mon Sep 17 00:00:00 2001 From: ReturnFI <151555003+ReturnFI@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:45:35 +0000 Subject: [PATCH] refactor: Externalize all template JavaScript to asset files --- core/scripts/webpanel/assets/js/base.js | 138 +++ core/scripts/webpanel/assets/js/config.js | 95 ++ core/scripts/webpanel/assets/js/index.js | 73 ++ core/scripts/webpanel/assets/js/settings.js | 1193 +++++++++++++++++++ core/scripts/webpanel/assets/js/users.js | 73 ++ 5 files changed, 1572 insertions(+) create mode 100644 core/scripts/webpanel/assets/js/base.js create mode 100644 core/scripts/webpanel/assets/js/config.js create mode 100644 core/scripts/webpanel/assets/js/index.js create mode 100644 core/scripts/webpanel/assets/js/settings.js create mode 100644 core/scripts/webpanel/assets/js/users.js diff --git a/core/scripts/webpanel/assets/js/base.js b/core/scripts/webpanel/assets/js/base.js new file mode 100644 index 0000000..cae0bcd --- /dev/null +++ b/core/scripts/webpanel/assets/js/base.js @@ -0,0 +1,138 @@ +$(function () { + const darkModeToggle = $("#darkModeToggle"); + const darkModeIcon = $("#darkModeIcon"); + const isDarkMode = localStorage.getItem("darkMode") === "enabled"; + + setDarkMode(isDarkMode); + updateIcon(isDarkMode); + + darkModeToggle.on("click", function (e) { + e.preventDefault(); + const enabled = $("body").hasClass("dark-mode"); + localStorage.setItem("darkMode", enabled ? "disabled" : "enabled"); + setDarkMode(!enabled); + updateIcon(!enabled); + }); + + function setDarkMode(enabled) { + $("body").toggleClass("dark-mode", enabled); + + if (enabled) { + $(".main-header").addClass("navbar-dark").removeClass("navbar-light navbar-white"); + $(".card").addClass("bg-dark"); + } else { + $(".main-header").addClass("navbar-white navbar-light").removeClass("navbar-dark"); + $(".card").removeClass("bg-dark"); + } + } + + function updateIcon(enabled) { + darkModeIcon.removeClass("fa-moon fa-sun") + .addClass(enabled ? "fa-sun" : "fa-moon"); + } + + const versionUrl = $('body').data('version-url'); + $.ajax({ + url: versionUrl, + type: 'GET', + success: function (response) { + $('#panel-version').text(`Version: ${response.current_version || 'N/A'}`); + }, + error: function (error) { + console.error("Error fetching version:", error); + $('#panel-version').text('Version: Error'); + } + }); + + function shouldCheckForUpdates() { + const lastCheck = localStorage.getItem('lastUpdateCheck'); + const updateDismissed = localStorage.getItem('updateDismissed'); + const now = Date.now(); + const checkInterval = 24 * 60 * 60 * 1000; + + if (!lastCheck) return true; + if (updateDismissed && now - parseInt(updateDismissed) < 2 * 60 * 60 * 1000) return false; + + return now - parseInt(lastCheck) > checkInterval; + } + + function showUpdateBar(version, changelog) { + $('#updateMessage').text(`Version ${version} is now available`); + + const converter = new showdown.Converter(); + const htmlChangelog = changelog ? converter.makeHtml(changelog) : '

No changelog available.

'; + $('#changelogText').html(htmlChangelog); + + $('#updateBar').slideDown(300); + + $('#viewRelease').off('click').on('click', function(e) { + e.preventDefault(); + window.open('https://github.com/ReturnFI/Blitz/releases/latest', '_blank'); + }); + + $('#showChangelog').off('click').on('click', function() { + const $content = $('#changelogContent'); + const $icon = $(this).find('i'); + + if ($content.is(':visible')) { + $content.slideUp(250); + $icon.removeClass('fa-chevron-up').addClass('fa-chevron-down'); + $(this).css('opacity', '0.8'); + } else { + $content.slideDown(250); + $icon.removeClass('fa-chevron-down').addClass('fa-chevron-up'); + $(this).css('opacity', '1'); + } + }); + + $('.dropdown-toggle').dropdown(); + + $('#remindLater').off('click').on('click', function(e) { + e.preventDefault(); + $('#updateBar').slideUp(350); + }); + + $('#skipVersion').off('click').on('click', function(e) { + e.preventDefault(); + localStorage.setItem('dismissedVersion', version); + localStorage.setItem('updateDismissed', Date.now().toString()); + $('#updateBar').slideUp(350); + }); + + $('#closeUpdateBar').off('click').on('click', function() { + $('#updateBar').slideUp(350); + }); + } + + function checkForUpdates() { + if (!shouldCheckForUpdates()) return; + + const checkVersionUrl = $('body').data('check-version-url'); + $.ajax({ + url: checkVersionUrl, + type: 'GET', + timeout: 10000, + success: function (response) { + localStorage.setItem('lastUpdateCheck', Date.now().toString()); + + if (response.is_latest) { + localStorage.removeItem('updateDismissed'); + return; + } + + const dismissedVersion = localStorage.getItem('dismissedVersion'); + if (dismissedVersion === response.latest_version) return; + + showUpdateBar(response.latest_version, response.changelog); + }, + error: function (xhr, status, error) { + if (status !== 'timeout') { + console.warn("Update check failed:", error); + } + localStorage.setItem('lastUpdateCheck', Date.now().toString()); + } + }); + } + + setTimeout(checkForUpdates, 2000); +}); \ No newline at end of file diff --git a/core/scripts/webpanel/assets/js/config.js b/core/scripts/webpanel/assets/js/config.js new file mode 100644 index 0000000..84951cb --- /dev/null +++ b/core/scripts/webpanel/assets/js/config.js @@ -0,0 +1,95 @@ +document.addEventListener('DOMContentLoaded', function () { + const mainContent = document.querySelector('.content-wrapper > div'); + const GET_FILE_URL = mainContent.dataset.getFileUrl; + const SET_FILE_URL = mainContent.dataset.setFileUrl; + + const saveButton = document.getElementById("save-button"); + const restoreButton = document.getElementById("restore-button"); + const container = document.getElementById("jsoneditor"); + + const editor = new JSONEditor(container, { + mode: "code", + onChange: validateJson + }); + + function validateJson() { + try { + editor.get(); + updateSaveButton(true); + hideErrorMessage(); + } catch (error) { + updateSaveButton(false); + showErrorMessage("Invalid JSON! Please correct the errors."); + } + } + + function updateSaveButton(isValid) { + saveButton.disabled = !isValid; + saveButton.style.cursor = isValid ? "pointer" : "not-allowed"; + saveButton.style.setProperty('background-color', isValid ? "#28a745" : "#ccc", 'important'); + saveButton.style.setProperty('color', isValid ? "#fff" : "#666", 'important'); + } + + function showErrorMessage(message) { + Swal.fire({ + title: "Error", + text: message, + icon: "error", + showConfirmButton: false, + timer: 5000, + position: 'top-right', + toast: true, + showClass: { popup: 'animate__animated animate__fadeInDown' }, + hideClass: { popup: 'animate__animated animate__fadeOutUp' } + }); + } + + function hideErrorMessage() { + Swal.close(); + } + + function saveJson() { + Swal.fire({ + title: 'Are you sure?', + text: 'Do you want to save the changes?', + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Yes, save it!', + cancelButtonText: 'Cancel', + reverseButtons: true + }).then((result) => { + if (result.isConfirmed) { + fetch(SET_FILE_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(editor.get()) + }) + .then(() => { + Swal.fire('Saved!', 'Your changes have been saved.', 'success'); + }) + .catch(error => { + Swal.fire('Error!', 'There was an error saving your data.', 'error'); + console.error("Error saving JSON:", error); + }); + } + }); + } + + function restoreJson() { + fetch(GET_FILE_URL) + .then(response => response.json()) + .then(json => { + editor.set(json); + Swal.fire('Success!', 'Your JSON has been loaded.', 'success'); + }) + .catch(error => { + Swal.fire('Error!', 'There was an error loading your JSON.', 'error'); + console.error("Error loading JSON:", error); + }); + } + + saveButton.addEventListener('click', saveJson); + restoreButton.addEventListener('click', restoreJson); + + restoreJson(); +}); \ No newline at end of file diff --git a/core/scripts/webpanel/assets/js/index.js b/core/scripts/webpanel/assets/js/index.js new file mode 100644 index 0000000..a8ec42e --- /dev/null +++ b/core/scripts/webpanel/assets/js/index.js @@ -0,0 +1,73 @@ +function updateServerInfo() { + const serverStatusUrl = document.querySelector('.content').dataset.serverStatusUrl; + fetch(serverStatusUrl) + .then(response => response.json()) + .then(data => { + document.getElementById('cpu-usage').textContent = data.cpu_usage; + document.getElementById('ram-usage').textContent = `${data.ram_usage} / ${data.total_ram}`; + document.getElementById('online-users').textContent = data.online_users; + document.getElementById('uptime').textContent = data.uptime; + + document.getElementById('server-ipv4').textContent = `IPv4: ${data.server_ipv4 || 'N/A'}`; + document.getElementById('server-ipv6').textContent = `IPv6: ${data.server_ipv6 || 'N/A'}`; + + document.getElementById('download-speed').textContent = `🔽 Download: ${data.download_speed}`; + document.getElementById('upload-speed').textContent = `🔼 Upload: ${data.upload_speed}`; + document.getElementById('tcp-connections').textContent = `TCP: ${data.tcp_connections}`; + document.getElementById('udp-connections').textContent = `UDP: ${data.udp_connections}`; + + document.getElementById('reboot-uploaded-traffic').textContent = data.reboot_uploaded_traffic; + document.getElementById('reboot-downloaded-traffic').textContent = data.reboot_downloaded_traffic; + document.getElementById('reboot-total-traffic').textContent = data.reboot_total_traffic; + + document.getElementById('user-uploaded-traffic').textContent = data.user_uploaded_traffic; + document.getElementById('user-downloaded-traffic').textContent = data.user_downloaded_traffic; + document.getElementById('user-total-traffic').textContent = data.user_total_traffic; + }) + .catch(error => console.error('Error fetching server info:', error)); +} + +function updateServiceStatuses() { + const servicesStatusUrl = document.querySelector('.content').dataset.servicesStatusUrl; + fetch(servicesStatusUrl) + .then(response => response.json()) + .then(data => { + updateServiceBox('hysteria2', data.hysteria_server); + updateServiceBox('telegrambot', data.hysteria_telegram_bot); + updateServiceBox('iplimit', data.hysteria_iplimit); + updateServiceBox('normalsub', data.hysteria_normal_sub); + }) + .catch(error => console.error('Error fetching service statuses:', error)); +} + +function updateServiceBox(serviceName, status) { + const statusElement = document.getElementById(serviceName + '-status'); + const statusBox = document.getElementById(serviceName + '-status-box'); + + if (status === true) { + statusElement.textContent = 'Active'; + statusBox.classList.remove('bg-danger'); + statusBox.classList.add('bg-success'); + } else { + statusElement.textContent = 'Inactive'; + statusBox.classList.remove('bg-success'); + statusBox.classList.add('bg-danger'); + } +} + +document.addEventListener('DOMContentLoaded', function () { + updateServerInfo(); + updateServiceStatuses(); + setInterval(updateServerInfo, 2000); + setInterval(updateServiceStatuses, 10000); + + const toggleIpBtn = document.getElementById('toggle-ip-visibility'); + const ipAddressesDiv = document.getElementById('ip-addresses'); + toggleIpBtn.addEventListener('click', function(e) { + e.preventDefault(); + const isBlurred = ipAddressesDiv.style.filter === 'blur(5px)'; + ipAddressesDiv.style.filter = isBlurred ? 'none' : 'blur(5px)'; + toggleIpBtn.querySelector('i').classList.toggle('fa-eye'); + toggleIpBtn.querySelector('i').classList.toggle('fa-eye-slash'); + }); +}); \ No newline at end of file diff --git a/core/scripts/webpanel/assets/js/settings.js b/core/scripts/webpanel/assets/js/settings.js new file mode 100644 index 0000000..8cfcddb --- /dev/null +++ b/core/scripts/webpanel/assets/js/settings.js @@ -0,0 +1,1193 @@ +$(document).ready(function () { + const contentSection = document.querySelector('.content'); + + const API_URLS = { + serverServicesStatus: contentSection.dataset.serverServicesStatusUrl, + getIp: contentSection.dataset.getIpUrl, + getPort: contentSection.dataset.getPortUrl, + getSni: contentSection.dataset.getSniUrl, + getAllNodes: contentSection.dataset.getAllNodesUrl, + addNode: contentSection.dataset.addNodeUrl, + deleteNode: contentSection.dataset.deleteNodeUrl, + getAllExtraConfigs: contentSection.dataset.getAllExtraConfigsUrl, + addExtraConfig: contentSection.dataset.addExtraConfigUrl, + deleteExtraConfig: contentSection.dataset.deleteExtraConfigUrl, + normalSubGetSubpath: contentSection.dataset.normalSubGetSubpathUrl, + telegramGetInterval: contentSection.dataset.telegramGetIntervalUrl, + getIpLimitConfig: contentSection.dataset.getIpLimitConfigUrl, + normalSubEditSubpath: contentSection.dataset.normalSubEditSubpathUrl, + setupDecoy: contentSection.dataset.setupDecoyUrl, + stopDecoy: contentSection.dataset.stopDecoyUrl, + getDecoyStatus: contentSection.dataset.getDecoyStatusUrl, + checkObfs: contentSection.dataset.checkObfsUrl, + enableObfs: contentSection.dataset.enableObfsUrl, + disableObfs: contentSection.dataset.disableObfsUrl, + telegramStart: contentSection.dataset.telegramStartUrl, + telegramStop: contentSection.dataset.telegramStopUrl, + telegramSetInterval: contentSection.dataset.telegramSetIntervalUrl, + normalSubStart: contentSection.dataset.normalSubStartUrl, + normalSubStop: contentSection.dataset.normalSubStopUrl, + setPortTemplate: contentSection.dataset.setPortUrlTemplate, + setSniTemplate: contentSection.dataset.setSniUrlTemplate, + editIp: contentSection.dataset.editIpUrl, + backup: contentSection.dataset.backupUrl, + restore: contentSection.dataset.restoreUrl, + startIpLimit: contentSection.dataset.startIpLimitUrl, + stopIpLimit: contentSection.dataset.stopIpLimitUrl, + configIpLimit: contentSection.dataset.configIpLimitUrl, + statusWarp: contentSection.dataset.statusWarpUrl, + updateGeoTemplate: contentSection.dataset.updateGeoUrlTemplate, + installWarp: contentSection.dataset.installWarpUrl, + uninstallWarp: contentSection.dataset.uninstallWarpUrl, + configureWarp: contentSection.dataset.configureWarpUrl + }; + + initUI(); + fetchDecoyStatus(); + fetchObfsStatus(); + fetchNodes(); + fetchExtraConfigs(); + + function escapeHtml(text) { + var map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + if (text === null || typeof text === 'undefined') { + return ''; + } + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; }); + } + + function isValidURI(uri) { + if (!uri) return false; + const lowerUri = uri.toLowerCase(); + return lowerUri.startsWith("vmess://") || lowerUri.startsWith("vless://") || lowerUri.startsWith("ss://") || lowerUri.startsWith("trojan://"); + } + + function isValidPath(path) { + if (!path) return false; + return path.trim() !== ''; + } + + function isValidDomain(domain) { + if (!domain) return false; + const lowerDomain = domain.toLowerCase(); + return !lowerDomain.startsWith("http://") && !lowerDomain.startsWith("https://"); + } + + function isValidPort(port) { + if (!port) return false; + return /^[0-9]+$/.test(port) && parseInt(port) > 0 && parseInt(port) <= 65535; + } + + function isValidSubPath(subpath) { + if (!subpath) return false; + return /^[a-zA-Z0-9]+$/.test(subpath); + } + + function isValidIPorDomain(input) { + if (input === null || typeof input === 'undefined') return false; + input = input.trim(); + if (input === '') return false; + + const ipV4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/; + const domainRegex = /^(?!-)(?:[a-zA-Z\d-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+$)[a-zA-Z\d]{1,63}$/; + const lowerInput = input.toLowerCase(); + + return ipV4Regex.test(input) || ipV6Regex.test(input) || domainRegex.test(lowerInput); + } + + function isValidPositiveNumber(value) { + if (!value) return false; + return /^[0-9]+$/.test(value) && parseInt(value) > 0; + } + + function confirmAction(actionName, callback) { + Swal.fire({ + title: `Are you sure?`, + text: `Do you really want to ${actionName}?`, + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, proceed!", + cancelButtonText: "Cancel" + }).then((result) => { + if (result.isConfirmed) { + callback(); + } + }); + } + + function sendRequest(url, type, data, successMessage, buttonSelector, showReload = true, postSuccessCallback = null) { + $.ajax({ + url: url, + type: type, + contentType: "application/json", + data: data ? JSON.stringify(data) : null, + beforeSend: function() { + if (buttonSelector) { + $(buttonSelector).prop('disabled', true); + $(buttonSelector + ' .spinner-border').show(); + } + }, + success: function (response) { + Swal.fire("Success!", successMessage, "success").then(() => { + if (showReload) { + location.reload(); + } else { + if (postSuccessCallback) { + postSuccessCallback(response); + } + } + }); + }, + error: function (xhr, status, error) { + let errorMessage = "An unexpected error occurred."; + if (xhr.responseJSON && xhr.responseJSON.detail) { + const detail = xhr.responseJSON.detail; + if (Array.isArray(detail)) { + errorMessage = detail.map(err => `Error in '${err.loc[1]}': ${err.msg}`).join('\n'); + } else if (typeof detail === 'string') { + let userMessage = detail; + const failMarker = 'failed with exit code'; + const markerIndex = detail.indexOf(failMarker); + if (markerIndex > -1) { + const colonIndex = detail.indexOf(':', markerIndex); + if (colonIndex > -1) { + userMessage = detail.substring(colonIndex + 1).trim(); + } + } + errorMessage = userMessage; + } + } + Swal.fire("Error!", errorMessage, "error"); + console.error("AJAX Error:", status, error, xhr.responseText); + }, + complete: function() { + if (buttonSelector) { + $(buttonSelector).prop('disabled', false); + $(buttonSelector + ' .spinner-border').hide(); + } + } + }); + } + + function validateForm(formId) { + let isValid = true; + $(`#${formId} .form-control:visible`).each(function () { + const input = $(this); + const id = input.attr('id'); + let fieldValid = true; + + if (id === 'normal_domain' || id === 'sni_domain' || id === 'decoy_domain') { + fieldValid = isValidDomain(input.val()); + } else if (id === 'normal_port' || id === 'hysteria_port') { + fieldValid = isValidPort(input.val()); + } else if (id === 'normal_subpath_input') { + fieldValid = isValidSubPath(input.val()); + } else if (id === 'ipv4' || id === 'ipv6') { + fieldValid = (input.val().trim() === '') ? true : isValidIPorDomain(input.val()); + } else if (id === 'node_ip') { + fieldValid = isValidIPorDomain(input.val()); + } else if (id === 'node_name' || id === 'extra_config_name') { + fieldValid = input.val().trim() !== ""; + } else if (id === 'extra_config_uri') { + fieldValid = isValidURI(input.val()); + } else if (id === 'block_duration' || id === 'max_ips' || id === 'telegram_backup_interval') { + if (input.val().trim() === '' && id === 'telegram_backup_interval') { + fieldValid = true; + } else { + fieldValid = isValidPositiveNumber(input.val()); + } + } else if (id === 'decoy_path') { + fieldValid = isValidPath(input.val()); + } else { + if (input.attr('placeholder') && input.attr('placeholder').includes('Enter') && !input.attr('id').startsWith('ipv')) { + fieldValid = input.val().trim() !== ""; + } + } + + if (!fieldValid) { + input.addClass('is-invalid'); + isValid = false; + } else { + input.removeClass('is-invalid'); + } + }); + return isValid; + } + + function initUI() { + $.ajax({ + url: API_URLS.serverServicesStatus, + type: "GET", + success: function (data) { + updateServiceUI(data); + }, + error: function (xhr, status, error) { + console.error("Failed to fetch service status:", error, xhr.responseText); + Swal.fire("Error!", "Could not fetch service statuses.", "error"); + } + }); + + $.ajax({ + url: API_URLS.getIp, + type: "GET", + success: function (data) { + $("#ipv4").val(data.ipv4 || ""); + $("#ipv6").val(data.ipv6 || ""); + }, + error: function (xhr, status, error) { + console.error("Failed to fetch IP addresses:", error, xhr.responseText); + } + }); + + $.ajax({ + url: API_URLS.getPort, + type: "GET", + success: function (data) { + $("#hysteria_port").val(data.port || ""); + }, + error: function (xhr, status, error) { + console.error("Failed to fetch port:", error, xhr.responseText); + } + }); + + $.ajax({ + url: API_URLS.getSni, + type: "GET", + success: function (data) { + $("#sni_domain").val(data.sni || ""); + }, + error: function (xhr, status, error) { + console.error("Failed to fetch SNI domain:", error, xhr.responseText); + } + }); + } + + function fetchNodes() { + $.ajax({ + url: API_URLS.getAllNodes, + type: "GET", + success: function (nodes) { + renderNodes(nodes); + }, + error: function(xhr) { + Swal.fire("Error!", "Failed to fetch external nodes list.", "error"); + console.error("Error fetching nodes:", xhr.responseText); + } + }); + } + + function renderNodes(nodes) { + const tableBody = $("#nodes_table tbody"); + tableBody.empty(); + + if (nodes && nodes.length > 0) { + $("#nodes_table").show(); + $("#no_nodes_message").hide(); + nodes.forEach(node => { + const row = ` + ${escapeHtml(node.name)} + ${escapeHtml(node.ip)} + + + + `; + tableBody.append(row); + }); + } else { + $("#nodes_table").hide(); + $("#no_nodes_message").show(); + } + } + + function addNode() { + if (!validateForm('add_node_form')) return; + + const name = $("#node_name").val().trim(); + const ip = $("#node_ip").val().trim(); + + confirmAction(`add the node '${name}'`, function () { + sendRequest( + API_URLS.addNode, + "POST", + { name: name, ip: ip }, + `Node '${name}' added successfully!`, + "#add_node_btn", + false, + function() { + $("#node_name").val(''); + $("#node_ip").val(''); + $("#add_node_form .form-control").removeClass('is-invalid'); + fetchNodes(); + } + ); + }); + } + + function deleteNode(nodeName) { + confirmAction(`delete the node '${nodeName}'`, function () { + sendRequest( + API_URLS.deleteNode, + "POST", + { name: nodeName }, + `Node '${nodeName}' deleted successfully!`, + null, + false, + fetchNodes + ); + }); + } + + function fetchExtraConfigs() { + $.ajax({ + url: API_URLS.getAllExtraConfigs, + type: "GET", + success: function (configs) { + renderExtraConfigs(configs); + }, + error: function(xhr) { + Swal.fire("Error!", "Failed to fetch extra configurations.", "error"); + console.error("Error fetching extra configs:", xhr.responseText); + } + }); + } + + function renderExtraConfigs(configs) { + const tableBody = $("#extra_configs_table tbody"); + tableBody.empty(); + + if (configs && configs.length > 0) { + $("#extra_configs_table").show(); + $("#no_extra_configs_message").hide(); + configs.forEach(config => { + const shortUri = config.uri.length > 50 ? config.uri.substring(0, 50) + '...' : config.uri; + const row = ` + ${escapeHtml(config.name)} + ${escapeHtml(shortUri)} + + + + `; + tableBody.append(row); + }); + } else { + $("#extra_configs_table").hide(); + $("#no_extra_configs_message").show(); + } + } + + function addExtraConfig() { + if (!validateForm('add_extra_config_form')) return; + + const name = $("#extra_config_name").val().trim(); + const uri = $("#extra_config_uri").val().trim(); + + confirmAction(`add the configuration '${name}'`, function () { + sendRequest( + API_URLS.addExtraConfig, + "POST", + { name: name, uri: uri }, + `Configuration '${name}' added successfully!`, + "#add_extra_config_btn", + false, + function() { + $("#extra_config_name").val(''); + $("#extra_config_uri").val(''); + $("#add_extra_config_form .form-control").removeClass('is-invalid'); + fetchExtraConfigs(); + } + ); + }); + } + + function deleteExtraConfig(configName) { + confirmAction(`delete the configuration '${configName}'`, function () { + sendRequest( + API_URLS.deleteExtraConfig, + "POST", + { name: configName }, + `Configuration '${configName}' deleted successfully!`, + null, + false, + fetchExtraConfigs + ); + }); + } + + function updateServiceUI(data) { + const servicesMap = { + "hysteria_telegram_bot": "#telegram_form", + "hysteria_normal_sub": "#normal_sub_service_form", + "hysteria_iplimit": "#ip-limit-service", + "hysteria_warp": "#warp_service" + }; + + Object.keys(servicesMap).forEach(serviceKey => { + let isRunning = data[serviceKey]; + + if (serviceKey === "hysteria_telegram_bot") { + const $form = $("#telegram_form"); + if (isRunning) { + $form.find('[data-group="start-only"]').hide(); + $("#telegram_start").hide(); + $("#telegram_stop").show(); + $("#telegram_save_interval").show(); + if ($form.find(".alert-info").length === 0) { + $form.prepend(`
Service is running. You can stop it or change the backup interval.
`); + } + fetchTelegramBackupInterval(); + } else { + $form.find('[data-group="start-only"]').show(); + $("#telegram_start").show(); + $("#telegram_stop").hide(); + $("#telegram_save_interval").hide(); + $form.find(".alert-info").remove(); + $("#telegram_backup_interval").val(""); + } + + } else if (serviceKey === "hysteria_normal_sub") { + const $normalForm = $("#normal_sub_service_form"); + const $normalFormGroups = $normalForm.find(".form-group"); + const $normalStartBtn = $("#normal_start"); + const $normalStopBtn = $("#normal_stop"); + const $normalSubConfigTabLi = $(".normal-sub-config-tab-li"); + + if (isRunning) { + $normalFormGroups.hide(); + $normalStartBtn.hide(); + $normalStopBtn.show(); + if ($normalForm.find(".alert-info").length === 0) { + $normalForm.prepend(`
NormalSub service is running. You can stop it or configure its subpath.
`); + } + $normalSubConfigTabLi.show(); + fetchNormalSubPath(); + } else { + $normalFormGroups.show(); + $normalStartBtn.show(); + $normalStopBtn.hide(); + $normalForm.find(".alert-info").remove(); + $normalSubConfigTabLi.hide(); + if ($('#normal-sub-config-link-tab').hasClass('active')) { + $('#normal-tab').tab('show'); + } + $("#normal_subpath_input").val(""); + $("#normal_subpath_input").removeClass('is-invalid'); + } + } else if (serviceKey === "hysteria_iplimit") { + const $ipLimitServiceForm = $("#ip_limit_service_form"); + const $configTabLi = $(".ip-limit-config-tab-li"); + if (isRunning) { + $("#ip_limit_start").hide(); + $("#ip_limit_stop").show(); + $configTabLi.show(); + fetchIpLimitConfig(); + if ($ipLimitServiceForm.find(".alert-info").length === 0) { + $ipLimitServiceForm.prepend(`
IP-Limit service is running. You can stop it if needed.
`); + } + } else { + $("#ip_limit_start").show(); + $("#ip_limit_stop").hide(); + $configTabLi.hide(); + if ($('#ip-limit-config-tab').hasClass('active')) { + $('#ip-limit-service-tab').tab('show'); + } + $ipLimitServiceForm.find(".alert-info").remove(); + $("#block_duration").val(""); + $("#max_ips").val(""); + $("#block_duration, #max_ips").removeClass('is-invalid'); + } + } else if (serviceKey === "hysteria_warp") { + const isWarpServiceRunning = data[serviceKey]; + if (isWarpServiceRunning) { + $("#warp_initial_controls").hide(); + $("#warp_active_controls").show(); + fetchWarpFullStatusAndConfig(); + } else { + $("#warp_initial_controls").show(); + $("#warp_active_controls").hide(); + if ($("#warp_config_form").length > 0) { + $("#warp_config_form")[0].reset(); + } + } + } + }); + } + + function fetchNormalSubPath() { + $.ajax({ + url: API_URLS.normalSubGetSubpath, + type: "GET", + success: function (data) { + $("#normal_subpath_input").val(data.subpath || ""); + if (data.subpath) { + $("#normal_subpath_input").removeClass('is-invalid'); + } + }, + error: function (xhr, status, error) { + console.error("Failed to fetch NormalSub subpath:", error, xhr.responseText); + $("#normal_subpath_input").val(""); + } + }); + } + + function fetchTelegramBackupInterval() { + $.ajax({ + url: API_URLS.telegramGetInterval, + type: "GET", + success: function (data) { + if (data.backup_interval) { + $("#telegram_backup_interval").val(data.backup_interval); + } else { + $("#telegram_backup_interval").val(""); + } + }, + error: function (xhr, status, error) { + console.error("Failed to fetch Telegram backup interval:", error, xhr.responseText); + $("#telegram_backup_interval").val(""); + } + }); + } + + function fetchIpLimitConfig() { + $.ajax({ + url: API_URLS.getIpLimitConfig, + type: "GET", + success: function (data) { + $("#block_duration").val(data.block_duration || ""); + $("#max_ips").val(data.max_ips || ""); + if (data.block_duration) $("#block_duration").removeClass('is-invalid'); + if (data.max_ips) $("#max_ips").removeClass('is-invalid'); + }, + error: function (xhr, status, error) { + console.error("Failed to fetch IP Limit config:", error, xhr.responseText); + $("#block_duration").val(""); + $("#max_ips").val(""); + } + }); + } + + function editNormalSubPath() { + if (!validateForm('normal_sub_config_form')) return; + const subpath = $("#normal_subpath_input").val(); + + confirmAction("change the NormalSub subpath to '" + subpath + "'", function () { + sendRequest( + API_URLS.normalSubEditSubpath, + "PUT", + { subpath: subpath }, + "NormalSub subpath updated successfully!", + "#normal_subpath_save_btn", + false, + fetchNormalSubPath + ); + }); + } + + function setupDecoy() { + if (!validateForm('decoy_form')) return; + const domain = $("#decoy_domain").val(); + const path = $("#decoy_path").val(); + confirmAction("set up the decoy site", function () { + sendRequest( + API_URLS.setupDecoy, + "POST", + { domain: domain, decoy_path: path }, + "Decoy site setup initiated successfully!", + "#decoy_setup", + false, + function() { setTimeout(fetchDecoyStatus, 1000); } + ); + }); + } + + function stopDecoy() { + confirmAction("stop the decoy site", function () { + sendRequest( + API_URLS.stopDecoy, + "POST", + null, + "Decoy site stop initiated successfully!", + "#decoy_stop", + false, + function() { setTimeout(fetchDecoyStatus, 1000); } + ); + }); + } + + function fetchDecoyStatus() { + $.ajax({ + url: API_URLS.getDecoyStatus, + type: "GET", + success: function (data) { + updateDecoyStatusUI(data); + }, + error: function (xhr, status, error) { + $("#decoy_status_message").html('
Failed to fetch decoy status.
'); + console.error("Failed to fetch decoy status:", error, xhr.responseText); + } + }); + } + + function updateDecoyStatusUI(data) { + const $form = $("#decoy_form"); + const $formGroups = $form.find(".form-group"); + const $setupBtn = $("#decoy_setup"); + const $stopBtn = $("#decoy_stop"); + const $alertInfo = $form.find(".alert-info"); + + if (data.active) { + $formGroups.hide(); + $setupBtn.hide(); + $stopBtn.show(); + if ($alertInfo.length === 0) { + $form.prepend(`
Decoy site is running. You can stop it if needed.
`); + } else { + $alertInfo.text('Decoy site is running. You can stop it if needed.'); + } + $("#decoy_status_message").html(` + Status: Active
+ Path: ${data.path || 'N/A'} + `); + } else { + $formGroups.show(); + $setupBtn.show(); + $stopBtn.hide(); + $alertInfo.remove(); + $("#decoy_status_message").html('Status: Not Active'); + } + } + + function fetchObfsStatus() { + $.ajax({ + url: API_URLS.checkObfs, + type: "GET", + success: function (data) { + updateObfsUI(data.obfs); + }, + error: function (xhr, status, error) { + $("#obfs_status_message").html('Failed to fetch OBFS status.'); + console.error("Failed to fetch OBFS status:", error, xhr.responseText); + $("#obfs_enable_btn").hide(); + $("#obfs_disable_btn").hide(); + } + }); + } + + function updateObfsUI(statusMessage) { + $("#obfs_status_message").text(statusMessage); + if (statusMessage === "OBFS is active.") { + $("#obfs_enable_btn").hide(); + $("#obfs_disable_btn").show(); + $("#obfs_status_container").removeClass("border-danger border-warning alert-danger alert-warning").addClass("border-success alert-success"); + } else if (statusMessage === "OBFS is not active.") { + $("#obfs_enable_btn").show(); + $("#obfs_disable_btn").hide(); + $("#obfs_status_container").removeClass("border-success border-danger alert-success alert-danger").addClass("border-warning alert-warning"); + } else { + $("#obfs_enable_btn").hide(); + $("#obfs_disable_btn").hide(); + $("#obfs_status_container").removeClass("border-success border-warning alert-success alert-warning").addClass("border-danger alert-danger"); + } + } + + function enableObfs() { + confirmAction("enable OBFS", function () { + sendRequest( + API_URLS.enableObfs, + "GET", + null, + "OBFS enabled successfully!", + "#obfs_enable_btn", + false, + fetchObfsStatus + ); + }); + } + + function disableObfs() { + confirmAction("disable OBFS", function () { + sendRequest( + API_URLS.disableObfs, + "GET", + null, + "OBFS disabled successfully!", + "#obfs_disable_btn", + false, + fetchObfsStatus + ); + }); + } + + function startTelegram() { + if (!validateForm('telegram_form')) return; + const apiToken = $("#telegram_api_token").val(); + const adminId = $("#telegram_admin_id").val(); + let backupInterval = $("#telegram_backup_interval").val(); + + const data = { + token: apiToken, + admin_id: adminId + }; + if (backupInterval) { + data.backup_interval = parseInt(backupInterval); + } + + confirmAction("start the Telegram bot", function () { + sendRequest( + API_URLS.telegramStart, + "POST", + data, + "Telegram bot started successfully!", + "#telegram_start" + ); + }); + } + + function stopTelegram() { + confirmAction("stop the Telegram bot", function () { + sendRequest( + API_URLS.telegramStop, + "DELETE", + null, + "Telegram bot stopped successfully!", + "#telegram_stop" + ); + }); + } + + function saveTelegramInterval() { + if (!validateForm('telegram_form')) return; + let backupInterval = $("#telegram_backup_interval").val(); + + if (!backupInterval) { + Swal.fire("Error!", "Backup interval cannot be empty.", "error"); + return; + } + + const data = { + backup_interval: parseInt(backupInterval) + }; + + confirmAction(`change the backup interval to ${backupInterval} hours`, function () { + sendRequest( + API_URLS.telegramSetInterval, + "POST", + data, + "Backup interval updated successfully!", + "#telegram_save_interval", + false, + fetchTelegramBackupInterval + ); + }); + } + + + function startNormal() { + if (!validateForm('normal_sub_service_form')) return; + const domain = $("#normal_domain").val(); + const port = $("#normal_port").val(); + confirmAction("start the normal subscription", function () { + sendRequest( + API_URLS.normalSubStart, + "POST", + { domain: domain, port: port }, + "Normal subscription started successfully!", + "#normal_start" + ); + }); + } + + function stopNormal() { + confirmAction("stop the normal subscription", function () { + sendRequest( + API_URLS.normalSubStop, + "DELETE", + null, + "Normal subscription stopped successfully!", + "#normal_stop" + ); + }); + } + + function changePort() { + if (!validateForm('port_form')) return; + const port = $("#hysteria_port").val(); + const url = API_URLS.setPortTemplate.replace("PORT_PLACEHOLDER", port); + confirmAction("change the port", function () { + sendRequest(url, "GET", null, "Port changed successfully!", "#port_change"); + }); + } + + function changeSNI() { + if (!validateForm('sni_form')) return; + const domain = $("#sni_domain").val(); + const url = API_URLS.setSniTemplate.replace("SNI_PLACEHOLDER", domain); + confirmAction("change the SNI", function () { + sendRequest(url, "GET", null, "SNI changed successfully!", "#sni_change"); + }); + } + + function saveIP() { + if (!validateForm('change_ip_form')) return; + const ipv4 = $("#ipv4").val().trim() || null; + const ipv6 = $("#ipv6").val().trim() || null; + confirmAction("save the new IP settings", function () { + sendRequest( + API_URLS.editIp, + "POST", + { ipv4: ipv4, ipv6: ipv6 }, + "IP settings saved successfully!", + "#ip_change" + ); + }); + } + + function downloadBackup() { + window.location.href = API_URLS.backup; + Swal.fire("Starting Download", "Your backup download should start shortly.", "info"); + } + + function uploadBackup() { + var fileInput = document.getElementById('backup_file'); + var file = fileInput.files[0]; + + if (!file) { + Swal.fire("Error!", "Please select a file to upload.", "error"); + return; + } + if (!file.name.toLowerCase().endsWith('.zip')) { + Swal.fire("Error!", "Only .zip files are allowed for restore.", "error"); + return; + } + + confirmAction(`restore the system from the selected backup file (${file.name})`, function() { + var formData = new FormData(); + formData.append('file', file); + + var progressBar = document.getElementById('backup_progress_bar'); + var progressContainer = progressBar.parentElement; + var statusDiv = document.getElementById('backup_status'); + + progressContainer.style.display = 'block'; + progressBar.style.width = '0%'; + progressBar.setAttribute('aria-valuenow', 0); + statusDiv.innerText = 'Uploading...'; + statusDiv.className = 'mt-2'; + + $.ajax({ + url: API_URLS.restore, + type: "POST", + data: formData, + processData: false, + contentType: false, + xhr: function() { + var xhr = new window.XMLHttpRequest(); + xhr.upload.addEventListener("progress", function(evt) { + if (evt.lengthComputable) { + var percentComplete = Math.round((evt.loaded / evt.total) * 100); + progressBar.style.width = percentComplete + '%'; + progressBar.setAttribute('aria-valuenow', percentComplete); + statusDiv.innerText = `Uploading... ${percentComplete}%`; + } + }, false); + return xhr; + }, + success: function(response) { + progressBar.style.width = '100%'; + progressBar.classList.add('bg-success'); + statusDiv.innerText = 'Backup restored successfully! Reloading page...'; + statusDiv.className = 'mt-2 text-success'; + Swal.fire("Success!", "Backup restored successfully!", "success").then(() => { + location.reload(); + }); + console.log("Restore Success:", response); + }, + error: function(xhr, status, error) { + progressBar.classList.add('bg-danger'); + let detail = (xhr.responseJSON && xhr.responseJSON.detail) ? xhr.responseJSON.detail : 'Check console for details.'; + statusDiv.innerText = `Error restoring backup: ${detail}`; + statusDiv.className = 'mt-2 text-danger'; + Swal.fire("Error!", `Failed to restore backup. ${detail}`, "error"); + console.error("Restore Error:", status, error, xhr.responseText); + }, + complete: function() { + fileInput.value = ''; + } + }); + }); + } + + function startIPLimit() { + confirmAction("start the IP Limit service", function () { + sendRequest( + API_URLS.startIpLimit, + "POST", + null, + "IP Limit service started successfully!", + "#ip_limit_start" + ); + }); + } + + function stopIPLimit() { + confirmAction("stop the IP Limit service", function () { + sendRequest( + API_URLS.stopIpLimit, + "POST", + null, + "IP Limit service stopped successfully!", + "#ip_limit_stop" + ); + }); + } + + function configIPLimit() { + if (!validateForm('ip_limit_config_form')) return; + const blockDuration = $("#block_duration").val(); + const maxIps = $("#max_ips").val(); + confirmAction("save the IP Limit configuration", function () { + sendRequest( + API_URLS.configIpLimit, + "POST", + { block_duration: parseInt(blockDuration), max_ips: parseInt(maxIps) }, + "IP Limit configuration saved successfully!", + "#ip_limit_change_config", + false, + fetchIpLimitConfig + ); + }); + } + + function fetchWarpFullStatusAndConfig() { + $.ajax({ + url: API_URLS.statusWarp, + type: "GET", + success: function (data) { + $("#warp_all_traffic").prop('checked', data.all_traffic_via_warp || false); + $("#warp_popular_sites").prop('checked', data.popular_sites_via_warp || false); + $("#warp_domestic_sites").prop('checked', data.domestic_sites_via_warp || false); + $("#warp_block_adult_sites").prop('checked', data.block_adult_content || false); + + $("#warp_initial_controls").hide(); + $("#warp_active_controls").show(); + }, + error: function (xhr, status, error) { + let errorMsg = "Failed to fetch WARP configuration."; + if (xhr.responseJSON && xhr.responseJSON.detail) { + errorMsg = xhr.responseJSON.detail; + } + console.error("Error fetching WARP config:", errorMsg, xhr.responseText); + + if (xhr.status === 404) { + $("#warp_initial_controls").show(); + $("#warp_active_controls").hide(); + if ($("#warp_config_form").length > 0) { + $("#warp_config_form")[0].reset(); + } + Swal.fire("Info", "WARP service might not be fully configured. Please try reinstalling if issues persist.", "info"); + } else { + if ($("#warp_config_form").length > 0) { + $("#warp_config_form")[0].reset(); + } + Swal.fire("Warning", "Could not load current WARP configuration values. Please check manually or re-save.", "warning"); + } + } + }); + } + + function updateGeo(country) { + const countryName = country.charAt(0).toUpperCase() + country.slice(1); + const buttonId = `#geo_update_${country}`; + const url = API_URLS.updateGeoTemplate.replace('COUNTRY_PLACEHOLDER', country); + + confirmAction(`update the Geo files for ${countryName}`, function () { + sendRequest( + url, + "GET", + null, + `Geo files for ${countryName} updated successfully!`, + buttonId, + false, + null + ); + }); + } + + $("#warp_start_btn").on("click", function() { + confirmAction("install and start WARP", function () { + sendRequest( + API_URLS.installWarp, + "POST", + null, + "WARP installation request sent. The page will reload.", + "#warp_start_btn", + true + ); + }); + }); + + $("#warp_stop_btn").on("click", function() { + confirmAction("stop and uninstall WARP", function () { + sendRequest( + API_URLS.uninstallWarp, + "DELETE", + null, + "WARP uninstallation request sent. The page will reload.", + "#warp_stop_btn", + true + ); + }); + }); + + $("#warp_save_config_btn").on("click", function() { + const configData = { + all: $("#warp_all_traffic").is(":checked"), + popular_sites: $("#warp_popular_sites").is(":checked"), + domestic_sites: $("#warp_domestic_sites").is(":checked"), + block_adult_sites: $("#warp_block_adult_sites").is(":checked") + }; + confirmAction("save WARP configuration", function () { + sendRequest( + API_URLS.configureWarp, + "POST", + configData, + "WARP configuration saved successfully!", + "#warp_save_config_btn", + false, + fetchWarpFullStatusAndConfig + ); + }); + }); + + $("#telegram_start").on("click", startTelegram); + $("#telegram_stop").on("click", stopTelegram); + $("#telegram_save_interval").on("click", saveTelegramInterval); + $("#normal_start").on("click", startNormal); + $("#normal_stop").on("click", stopNormal); + $("#normal_subpath_save_btn").on("click", editNormalSubPath); + $("#port_change").on("click", changePort); + $("#sni_change").on("click", changeSNI); + $("#ip_change").on("click", saveIP); + $("#download_backup").on("click", downloadBackup); + $("#upload_backup").on("click", uploadBackup); + $("#ip_limit_start").on("click", startIPLimit); + $("#ip_limit_stop").on("click", stopIPLimit); + $("#ip_limit_change_config").on("click", configIPLimit); + $("#decoy_setup").on("click", setupDecoy); + $("#decoy_stop").on("click", stopDecoy); + $("#obfs_enable_btn").on("click", enableObfs); + $("#obfs_disable_btn").on("click", disableObfs); + $("#add_node_btn").on("click", addNode); + $("#nodes_table").on("click", ".delete-node-btn", function() { + const nodeName = $(this).data("name"); + deleteNode(nodeName); + }); + $("#add_extra_config_btn").on("click", addExtraConfig); + $("#extra_configs_table").on("click", ".delete-extra-config-btn", function() { + const configName = $(this).data("name"); + deleteExtraConfig(configName); + }); + $("#geo_update_iran").on("click", function() { updateGeo('iran'); }); + $("#geo_update_china").on("click", function() { updateGeo('china'); }); + $("#geo_update_russia").on("click", function() { updateGeo('russia'); }); + + $('#normal_domain, #sni_domain, #decoy_domain').on('input', function () { + if (isValidDomain($(this).val())) { + $(this).removeClass('is-invalid'); + } else if ($(this).val().trim() !== "") { + $(this).addClass('is-invalid'); + } else { + $(this).removeClass('is-invalid'); + } + }); + + $('#normal_port, #hysteria_port').on('input', function () { + if (isValidPort($(this).val())) { + $(this).removeClass('is-invalid'); + } else if ($(this).val().trim() !== "") { + $(this).addClass('is-invalid'); + } else { + $(this).removeClass('is-invalid'); + } + }); + + $('#normal_subpath_input').on('input', function () { + if (isValidSubPath($(this).val())) { + $(this).removeClass('is-invalid'); + } else if ($(this).val().trim() !== "") { + $(this).addClass('is-invalid'); + } else { + $(this).removeClass('is-invalid'); + } + }); + + $('#ipv4, #ipv6, #node_ip').on('input', function () { + const isLocalIpField = $(this).attr('id') === 'ipv4' || $(this).attr('id') === 'ipv6'; + if (isLocalIpField && $(this).val().trim() === '') { + $(this).removeClass('is-invalid'); + } else if (isValidIPorDomain($(this).val())) { + $(this).removeClass('is-invalid'); + } else { + $(this).addClass('is-invalid'); + } + }); + + $('#node_name, #extra_config_name').on('input', function() { + if ($(this).val().trim() !== "") { + $(this).removeClass('is-invalid'); + } else { + $(this).addClass('is-invalid'); + } + }); + + $('#extra_config_uri').on('input', function () { + if (isValidURI($(this).val())) { + $(this).removeClass('is-invalid'); + } else if ($(this).val().trim() !== "") { + $(this).addClass('is-invalid'); + } + }); + + $('#telegram_api_token, #telegram_admin_id').on('input', function () { + if ($(this).val().trim() !== "") { + $(this).removeClass('is-invalid'); + } else { + $(this).addClass('is-invalid'); + } + }); + $('#block_duration, #max_ips, #telegram_backup_interval').on('input', function () { + if ($(this).attr('id') === 'telegram_backup_interval' && $(this).val().trim() === '') { + $(this).removeClass('is-invalid'); + return; + } + if (isValidPositiveNumber($(this).val())) { + $(this).removeClass('is-invalid'); + } else if ($(this).val().trim() !== "") { + $(this).addClass('is-invalid'); + } else { + $(this).addClass('is-invalid'); + } + }); + + $('#decoy_path').on('input', function () { + if (isValidPath($(this).val())) { + $(this).removeClass('is-invalid'); + } else if ($(this).val().trim() !== "") { + $(this).addClass('is-invalid'); + } else { + $(this).addClass('is-invalid'); + } + }); +}); \ No newline at end of file diff --git a/core/scripts/webpanel/assets/js/users.js b/core/scripts/webpanel/assets/js/users.js new file mode 100644 index 0000000..a8ec42e --- /dev/null +++ b/core/scripts/webpanel/assets/js/users.js @@ -0,0 +1,73 @@ +function updateServerInfo() { + const serverStatusUrl = document.querySelector('.content').dataset.serverStatusUrl; + fetch(serverStatusUrl) + .then(response => response.json()) + .then(data => { + document.getElementById('cpu-usage').textContent = data.cpu_usage; + document.getElementById('ram-usage').textContent = `${data.ram_usage} / ${data.total_ram}`; + document.getElementById('online-users').textContent = data.online_users; + document.getElementById('uptime').textContent = data.uptime; + + document.getElementById('server-ipv4').textContent = `IPv4: ${data.server_ipv4 || 'N/A'}`; + document.getElementById('server-ipv6').textContent = `IPv6: ${data.server_ipv6 || 'N/A'}`; + + document.getElementById('download-speed').textContent = `🔽 Download: ${data.download_speed}`; + document.getElementById('upload-speed').textContent = `🔼 Upload: ${data.upload_speed}`; + document.getElementById('tcp-connections').textContent = `TCP: ${data.tcp_connections}`; + document.getElementById('udp-connections').textContent = `UDP: ${data.udp_connections}`; + + document.getElementById('reboot-uploaded-traffic').textContent = data.reboot_uploaded_traffic; + document.getElementById('reboot-downloaded-traffic').textContent = data.reboot_downloaded_traffic; + document.getElementById('reboot-total-traffic').textContent = data.reboot_total_traffic; + + document.getElementById('user-uploaded-traffic').textContent = data.user_uploaded_traffic; + document.getElementById('user-downloaded-traffic').textContent = data.user_downloaded_traffic; + document.getElementById('user-total-traffic').textContent = data.user_total_traffic; + }) + .catch(error => console.error('Error fetching server info:', error)); +} + +function updateServiceStatuses() { + const servicesStatusUrl = document.querySelector('.content').dataset.servicesStatusUrl; + fetch(servicesStatusUrl) + .then(response => response.json()) + .then(data => { + updateServiceBox('hysteria2', data.hysteria_server); + updateServiceBox('telegrambot', data.hysteria_telegram_bot); + updateServiceBox('iplimit', data.hysteria_iplimit); + updateServiceBox('normalsub', data.hysteria_normal_sub); + }) + .catch(error => console.error('Error fetching service statuses:', error)); +} + +function updateServiceBox(serviceName, status) { + const statusElement = document.getElementById(serviceName + '-status'); + const statusBox = document.getElementById(serviceName + '-status-box'); + + if (status === true) { + statusElement.textContent = 'Active'; + statusBox.classList.remove('bg-danger'); + statusBox.classList.add('bg-success'); + } else { + statusElement.textContent = 'Inactive'; + statusBox.classList.remove('bg-success'); + statusBox.classList.add('bg-danger'); + } +} + +document.addEventListener('DOMContentLoaded', function () { + updateServerInfo(); + updateServiceStatuses(); + setInterval(updateServerInfo, 2000); + setInterval(updateServiceStatuses, 10000); + + const toggleIpBtn = document.getElementById('toggle-ip-visibility'); + const ipAddressesDiv = document.getElementById('ip-addresses'); + toggleIpBtn.addEventListener('click', function(e) { + e.preventDefault(); + const isBlurred = ipAddressesDiv.style.filter === 'blur(5px)'; + ipAddressesDiv.style.filter = isBlurred ? 'none' : 'blur(5px)'; + toggleIpBtn.querySelector('i').classList.toggle('fa-eye'); + toggleIpBtn.querySelector('i').classList.toggle('fa-eye-slash'); + }); +}); \ No newline at end of file