From 23f8975b3a164aa563b560bbf5bbe2be8e77415d Mon Sep 17 00:00:00 2001
From: ReturnFI <151555003+ReturnFI@users.noreply.github.com>
Date: Wed, 24 Sep 2025 20:18:57 +0000
Subject: [PATCH] Fix: user management JavaScript
---
core/scripts/webpanel/assets/js/users.js | 388 +++++++++++++++++++----
1 file changed, 322 insertions(+), 66 deletions(-)
diff --git a/core/scripts/webpanel/assets/js/users.js b/core/scripts/webpanel/assets/js/users.js
index a8ec42e..6b04d2c 100644
--- a/core/scripts/webpanel/assets/js/users.js
+++ b/core/scripts/webpanel/assets/js/users.js
@@ -1,73 +1,329 @@
-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;
+$(function () {
+ const contentSection = document.querySelector('.content');
+ const SERVICE_STATUS_URL = contentSection.dataset.serviceStatusUrl;
+ const BULK_REMOVE_URL = contentSection.dataset.bulkRemoveUrl;
+ const REMOVE_USER_URL_TEMPLATE = contentSection.dataset.removeUserUrlTemplate;
+ const BULK_ADD_URL = contentSection.dataset.bulkAddUrl;
+ const ADD_USER_URL = contentSection.dataset.addUserUrl;
+ const EDIT_USER_URL_TEMPLATE = contentSection.dataset.editUserUrlTemplate;
+ const RESET_USER_URL_TEMPLATE = contentSection.dataset.resetUserUrlTemplate;
+ const USER_URI_URL_TEMPLATE = contentSection.dataset.userUriUrlTemplate;
+ const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
- document.getElementById('server-ipv4').textContent = `IPv4: ${data.server_ipv4 || 'N/A'}`;
- document.getElementById('server-ipv6').textContent = `IPv6: ${data.server_ipv6 || 'N/A'}`;
+ const usernameRegex = /^[a-zA-Z0-9_]+$/;
+ let cachedUserData = [];
- 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');
+ function checkIpLimitServiceStatus() {
+ $.getJSON(SERVICE_STATUS_URL)
+ .done(data => {
+ if (data.hysteria_iplimit === true) {
+ $('.requires-iplimit-service').show();
+ }
+ })
+ .fail(() => console.error('Error fetching IP limit service status.'));
}
-}
-document.addEventListener('DOMContentLoaded', function () {
- updateServerInfo();
- updateServiceStatuses();
- setInterval(updateServerInfo, 2000);
- setInterval(updateServiceStatuses, 10000);
+ function validateUsername(inputElement, errorElement) {
+ const username = $(inputElement).val();
+ const isValid = usernameRegex.test(username);
+ $(errorElement).text(isValid ? "" : "Usernames can only contain letters, numbers, and underscores.");
+ $(inputElement).closest('form').find('button[type="submit"]').prop('disabled', !isValid);
+ }
- 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');
+ $('#addUsername, #addBulkPrefix, #editUsername').on('input', function() {
+ validateUsername(this, `#${this.id}Error`);
});
+
+ $(".filter-button").on("click", function (e) {
+ e.preventDefault();
+ const filter = $(this).data("filter");
+ $("#selectAll").prop("checked", false);
+ $("#userTable tbody tr.user-main-row").each(function () {
+ let showRow;
+ switch (filter) {
+ case "on-hold": showRow = $(this).find("td:eq(3) i").hasClass("text-warning"); break;
+ case "online": showRow = $(this).find("td:eq(3) i").hasClass("text-success"); break;
+ case "enable": showRow = $(this).find("td:eq(8) i").hasClass("text-success"); break;
+ case "disable": showRow = $(this).find("td:eq(8) i").hasClass("text-danger"); break;
+ default: showRow = true;
+ }
+ $(this).toggle(showRow).find(".user-checkbox").prop("checked", false);
+ if (!showRow) {
+ $(this).next('tr.user-details-row').hide();
+ }
+ });
+ });
+
+ $("#selectAll").on("change", function () {
+ $("#userTable tbody tr.user-main-row:visible .user-checkbox").prop("checked", this.checked);
+ });
+
+ $("#deleteSelected").on("click", function () {
+ const selectedUsers = $(".user-checkbox:checked").map((_, el) => $(el).val()).get();
+ if (selectedUsers.length === 0) {
+ return Swal.fire("Warning!", "Please select at least one user to delete.", "warning");
+ }
+ Swal.fire({
+ title: "Are you sure?",
+ html: `This will delete: ${selectedUsers.join(", ")}.
This action cannot be undone!`,
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#d33",
+ confirmButtonText: "Yes, delete them!",
+ }).then((result) => {
+ if (!result.isConfirmed) return;
+
+ if (selectedUsers.length > 1) {
+ $.ajax({
+ url: BULK_REMOVE_URL,
+ method: "POST",
+ contentType: "application/json",
+ data: JSON.stringify({ usernames: selectedUsers })
+ })
+ .done(() => Swal.fire("Success!", "Selected users have been deleted.", "success").then(() => location.reload()))
+ .fail((err) => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred while deleting users.", "error"));
+ } else {
+ const singleUrl = REMOVE_USER_URL_TEMPLATE.replace('U', selectedUsers[0]);
+ $.ajax({
+ url: singleUrl,
+ method: "DELETE"
+ })
+ .done(() => Swal.fire("Success!", "The user has been deleted.", "success").then(() => location.reload()))
+ .fail((err) => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred while deleting the user.", "error"));
+ }
+ });
+ });
+
+ $("#addUserForm, #addBulkUsersForm").on("submit", function (e) {
+ e.preventDefault();
+ const form = $(this);
+ const isBulk = form.attr('id') === 'addBulkUsersForm';
+ const url = isBulk ? BULK_ADD_URL : ADD_USER_URL;
+ const button = form.find('button[type="submit"]').prop('disabled', true);
+
+ const formData = new FormData(this);
+ const jsonData = Object.fromEntries(formData.entries());
+
+ jsonData.unlimited = jsonData.unlimited === 'on';
+
+ $.ajax({
+ url: url,
+ method: "POST",
+ contentType: "application/json",
+ data: JSON.stringify(jsonData),
+ })
+ .done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload()))
+ .fail(err => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred.", "error"))
+ .always(() => button.prop('disabled', false));
+ });
+
+ $("#editUserModal").on("show.bs.modal", function (event) {
+ const user = $(event.relatedTarget).data("user");
+ const clickedRow = $(event.relatedTarget).closest("tr");
+ const dataRow = clickedRow.hasClass('user-main-row') ? clickedRow : clickedRow.prev('.user-main-row');
+
+ const trafficText = dataRow.find("td:eq(4)").text();
+ const expiryText = dataRow.find("td:eq(6)").text();
+
+ $("#originalUsername").val(user);
+ $("#editUsername").val(user);
+ $("#editTrafficLimit").val(parseFloat(trafficText.split('/')[1]) || 0);
+ $("#editExpirationDays").val(parseInt(expiryText) || 0);
+ $("#editBlocked").prop("checked", !dataRow.find("td:eq(8) i").hasClass("text-success"));
+ $("#editUnlimitedIp").prop("checked", dataRow.find(".unlimited-ip-cell i").hasClass("text-primary"));
+ validateUsername('#editUsername', '#editUsernameError');
+ });
+
+ $("#editUserForm").on("submit", function (e) {
+ e.preventDefault();
+ const button = $("#editSubmitButton").prop("disabled", true);
+ const originalUsername = $("#originalUsername").val();
+ const url = EDIT_USER_URL_TEMPLATE.replace('U', originalUsername);
+
+ const formData = new FormData(this);
+ const jsonData = Object.fromEntries(formData.entries());
+ jsonData.blocked = jsonData.blocked === 'on';
+ jsonData.unlimited_ip = jsonData.unlimited_ip === 'on';
+ if (jsonData.new_username === originalUsername) delete jsonData.new_username;
+
+ $.ajax({
+ url: url,
+ method: "PATCH",
+ contentType: "application/json",
+ data: JSON.stringify(jsonData),
+ })
+ .done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload()))
+ .fail(err => Swal.fire("Error!", err.responseJSON?.detail, "error"))
+ .always(() => button.prop('disabled', false));
+ });
+
+ $("#userTable").on("click", ".reset-user, .delete-user", function () {
+ const button = $(this);
+ const username = button.data("user");
+ const isDelete = button.hasClass("delete-user");
+ const action = isDelete ? "delete" : "reset";
+ const urlTemplate = isDelete ? REMOVE_USER_URL_TEMPLATE : RESET_USER_URL_TEMPLATE;
+
+ Swal.fire({
+ title: `Are you sure you want to ${action}?`,
+ html: `This will ${action} user ${username}.`,
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#d33",
+ confirmButtonText: `Yes, ${action} it!`,
+ }).then((result) => {
+ if (!result.isConfirmed) return;
+ $.ajax({
+ url: urlTemplate.replace("U", encodeURIComponent(username)),
+ method: isDelete ? "DELETE" : "GET",
+ })
+ .done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload()))
+ .fail(() => Swal.fire("Error!", `Failed to ${action} user.`, "error"));
+ });
+ });
+
+ $("#qrcodeModal").on("show.bs.modal", function (event) {
+ const username = $(event.relatedTarget).data("username");
+ const qrcodesContainer = $("#qrcodesContainer").empty();
+ const url = USER_URI_URL_TEMPLATE.replace("U", encodeURIComponent(username));
+ $.getJSON(url, response => {
+ [
+ { type: "IPv4", link: response.ipv4 },
+ { type: "IPv6", link: response.ipv6 },
+ { type: "Normal-SUB", link: response.normal_sub }
+ ].forEach(config => {
+ if (!config.link) return;
+ const qrId = `qrcode-${config.type}`;
+ const card = $(`