1041 lines
49 KiB
HTML
1041 lines
49 KiB
HTML
{% 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 {% if users %}({{ users|length }}){% endif %}</h3>
|
||
<div class="card-tools d-flex align-items-center flex-wrap">
|
||
|
||
<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-info filter-button" data-filter="online">
|
||
<i class="fas fa-wifi"></i> Online
|
||
</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>
|
||
|
||
<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>
|
||
<button type="button" class="btn btn-sm btn-primary ml-2" data-toggle="modal" data-target="#addUserModal">
|
||
<i class="fas fa-plus"></i>
|
||
</button>
|
||
<button type="button" class="btn btn-sm btn-info ml-2" id="showSelectedLinks">
|
||
<i class="fas fa-link"></i>
|
||
</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>#</th>
|
||
<th>Status</th>
|
||
<th>Username</th>
|
||
<th>Traffic Usage</th>
|
||
<th class="text-nowrap">Expiry Date</th>
|
||
<th class="text-nowrap">Expiry Days</th>
|
||
<th>Enable</th>
|
||
<th class="text-nowrap requires-iplimit-service" style="display: none;">Unlimited IP</th>
|
||
<th class="text-nowrap">Configs</th>
|
||
<th class="text-nowrap">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for user in users|sort(attribute='username', case_sensitive=false) %}
|
||
<tr>
|
||
<td>
|
||
<input type="checkbox" class="user-checkbox" value="{{ user['username'] }}">
|
||
</td>
|
||
<td>{{ loop.index }}</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
|
||
{% elif user['status'] == "On-hold" %}
|
||
<i class="fas fa-circle text-warning"></i> On-hold
|
||
{% elif user['status'] == "Conflict" %}
|
||
<i class="fas fa-circle text-danger"></i> Conflict
|
||
{% else %}
|
||
<i class="fas fa-circle text-danger"></i> {{ user['status'] }}
|
||
{% endif %}
|
||
</td>
|
||
<td data-username="{{ user['username'] }}">{{ user['username'] }}</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="unlimited-ip-cell requires-iplimit-service" style="display: none;">
|
||
{% if user['unlimited_ip'] %}
|
||
<i class="fas fa-shield-alt text-primary"></i>
|
||
{% else %}
|
||
<i class="fas fa-times-circle text-muted"></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>
|
||
</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">
|
||
<ul class="nav nav-tabs" id="addUserTab" role="tablist">
|
||
<li class="nav-item">
|
||
<a class="nav-link active" id="single-user-tab" data-toggle="tab" href="#single-user" role="tab" aria-controls="single-user" aria-selected="true">Add User</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" id="bulk-users-tab" data-toggle="tab" href="#bulk-users" role="tab" aria-controls="bulk-users" aria-selected="false">Bulk Add</a>
|
||
</li>
|
||
</ul>
|
||
<div class="tab-content" id="addUserTabContent">
|
||
<div class="tab-pane fade show active" id="single-user" role="tabpanel" aria-labelledby="single-user-tab">
|
||
<form id="addUserForm" class="mt-3">
|
||
<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>
|
||
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
||
<input type="checkbox" class="form-check-input" id="addUnlimited" name="unlimited">
|
||
<label class="form-check-label" for="addUnlimited">Unlimited IP (Exempt from IP limit checks)</label>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" id="addSubmitButton">Add User</button>
|
||
</form>
|
||
</div>
|
||
<div class="tab-pane fade" id="bulk-users" role="tabpanel" aria-labelledby="bulk-users-tab">
|
||
<form id="addBulkUsersForm" class="mt-3">
|
||
<div class="form-group">
|
||
<label for="addBulkPrefix">Username Prefix</label>
|
||
<input type="text" class="form-control" id="addBulkPrefix" name="prefix" required>
|
||
<small class="form-text text-danger" id="addBulkPrefixError"></small>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group col-md-6">
|
||
<label for="addBulkCount">Number of Users</label>
|
||
<input type="number" class="form-control" id="addBulkCount" name="count" value="10" required>
|
||
</div>
|
||
<div class="form-group col-md-6">
|
||
<label for="addBulkStartNumber">Start Number</label>
|
||
<input type="number" class="form-control" id="addBulkStartNumber" name="start_number" value="1" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group col-md-6">
|
||
<label for="addBulkTrafficLimit">Traffic Limit (GB)</label>
|
||
<input type="number" class="form-control" id="addBulkTrafficLimit" name="traffic_gb" required>
|
||
</div>
|
||
<div class="form-group col-md-6">
|
||
<label for="addBulkExpirationDays">Expiration Days</label>
|
||
<input type="number" class="form-control" id="addBulkExpirationDays" name="expiration_days" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
||
<input type="checkbox" class="form-check-input" id="addBulkUnlimited" name="unlimited">
|
||
<label class="form-check-label" for="addBulkUnlimited">Unlimited IP (Exempt from IP limit checks)</label>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" id="addBulkSubmitButton">Add Bulk Users</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</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">
|
||
<label class="form-check-label" for="editBlocked">Blocked</label>
|
||
</div>
|
||
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
||
<input type="checkbox" class="form-check-input" id="editUnlimitedIp" name="unlimited_ip">
|
||
<label class="form-check-label" for="editUnlimitedIp">Unlimited IP (Exempt from IP limit checks)</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>
|
||
|
||
<!-- Show Links Modal -->
|
||
<div class="modal fade" id="showLinksModal" tabindex="-1" role="dialog" aria-labelledby="showLinksModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="showLinksModalLabel">Selected User Links</h5>
|
||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Here are the Normal-SUB links for the selected users:</p>
|
||
<textarea id="linksTextarea" class="form-control" rows="10" readonly></textarea>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||
<button type="button" class="btn btn-primary" id="copyLinksButton">Copy All</button>
|
||
</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 () {
|
||
|
||
function checkIpLimitServiceStatus() {
|
||
fetch('{{ url_for("server_services_status_api") }}')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.hysteria_iplimit === true) {
|
||
$('.requires-iplimit-service').show();
|
||
}
|
||
})
|
||
.catch(error => console.error('Error fetching IP limit service status:', error));
|
||
}
|
||
|
||
checkIpLimitServiceStatus();
|
||
|
||
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
||
|
||
function validateUsername(username, errorElementId) {
|
||
const errorElement = $("#" + errorElementId);
|
||
if (!username) {
|
||
errorElement.text("");
|
||
return false;
|
||
}
|
||
const isValid = usernameRegex.test(username);
|
||
|
||
if (!isValid) {
|
||
errorElement.text("Usernames can only contain letters, numbers, and underscores.");
|
||
return false;
|
||
} else {
|
||
errorElement.text("");
|
||
return true;
|
||
}
|
||
}
|
||
|
||
$("#addSubmitButton").prop("disabled", true);
|
||
|
||
$("#addUsername").on("input", function () {
|
||
const username = $(this).val();
|
||
const isValid = validateUsername(username, "addUsernameError");
|
||
$("#addSubmitButton").prop("disabled", !isValid);
|
||
});
|
||
|
||
$("#addBulkPrefix").on("input", function () {
|
||
const prefix = $(this).val();
|
||
const isValid = validateUsername(prefix, "addBulkPrefixError");
|
||
$("#addBulkSubmitButton").prop("disabled", !isValid);
|
||
});
|
||
|
||
$("#editUsername").on("input", function () {
|
||
const username = $(this).val();
|
||
const isValid = validateUsername(username, "editUsernameError");
|
||
$("#editSubmitButton").prop("disabled", !isValid);
|
||
});
|
||
|
||
$(".filter-button").on("click", function () {
|
||
const filter = $(this).data("filter");
|
||
|
||
$("#selectAll").prop("checked", false);
|
||
|
||
$("#userTable tbody tr").each(function () {
|
||
let showRow = true;
|
||
|
||
switch (filter) {
|
||
case "all":
|
||
showRow = true;
|
||
break;
|
||
case "not-active":
|
||
showRow = $(this).find("td:eq(2) i").hasClass("text-danger");
|
||
break;
|
||
case "online":
|
||
showRow = $(this).find("td:eq(2) i").hasClass("text-success");
|
||
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();
|
||
}
|
||
$(this).find(".user-checkbox").prop("checked", false);
|
||
});
|
||
});
|
||
|
||
$("#selectAll").on("change", function () {
|
||
$("#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) {
|
||
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",
|
||
});
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
$("#addUserForm").on("submit", function (e) {
|
||
e.preventDefault();
|
||
if (!validateUsername($("#addUsername").val(), "addUsernameError")) {
|
||
$("#addSubmitButton").prop("disabled", true);
|
||
return;
|
||
}
|
||
$("#addSubmitButton").prop("disabled", true);
|
||
|
||
const jsonData = {
|
||
username: $("#addUsername").val(),
|
||
traffic_limit: $("#addTrafficLimit").val(),
|
||
expiration_days: $("#addExpirationDays").val(),
|
||
unlimited: $("#addUnlimited").is(":checked")
|
||
};
|
||
|
||
$.ajax({
|
||
url: " {{ url_for('add_user_api') }} ",
|
||
method: "POST",
|
||
contentType: "application/json",
|
||
data: JSON.stringify(jsonData),
|
||
success: function (response) {
|
||
Swal.fire({
|
||
title: "Success!",
|
||
text: response.detail || "User added successfully!",
|
||
icon: "success",
|
||
confirmButtonText: "OK",
|
||
}).then(() => {
|
||
location.reload();
|
||
});
|
||
},
|
||
error: function (jqXHR, textStatus, errorThrown) {
|
||
let errorMessage = "An error occurred while adding user.";
|
||
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
|
||
errorMessage = jqXHR.responseJSON.detail;
|
||
} else if (jqXHR.status === 409) {
|
||
errorMessage = "User '" + jsonData.username + "' already exists.";
|
||
$("#addUsernameError").text(errorMessage);
|
||
} else if (jqXHR.status === 422) {
|
||
errorMessage = jqXHR.responseJSON.detail || "Invalid input provided.";
|
||
}
|
||
Swal.fire({
|
||
title: "Error!",
|
||
text: errorMessage,
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
});
|
||
$("#addSubmitButton").prop("disabled", false);
|
||
}
|
||
});
|
||
});
|
||
|
||
$("#addBulkUsersForm").on("submit", function(e) {
|
||
e.preventDefault();
|
||
if (!validateUsername($("#addBulkPrefix").val(), "addBulkPrefixError")) {
|
||
$("#addBulkSubmitButton").prop("disabled", true);
|
||
return;
|
||
}
|
||
$("#addBulkSubmitButton").prop("disabled", true);
|
||
|
||
const jsonData = {
|
||
prefix: $("#addBulkPrefix").val(),
|
||
count: parseInt($("#addBulkCount").val()),
|
||
start_number: parseInt($("#addBulkStartNumber").val()),
|
||
traffic_gb: parseFloat($("#addBulkTrafficLimit").val()),
|
||
expiration_days: parseInt($("#addBulkExpirationDays").val()),
|
||
unlimited: $("#addBulkUnlimited").is(":checked")
|
||
};
|
||
|
||
$.ajax({
|
||
url: "{{ url_for('add_bulk_users_api') }}",
|
||
method: "POST",
|
||
contentType: "application/json",
|
||
data: JSON.stringify(jsonData),
|
||
success: function(response) {
|
||
Swal.fire({
|
||
title: "Success!",
|
||
text: response.detail || "Bulk user creation started successfully!",
|
||
icon: "success",
|
||
confirmButtonText: "OK",
|
||
}).then(() => {
|
||
location.reload();
|
||
});
|
||
},
|
||
error: function(jqXHR) {
|
||
let errorMessage = "An error occurred during bulk user creation.";
|
||
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
|
||
errorMessage = jqXHR.responseJSON.detail;
|
||
}
|
||
Swal.fire({
|
||
title: "Error!",
|
||
text: errorMessage,
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
});
|
||
$("#addBulkSubmitButton").prop("disabled", false);
|
||
}
|
||
});
|
||
});
|
||
|
||
$(document).on("click", ".edit-user", function () {
|
||
const username = $(this).data("user");
|
||
const row = $(this).closest("tr");
|
||
const trafficUsageText = row.find("td:eq(4)").text().trim();
|
||
const expiryDaysText = row.find("td:eq(6)").text().trim();
|
||
const blocked = row.find("td:eq(7) i").hasClass("text-danger");
|
||
const unlimited_ip = row.find(".unlimited-ip-cell i").hasClass("text-primary");
|
||
|
||
const expiryDaysValue = (expiryDaysText.toLowerCase() === 'unlimited' || expiryDaysText.toLowerCase() === 'on-hold') ? 0 : parseInt(expiryDaysText, 10);
|
||
|
||
let trafficLimitValue = 0;
|
||
if (!trafficUsageText.toLowerCase().includes('/unlimited')) {
|
||
const parts = trafficUsageText.split('/');
|
||
if (parts.length > 1) {
|
||
const limitPart = parts[1].trim();
|
||
const match = limitPart.match(/^[\d.]+/);
|
||
if (match) {
|
||
trafficLimitValue = parseFloat(match[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
$("#originalUsername").val(username);
|
||
$("#editUsername").val(username);
|
||
$("#editTrafficLimit").val(trafficLimitValue);
|
||
$("#editExpirationDays").val(expiryDaysValue);
|
||
$("#editBlocked").prop("checked", blocked);
|
||
$("#editUnlimitedIp").prop("checked", unlimited_ip);
|
||
|
||
const isValid = validateUsername(username, "editUsernameError");
|
||
$("#editUserForm button[type='submit']").prop("disabled", !isValid);
|
||
});
|
||
|
||
$("#editUserForm").on("submit", function (e) {
|
||
e.preventDefault();
|
||
if (!validateUsername($("#editUsername").val(), "editUsernameError")) {
|
||
return;
|
||
}
|
||
$("#editSubmitButton").prop("disabled", true);
|
||
|
||
const jsonData = {
|
||
new_username: $("#editUsername").val(),
|
||
new_traffic_limit: $("#editTrafficLimit").val() || null,
|
||
new_expiration_days: $("#editExpirationDays").val() || null,
|
||
blocked: $("#editBlocked").is(":checked"),
|
||
unlimited_ip: $("#editUnlimitedIp").is(":checked")
|
||
};
|
||
|
||
if (jsonData.new_username === $("#originalUsername").val()) {
|
||
delete jsonData.new_username;
|
||
}
|
||
|
||
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 (response && response.detail) {
|
||
$("#editUserModal").modal("hide");
|
||
Swal.fire({
|
||
title: "Success!",
|
||
text: response.detail,
|
||
icon: "success",
|
||
confirmButtonText: "OK",
|
||
}).then(() => {
|
||
location.reload();
|
||
});
|
||
} else {
|
||
$("#editUserModal").modal("hide");
|
||
Swal.fire({
|
||
title: "Error!",
|
||
text: (response && response.error) || "An unknown error occurred.",
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
});
|
||
$("#editSubmitButton").prop("disabled", false);
|
||
}
|
||
},
|
||
error: function (jqXHR) {
|
||
let errorMessage = "An error occurred while updating user.";
|
||
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
|
||
errorMessage = jqXHR.responseJSON.detail;
|
||
}
|
||
Swal.fire({
|
||
title: "Error!",
|
||
text: errorMessage,
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
});
|
||
$("#editSubmitButton").prop("disabled", false);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 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 qrcodesContainer = $("#qrcodesContainer");
|
||
qrcodesContainer.empty();
|
||
|
||
const userUriApiUrl = "{{ url_for('show_user_uri_api', username='USERNAME_PLACEHOLDER') }}";
|
||
const url = userUriApiUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
||
|
||
$.ajax({
|
||
url: url,
|
||
method: "GET",
|
||
dataType: 'json',
|
||
success: function (response) {
|
||
const configs = [
|
||
{ type: "IPv4", link: response.ipv4 },
|
||
{ type: "IPv6", link: response.ipv6 },
|
||
{ type: "Normal-SUB", link: response.normal_sub }
|
||
];
|
||
|
||
configs.forEach(config => {
|
||
if (config.link) {
|
||
const displayType = config.type;
|
||
const configLink = config.link;
|
||
const qrCodeId = `qrcode-${displayType}-${Math.random().toString(36).substring(2, 10)}`;
|
||
|
||
const card = $(`
|
||
<div class="card d-inline-block my-2">
|
||
<div class="card-body">
|
||
<div id="${qrCodeId}" class="mx-auto cursor-pointer"></div>
|
||
<br>
|
||
<div class="config-type-text mt-2 text-center">${displayType}</div>
|
||
</div>
|
||
</div>
|
||
`);
|
||
qrcodesContainer.append(card);
|
||
|
||
const qrCodeStyling = new QRCodeStyling({
|
||
width: 180,
|
||
height: 180,
|
||
data: configLink,
|
||
margin: 5,
|
||
dotsOptions: {
|
||
color: "#212121",
|
||
type: "square"
|
||
},
|
||
cornersSquareOptions: {
|
||
color: "#212121",
|
||
type: "square"
|
||
},
|
||
backgroundOptions: {
|
||
color: "#FAFAFA",
|
||
},
|
||
imageOptions: {
|
||
hideBackgroundDots: true,
|
||
}
|
||
});
|
||
qrCodeStyling.append(document.getElementById(qrCodeId));
|
||
|
||
card.on("click", function () {
|
||
navigator.clipboard.writeText(configLink)
|
||
.then(() => {
|
||
Swal.fire({
|
||
icon: "success",
|
||
title: displayType + " 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.",
|
||
});
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
},
|
||
error: function (error) {
|
||
console.error("Error fetching user URI:", error);
|
||
Swal.fire({
|
||
title: "Error!",
|
||
text: "Failed to fetch user configuration URIs.",
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
});
|
||
|
||
$("#qrcodeModal .modal-content").on("click", function (e) {
|
||
e.stopPropagation();
|
||
});
|
||
|
||
$("#qrcodeModal").on("hidden.bs.modal", function () {
|
||
$("#qrcodesContainer").empty();
|
||
});
|
||
|
||
$("#qrcodeModal .close").on("click", function () {
|
||
$("#qrcodeModal").modal("hide");
|
||
});
|
||
|
||
$("#showSelectedLinks").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.",
|
||
icon: "warning",
|
||
confirmButtonText: "OK",
|
||
});
|
||
return;
|
||
}
|
||
|
||
Swal.fire({
|
||
title: 'Fetching links...',
|
||
text: 'Please wait.',
|
||
allowOutsideClick: false,
|
||
didOpen: () => {
|
||
Swal.showLoading();
|
||
}
|
||
});
|
||
|
||
const userUriApiUrlTemplate = "{{ url_for('show_user_uri_api', username='USERNAME_PLACEHOLDER') }}";
|
||
|
||
const fetchPromises = selectedUsers.map(username => {
|
||
const url = userUriApiUrlTemplate.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
||
return fetch(url).then(response => {
|
||
if (!response.ok) {
|
||
return { username: username, error: `HTTP error! status: ${response.status}` };
|
||
}
|
||
return response.json();
|
||
}).catch(error => {
|
||
return { username: username, error: error.message };
|
||
});
|
||
});
|
||
|
||
Promise.all(fetchPromises)
|
||
.then(results => {
|
||
Swal.close();
|
||
|
||
const successfulLinks = results
|
||
.filter(res => res && res.normal_sub)
|
||
.map(res => res.normal_sub);
|
||
|
||
const failedUsers = results
|
||
.filter(res => !res || !res.normal_sub)
|
||
.map(res => res.username);
|
||
|
||
if (failedUsers.length > 0) {
|
||
console.error("Failed to fetch links for:", failedUsers);
|
||
Swal.fire({
|
||
icon: 'warning',
|
||
title: 'Partial Success',
|
||
text: `Could not fetch links for the following users: ${failedUsers.join(', ')}`,
|
||
});
|
||
}
|
||
|
||
if (successfulLinks.length > 0) {
|
||
$("#linksTextarea").val(successfulLinks.join('\n'));
|
||
$("#showLinksModal").modal("show");
|
||
} else if (failedUsers.length === selectedUsers.length) {
|
||
Swal.fire({
|
||
icon: 'error',
|
||
title: 'Operation Failed',
|
||
text: 'Could not fetch links for any of the selected users.',
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
$("#copyLinksButton").on("click", function () {
|
||
const links = $("#linksTextarea").val();
|
||
if (links) {
|
||
navigator.clipboard.writeText(links)
|
||
.then(() => {
|
||
Swal.fire({
|
||
icon: "success",
|
||
title: "All links copied!",
|
||
showConfirmButton: false,
|
||
timer: 1500,
|
||
});
|
||
})
|
||
.catch(err => {
|
||
console.error("Failed to copy links: ", err);
|
||
Swal.fire({
|
||
icon: "error",
|
||
title: "Failed to copy links",
|
||
text: "Please copy manually.",
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
function filterUsers() {
|
||
const searchText = $("#searchInput").val().toLowerCase();
|
||
|
||
$("#userTable tbody tr").each(function () {
|
||
const username = $(this).find("td:eq(3)").text().toLowerCase();
|
||
if (username.includes(searchText)) {
|
||
$(this).show();
|
||
} else {
|
||
$(this).hide();
|
||
}
|
||
});
|
||
}
|
||
|
||
$('#addUserModal').on('show.bs.modal', function (event) {
|
||
$('#addUserForm')[0].reset();
|
||
$('#addBulkUsersForm')[0].reset();
|
||
$('#addUsernameError').text('');
|
||
$('#addBulkPrefixError').text('');
|
||
$('#addTrafficLimit').val('30');
|
||
$('#addExpirationDays').val('30');
|
||
$('#addBulkTrafficLimit').val('30');
|
||
$('#addBulkExpirationDays').val('30');
|
||
$('#addBulkStartNumber').val('1');
|
||
$('#addSubmitButton').prop('disabled', true);
|
||
$('#addBulkSubmitButton').prop('disabled', true);
|
||
$('#addUserModal a[data-toggle="tab"]').first().tab('show');
|
||
});
|
||
|
||
$("#searchButton").on("click", filterUsers);
|
||
$("#searchInput").on("keyup", filterUsers);
|
||
|
||
});
|
||
</script>
|
||
{% endblock %} |