Files
Blitz-Proxy/core/scripts/webpanel/templates/users.html
Whispering Wind aa712e3e6f feat: Enhance user management with multi-select, filters, validation, and UI improvements
- Implemented multi-user selection and deletion with confirmation.
- Added filter buttons (All, Not Active, Enable, Disable) with client-side logic.
- Fixed "Select All" checkbox to only affect visible users after filtering.
- Implemented real-time username validation (alphanumeric only).
- Added a disable to prevent submit validation error
- Ensured that select all deselect button, work with filters and sorting
- Improve UI to be more intuitive.
-Improved responsivity buttons in smaller devices
2025-02-18 13:19:11 +03:30

782 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Users{% endblock %}
{% block content %}
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">Users <small class="font-weight-light"></small></h1>
</div>
</div>
</div>
</div>
<section class="content">
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h3 class="card-title">User List</h3>
<div class="card-tools d-flex align-items-center flex-wrap">
<!-- Filter Buttons -->
<div class="mr-2 mb-2">
<button type="button" class="btn btn-sm btn-default filter-button" data-filter="all">
<i class="fas fa-list"></i> All
</button>
</div>
<div class="mr-2 mb-2">
<button type="button" class="btn btn-sm btn-default filter-button" data-filter="not-active">
<i class="fas fa-exclamation-triangle"></i> NA
</button>
</div>
<div class="mr-2 mb-2">
<button type="button" class="btn btn-sm btn-success filter-button" data-filter="enable">
<i class="fas fa-check"></i> Enable
</button>
</div>
<div class="mr-2 mb-2">
<button type="button" class="btn btn-sm btn-danger filter-button" data-filter="disable">
<i class="fas fa-ban"></i> Disable
</button>
</div>
<!-- Search Form -->
<div class="input-group input-group-sm" style="width: 200px;">
<input type="text" id="searchInput" class="form-control float-right" placeholder="Search">
<div class="input-group-append">
<button type="button" class="btn btn-default" id="searchButton">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<!-- Add User Button -->
<button type="button" class="btn btn-sm btn-primary ml-2" data-toggle="modal" data-target="#addUserModal">
<i class="fas fa-plus"></i>
</button>
<!-- Delete Selected Button -->
<button type="button" class="btn btn-sm btn-danger ml-2" id="deleteSelected">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="card-body table-responsive p-0">
{% if users|length == 0 %}
<div class="alert alert-warning" role="alert" style="margin: 20px;">
No users found.
</div>
{% else %}
<table class="table table-bordered table-hover" id="userTable">
<thead>
<tr>
<th>
<input type="checkbox" id="selectAll">
</th>
<th>Status</th>
<th>Username</th>
<th>Quota</th>
<th>Used</th>
<th class="text-nowrap">Expiry Date</th>
<th class="text-nowrap">Expiry Days</th>
<th>Enable</th>
<th class="text-nowrap">Configs</th>
<th class="text-nowrap">Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
<input type="checkbox" class="user-checkbox" value="{{ user.username }}">
</td>
<td>
{% if user['status'] == "Online" %}
<i class="fas fa-circle text-success"></i> Online
{% elif user['status'] == "Offline" %}
<i class="fas fa-circle text-secondary"></i> Offline
{% else %}
<i class="fas fa-circle text-danger"></i> {{ user['status'] }}
{% endif %}
</td>
<td data-username="{{ user.username }}">{{ user.username }}</td>
<td>{{ user.quota }}</td>
<td>{{ user.traffic_used }}</td>
<td>{{ user.expiry_date }}</td>
<td>{{ user.expiry_days }}</td>
<td>
{% if user.enable %}
<i class="fas fa-check-circle text-success"></i>
{% else %}
<i class="fas fa-times-circle text-danger"></i>
{% endif %}
</td>
<td class="text-nowrap">
<a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal"
data-username="{{ user.username }}">
<i class="fas fa-qrcode"></i>
</a>
<div id="userConfigs-{{ user.username }}" style="display: none;">
{% for config in user.configs %}
<div class="config-container" data-link="{{ config.link }}">
<span class="config-type">{{ config.type }}:</span>
{% if config.type == "Singbox" or config.type == "Normal-SUB" %}
<span class="config-link-text">{{ config.link }}</span>
{% else %}
<span class="config-link-text">{{ config.link }}</span>
{% endif %}
</div>
{% endfor %}
</div>
</td>
<td class="text-nowrap">
<button type="button" class="btn btn-sm btn-info edit-user"
data-user='{{ user.username }}' data-toggle="modal"
data-target="#editUserModal">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-sm btn-warning reset-user"
data-user='{{ user.username }}'>
<i class="fas fa-undo"></i>
</button>
<button type="button" class="btn btn-sm btn-danger delete-user"
data-user='{{ user.username }}'>
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="row mt-3">
<div class="col-sm-12 col-md-7">
<div class="dataTables_paginate paging_simple_numbers" id="userTable_paginate">
{# {{ pagination.links }} #}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1" role="dialog" aria-labelledby="addUserModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addUserModalLabel">Add User</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form id="addUserForm">
<div class="form-group">
<label for="addUsername">Username</label>
<input type="text" class="form-control" id="addUsername" name="username" required>
<small class="form-text text-danger" id="addUsernameError"></small>
</div>
<div class="form-group">
<label for="addTrafficLimit">Traffic Limit (GB)</label>
<input type="number" class="form-control" id="addTrafficLimit" name="traffic_limit" required>
</div>
<div class="form-group">
<label for="addExpirationDays">Expiration Days</label>
<input type="number" class="form-control" id="addExpirationDays" name="expiration_days"
required>
</div>
<button type="submit" class="btn btn-primary" id="addSubmitButton">Add User</button>
</form>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editUserModalLabel">Edit User</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form id="editUserForm">
<div class="form-group">
<label for="editUsername">Username</label>
<input type="text" class="form-control" id="editUsername" name="new_username">
<small class="form-text text-danger" id="editUsernameError"></small>
</div>
<div class="form-group">
<label for="editTrafficLimit">Traffic Limit (GB)</label>
<input type="number" class="form-control" id="editTrafficLimit" name="new_traffic_limit">
</div>
<div class="form-group">
<label for="editExpirationDays">Expiration Days</label>
<input type="number" class="form-control" id="editExpirationDays" name="new_expiration_days">
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="editBlocked" name="blocked" value="true">
<label class="form-check-label" for="editBlocked">Blocked</label>
</div>
<input type="hidden" id="originalUsername" name="username">
<button type="submit" class="btn btn-primary" id="editSubmitButton">Save Changes</button>
</form>
</div>
</div>
</div>
</div>
<!-- QR Code Modal -->
<div class="modal fade" id="qrcodeModal" tabindex="-1" role="dialog" aria-labelledby="qrcodeModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="qrcodeModalLabel">QR Codes</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body text-center">
<div id="qrcodesContainer" class="mx-auto"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
<!-- Include qr-code-styling library -->
<script src="https://cdn.jsdelivr.net/npm/qr-code-styling/lib/qr-code-styling.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$(function () {
//** Username validation */
const usernameRegex = /^[a-zA-Z0-9]+$/;
function validateUsername(username, errorElementId) {
const isValid = usernameRegex.test(username);
const errorElement = $("#" + errorElementId);
if (!isValid) {
errorElement.text("Usernames can only contain letters and numbers.");
return false;
} else {
errorElement.text(""); // Clear any previous error
return true;
}
}
// Disable submit buttons by default
$("#addSubmitButton").prop("disabled", true);
$("#editSubmitButton").prop("disabled", true);
//** Add Username validation on Add User Modal */
$("#addUsername").on("input", function () {
const username = $(this).val();
const isValid = validateUsername(username, "addUsernameError");
$("#addUserForm button[type='submit']").prop("disabled", !isValid);
$("#addSubmitButton").prop("disabled", !isValid);
});
//** Add Username validation on Edit User Modal */
$("#editUsername").on("input", function () {
const username = $(this).val();
const isValid = validateUsername(username, "editUsernameError");
$("#editUserForm button[type='submit']").prop("disabled", !isValid);
$("#editSubmitButton").prop("disabled", !isValid);
});
// Filter Buttons Functionality
$(".filter-button").on("click", function () {
const filter = $(this).data("filter");
// Deselect "Select All" checkbox when a filter is applied
$("#selectAll").prop("checked", false);
$("#userTable tbody tr").each(function () {
let showRow = true;
switch (filter) {
case "all":
showRow = true; // Show all users
break;
case "not-active":
showRow = $(this).find("td:eq(1) i").hasClass("text-danger");
break;
case "enable":
showRow = $(this).find("td:eq(7) i").hasClass("text-success");
break;
case "disable":
showRow = $(this).find("td:eq(7) i").hasClass("text-danger");
break;
}
if (showRow) {
$(this).show();
} else {
$(this).hide();
}
//Deselect checkbox when is not visible after sorting
$(this).find(".user-checkbox").prop("checked", false);
});
});
// Multi Delete Functionality
$("#selectAll").on("change", function () {
// Only select checkboxes in visible rows
$("#userTable tbody tr:visible .user-checkbox").prop("checked", $(this).prop("checked"));
});
$("#deleteSelected").on("click", function () {
const selectedUsers = $(".user-checkbox:checked").map(function () {
return $(this).val();
}).get();
if (selectedUsers.length === 0) {
Swal.fire({
title: "Warning!",
text: "Please select at least one user to delete.",
icon: "warning",
confirmButtonText: "OK",
});
return;
}
Swal.fire({
title: "Are you sure?",
html: `This will delete the selected users: <b>${selectedUsers.join(", ")}</b>.<br>This action cannot be undone!`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete them!",
}).then((result) => {
if (result.isConfirmed) {
//AJAX request for each user selected
Promise.all(selectedUsers.map(username => {
const removeUserUrl = "{{ url_for('remove_user_api', username='USERNAME_PLACEHOLDER') }}";
const url = removeUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
return $.ajax({
url: url,
method: "DELETE",
contentType: "application/json",
data: JSON.stringify({ username: username }),
});
}))
.then(responses => {
const allSuccessful = responses.every(response => response.detail);
if (allSuccessful) {
Swal.fire({
title: "Success!",
text: "Selected users deleted successfully!",
icon: "success",
confirmButtonText: "OK",
}).then(() => {
location.reload();
});
} else {
Swal.fire({
title: "Error!",
text: "Failed to delete some users.",
icon: "error",
confirmButtonText: "OK",
});
}
})
.catch(error => {
console.error("Error deleting users:", error);
Swal.fire({
title: "Error!",
text: "An error occurred while deleting users.",
icon: "error",
confirmButtonText: "OK",
});
});
}
});
});
// Add User Form Submit
$("#addUserForm").on("submit", function (e) {
e.preventDefault();
// Additional check before submitting (in case JS is disabled briefly)
if (!validateUsername($("#addUsername").val(), "addUsernameError")) {
return;
}
const formData = $(this).serializeArray();
const jsonData = {};
formData.forEach(field => {
jsonData[field.name] = field.value;
});
$.ajax({
url: " {{ url_for('add_user_api') }} ",
method: "POST",
contentType: "application/json",
data: JSON.stringify(jsonData),
success: function (response) {
if (response.detail) {
Swal.fire({
title: "Success!",
text: response.detail,
icon: "success",
confirmButtonText: "OK",
}).then(() => {
location.reload();
});
} else {
Swal.fire({
title: "Error!",
text: response.error || "Failed to add user",
icon: "error",
confirmButtonText: "OK",
});
}
},
error: function () {
Swal.fire({
title: "Error!",
text: "An error occurred while adding user",
icon: "error",
confirmButtonText: "OK",
});
}
});
});
// Edit User Form Populate and Submit
$(document).on("click", ".edit-user", function () {
const username = $(this).data("user");
const row = $(this).closest("tr");
const quota = row.find("td:eq(3)").text().trim();
const expiry = row.find("td:eq(5)").text().trim(); // Get expiry from the table
const expiry_days = row.find("td:eq(6)").text().trim();
const blocked = row.find("td:eq(7)").text().trim().toLowerCase() === 'disabled'; // Check if 'disabled'
// Extract numeric values from quota and expiry strings
const quotaValue = parseFloat(quota);
const expiryValue = parseInt(expiry); // Parse expiry as integer
// Populate the modal fields
$("#originalUsername").val(username);
$("#editUsername").val(username);
$("#editTrafficLimit").val(quotaValue);
$("#editExpirationDays").val(expiry_days);
$("#editBlocked").prop("checked", blocked);
});
$("#editUserForm").on("submit", function (e) {
e.preventDefault();
// Additional check before submitting (in case JS is disabled briefly)
if (!validateUsername($("#editUsername").val(), "editUsernameError")) {
return;
}
const formData = $(this).serializeArray();
const jsonData = {};
formData.forEach(field => {
jsonData[field.name] = field.value;
});
const editUserUrl = "{{ url_for('edit_user_api', username='USERNAME_PLACEHOLDER') }}";
const url = editUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent($("#originalUsername").val()));
$.ajax({
url: url,
method: "PATCH",
contentType: "application/json",
data: JSON.stringify(jsonData),
success: function (response) {
if (typeof response === 'string' && response.includes("User updated successfully")) {
const username = $("#originalUsername").val();
const row = $(`td[data-username='${username}']`).closest("tr");
row.find("td:eq(2)").text($("#editUsername").val());
row.find("td:eq(3)").text($("#editTrafficLimit").val() + " GB");
row.find("td:eq(6)").text($("#editExpirationDays").val());
row.find("td:eq(7) i")
.removeClass()
.addClass(
$("#editBlocked").prop("checked")
? "fas fa-times-circle text-danger"
: "fas fa-check-circle text-success"
);
// Hide the modal
$("#editUserModal").modal("hide");
Swal.fire({
title: "Success!",
text: "User updated successfully!",
icon: "success",
confirmButtonText: "OK",
});
}
else if (response && response.detail) {
const username = $("#originalUsername").val();
const row = $(`td[data-username='${username}']`).closest("tr");
row.find("td:eq(2)").text($("#editUsername").val());
row.find("td:eq(3)").text($("#editTrafficLimit").val() + " GB");
row.find("td:eq(6)").text($("#editExpirationDays").val());
row.find("td:eq(7) i")
.removeClass()
.addClass(
$("#editBlocked").prop("checked")
? "fas fa-times-circle text-danger"
: "fas fa-check-circle text-success"
);
// Hide the modal
$("#editUserModal").modal("hide");
// Show a success message
Swal.fire({
title: "Success!",
text: response.detail,
icon: "success",
confirmButtonText: "OK",
});
} else {
$("#editUserModal").modal("hide");
Swal.fire({
title: "Error!",
text: response.error || "An error occurred.",
icon: "error",
confirmButtonText: "OK",
});
}
},
error: function (error) {
console.error(error);
Swal.fire({
title: "Error!",
text: "An error occurred while updating user",
icon: "error",
confirmButtonText: "OK",
});
}
});
});
// Prevent click event on the submit button from triggering form submission
$("#editUserForm button[type='submit']").on("click", function (e) {
e.preventDefault();
$(this).closest("form").submit();
});
// Reset User Button Click
$("#userTable").on("click", ".reset-user", function () {
const username = $(this).data("user");
Swal.fire({
title: "Are you sure?",
html: `This will reset <b>${username}</b>'s data.<br>This action cannot be undone!`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, reset it!",
}).then((result) => {
if (result.isConfirmed) {
const resetUserUrl = "{{ url_for('reset_user_api', username='USERNAME_PLACEHOLDER') }}";
const url = resetUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
$.ajax({
url: url,
method: "GET",
contentType: "application/json",
data: JSON.stringify({ username: username }),
success: function (response) {
if (response.detail) {
Swal.fire({
title: "Success!",
text: response.detail,
icon: "success",
confirmButtonText: "OK",
}).then(() => {
location.reload();
});
} else {
Swal.fire({
title: "Error!",
text: response.error || "Failed to reset user",
icon: "error",
confirmButtonText: "OK",
});
}
},
error: function () {
Swal.fire({
title: "Error!",
text: "An error occurred while resetting user",
icon: "error",
confirmButtonText: "OK",
});
}
});
}
});
});
// Delete User Button Click
$("#userTable").on("click", ".delete-user", function () {
const username = $(this).data("user");
Swal.fire({
title: "Are you sure?",
html: `This will delete the user <b>${username}</b>.<br>This action cannot be undone!`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!",
}).then((result) => {
if (result.isConfirmed) {
const removeUserUrl = "{{ url_for('remove_user_api', username='USERNAME_PLACEHOLDER') }}";
const url = removeUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
$.ajax({
url: url,
method: "DELETE",
contentType: "application/json",
data: JSON.stringify({ username: username }),
success: function (response) {
if (response.detail) {
Swal.fire({
title: "Success!",
text: response.detail,
icon: "success",
confirmButtonText: "OK",
}).then(() => {
location.reload();
});
} else {
Swal.fire({
title: "Error!",
text: response.error || "Failed to delete user",
icon: "error",
confirmButtonText: "OK",
});
}
},
error: function () {
Swal.fire({
title: "Error!",
text: "An error occurred while deleting user",
icon: "error",
confirmButtonText: "OK",
});
}
});
}
});
});
// QR Code Modal
$("#qrcodeModal").on("show.bs.modal", function (event) {
const button = $(event.relatedTarget);
const username = button.data("username");
const configContainer = $(`#userConfigs-${username}`);
const qrcodesContainer = $("#qrcodesContainer");
qrcodesContainer.empty();
configContainer.find(".config-container").each(function () {
const configLink = $(this).data("link");
const configType = $(this).find(".config-type").text().replace(":", "");
// Create a card for each QR code
const card = $(`
<div class="card d-inline-block mx-2 my-2" style="width: 180px;">
<div class="card-body">
<div id="qrcode-${configType}" class="mx-auto cursor-pointer"></div>
<div class="config-type-text mt-2 text-center">${configType}</div>
</div>
</div>
`);
qrcodesContainer.append(card);
const qrCodeStyling = new QRCodeStyling({
width: 150,
height: 150,
data: configLink,
dotsOptions: {
color: "#212121",
type: "square"
},
cornersSquareOptions: {
color: "#212121",
type: "square"
},
backgroundOptions: {
color: "#FAFAFA",
},
imageOptions: {
hideBackgroundDots: true,
}
});
qrCodeStyling.append(document.getElementById(`qrcode-${configType}`));
// Add click to copy functionality to the card
card.on("click", function () {
navigator.clipboard.writeText(configLink)
.then(() => {
Swal.fire({
icon: "success",
title: configType + " link copied!",
showConfirmButton: false,
timer: 1500,
});
})
.catch(err => {
console.error("Failed to copy link: ", err);
Swal.fire({
icon: "error",
title: "Failed to copy link",
text: "Please copy manually.",
});
});
});
});
});
// Prevent modal from closing when clicking inside
$("#qrcodeModal .modal-content").on("click", function (e) {
e.stopPropagation();
});
// Clear the QR code when the modal is hidden
$("#qrcodeModal").on("hidden.bs.modal", function () {
$("#qrcodesContainer").empty();
});
$("#qrcodeModal .close").on("click", function () {
$("#qrcodeModal").modal("hide");
});
// Search Functionality
function filterUsers() {
const searchText = $("#searchInput").val().toLowerCase();
$("#userTable tbody tr").each(function () {
const username = $(this).find("td:eq(2)").text().toLowerCase();
if (username.includes(searchText)) {
$(this).show();
} else {
$(this).hide();
}
});
}
$("#searchButton").on("click", filterUsers);
$("#searchInput").on("keyup", filterUsers);
});
</script>
{% endblock %}