feat(web): Add unlimited IP controls to users page

This commit is contained in:
Whispering Wind
2025-08-12 23:41:15 +03:30
committed by GitHub
parent 644c43b504
commit 6c6576ae87

View File

@ -76,6 +76,7 @@
<th class="text-nowrap">Expiry Date</th> <th class="text-nowrap">Expiry Date</th>
<th class="text-nowrap">Expiry Days</th> <th class="text-nowrap">Expiry Days</th>
<th>Enable</th> <th>Enable</th>
<th class="text-nowrap">Unlimited IP</th>
<th class="text-nowrap">Configs</th> <th class="text-nowrap">Configs</th>
<th class="text-nowrap">Actions</th> <th class="text-nowrap">Actions</th>
</tr> </tr>
@ -84,7 +85,7 @@
{% for user in users|sort(attribute='username', case_sensitive=false) %} {% for user in users|sort(attribute='username', case_sensitive=false) %}
<tr> <tr>
<td> <td>
<input type="checkbox" class="user-checkbox" value="{{ user.username }}"> <input type="checkbox" class="user-checkbox" value="{{ user['username'] }}">
</td> </td>
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td> <td>
@ -96,35 +97,42 @@
<i class="fas fa-circle text-danger"></i> {{ user['status'] }} <i class="fas fa-circle text-danger"></i> {{ user['status'] }}
{% endif %} {% endif %}
</td> </td>
<td data-username="{{ user.username }}">{{ user.username }}</td> <td data-username="{{ user['username'] }}">{{ user['username'] }}</td>
<td>{{ user.traffic_used }}</td> <td>{{ user['traffic_used'] }}</td>
<td>{{ user.expiry_date }}</td> <td>{{ user['expiry_date'] }}</td>
<td>{{ user.expiry_days }}</td> <td>{{ user['expiry_days'] }}</td>
<td> <td>
{% if user.enable %} {% if user['enable'] %}
<i class="fas fa-check-circle text-success"></i> <i class="fas fa-check-circle text-success"></i>
{% else %} {% else %}
<i class="fas fa-times-circle text-danger"></i> <i class="fas fa-times-circle text-danger"></i>
{% endif %} {% endif %}
</td> </td>
<td>
{% 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"> <td class="text-nowrap">
<a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal" <a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal"
data-username="{{ user.username }}"> data-username="{{ user['username'] }}">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
</a> </a>
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
<button type="button" class="btn btn-sm btn-info edit-user" <button type="button" class="btn btn-sm btn-info edit-user"
data-user='{{ user.username }}' data-toggle="modal" data-user="{{ user['username'] }}" data-toggle="modal"
data-target="#editUserModal"> data-target="#editUserModal">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
<button type="button" class="btn btn-sm btn-warning reset-user" <button type="button" class="btn btn-sm btn-warning reset-user"
data-user='{{ user.username }}'> data-user="{{ user['username'] }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</button> </button>
<button type="button" class="btn btn-sm btn-danger delete-user" <button type="button" class="btn btn-sm btn-danger delete-user"
data-user='{{ user.username }}'> data-user="{{ user['username'] }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</td> </td>
@ -173,6 +181,10 @@
<input type="number" class="form-control" id="addExpirationDays" name="expiration_days" <input type="number" class="form-control" id="addExpirationDays" name="expiration_days"
required> required>
</div> </div>
<div class="form-check mb-3">
<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> <button type="submit" class="btn btn-primary" id="addSubmitButton">Add User</button>
</form> </form>
</div> </div>
@ -206,9 +218,13 @@
<input type="number" class="form-control" id="editExpirationDays" name="new_expiration_days"> <input type="number" class="form-control" id="editExpirationDays" name="new_expiration_days">
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="editBlocked" name="blocked" value="true"> <input type="checkbox" class="form-check-input" id="editBlocked" name="blocked">
<label class="form-check-label" for="editBlocked">Blocked</label> <label class="form-check-label" for="editBlocked">Blocked</label>
</div> </div>
<div class="form-check mb-3">
<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"> <input type="hidden" id="originalUsername" name="username">
<button type="submit" class="btn btn-primary" id="editSubmitButton">Save Changes</button> <button type="submit" class="btn btn-primary" id="editSubmitButton">Save Changes</button>
</form> </form>
@ -259,7 +275,6 @@
} }
$("#addSubmitButton").prop("disabled", true); $("#addSubmitButton").prop("disabled", true);
// $("#editSubmitButton").prop("disabled", true);
$("#addUsername").on("input", function () { $("#addUsername").on("input", function () {
const username = $(this).val(); const username = $(this).val();
@ -387,11 +402,12 @@
} }
$("#addSubmitButton").prop("disabled", true); $("#addSubmitButton").prop("disabled", true);
const formData = $(this).serializeArray(); const jsonData = {
const jsonData = {}; username: $("#addUsername").val(),
formData.forEach(field => { traffic_limit: $("#addTrafficLimit").val(),
jsonData[field.name] = field.value; expiration_days: $("#addExpirationDays").val(),
}); unlimited: $("#addUnlimited").is(":checked")
};
$.ajax({ $.ajax({
url: " {{ url_for('add_user_api') }} ", url: " {{ url_for('add_user_api') }} ",
@ -435,6 +451,7 @@
const trafficUsageText = row.find("td:eq(4)").text().trim(); const trafficUsageText = row.find("td:eq(4)").text().trim();
const expiryDaysText = row.find("td:eq(6)").text().trim(); const expiryDaysText = row.find("td:eq(6)").text().trim();
const blocked = row.find("td:eq(7) i").hasClass("text-danger"); const blocked = row.find("td:eq(7) i").hasClass("text-danger");
const unlimited_ip = row.find("td:eq(8) i").hasClass("text-primary");
const expiryDaysValue = (expiryDaysText.toLowerCase() === 'unlimited') ? 0 : parseInt(expiryDaysText, 10); const expiryDaysValue = (expiryDaysText.toLowerCase() === 'unlimited') ? 0 : parseInt(expiryDaysText, 10);
@ -455,6 +472,7 @@
$("#editTrafficLimit").val(trafficLimitValue); $("#editTrafficLimit").val(trafficLimitValue);
$("#editExpirationDays").val(expiryDaysValue); $("#editExpirationDays").val(expiryDaysValue);
$("#editBlocked").prop("checked", blocked); $("#editBlocked").prop("checked", blocked);
$("#editUnlimitedIp").prop("checked", unlimited_ip);
const isValid = validateUsername(username, "editUsernameError"); const isValid = validateUsername(username, "editUsernameError");
$("#editUserForm button[type='submit']").prop("disabled", !isValid); $("#editUserForm button[type='submit']").prop("disabled", !isValid);
@ -465,12 +483,19 @@
if (!validateUsername($("#editUsername").val(), "editUsernameError")) { if (!validateUsername($("#editUsername").val(), "editUsernameError")) {
return; return;
} }
$("#editSubmitButton").prop("disabled", true);
const formData = $(this).serializeArray(); const jsonData = {
const jsonData = {}; new_username: $("#editUsername").val(),
formData.forEach(field => { new_traffic_limit: $("#editTrafficLimit").val() || null,
jsonData[field.name] = field.value; 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 editUserUrl = "{{ url_for('edit_user_api', username='USERNAME_PLACEHOLDER') }}";
const url = editUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent($("#originalUsername").val())); const url = editUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent($("#originalUsername").val()));
@ -481,22 +506,8 @@
contentType: "application/json", contentType: "application/json",
data: JSON.stringify(jsonData), data: JSON.stringify(jsonData),
success: function (response) { success: function (response) {
if (typeof response === 'string' && response.includes("User updated successfully")) { if (response && response.detail) {
$("#editUserModal").modal("hide"); $("#editUserModal").modal("hide");
Swal.fire({
title: "Success!",
text: "User updated successfully!",
icon: "success",
confirmButtonText: "OK",
}).then(() => {
location.reload();
});
}
else if (response && response.detail) {
// Hide the modal
$("#editUserModal").modal("hide");
// Show a success message
Swal.fire({ Swal.fire({
title: "Success!", title: "Success!",
text: response.detail, text: response.detail,
@ -509,29 +520,29 @@
$("#editUserModal").modal("hide"); $("#editUserModal").modal("hide");
Swal.fire({ Swal.fire({
title: "Error!", title: "Error!",
text: response.error || "An error occurred.", text: (response && response.error) || "An unknown error occurred.",
icon: "error", icon: "error",
confirmButtonText: "OK", confirmButtonText: "OK",
}); });
$("#editSubmitButton").prop("disabled", false);
} }
}, },
error: function (error) { error: function (jqXHR) {
console.error(error); let errorMessage = "An error occurred while updating user.";
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
errorMessage = jqXHR.responseJSON.detail;
}
Swal.fire({ Swal.fire({
title: "Error!", title: "Error!",
text: "An error occurred while updating user", text: errorMessage,
icon: "error", icon: "error",
confirmButtonText: "OK", confirmButtonText: "OK",
}); });
$("#editSubmitButton").prop("disabled", false);
} }
}); });
}); });
$("#editUserForm button[type='submit']").on("click", function (e) {
e.preventDefault();
$(this).closest("form").submit();
});
// Reset User Button Click // Reset User Button Click
$("#userTable").on("click", ".reset-user", function () { $("#userTable").on("click", ".reset-user", function () {
const username = $(this).data("user"); const username = $(this).data("user");
@ -653,15 +664,12 @@
method: "GET", method: "GET",
dataType: 'json', dataType: 'json',
success: function (response) { success: function (response) {
// console.log("API Response:", response);
const configs = [ const configs = [
{ type: "IPv4", link: response.ipv4 }, { type: "IPv4", link: response.ipv4 },
{ type: "IPv6", link: response.ipv6 }, { type: "IPv6", link: response.ipv6 },
{ type: "Normal-SUB", link: response.normal_sub } { type: "Normal-SUB", link: response.normal_sub }
]; ];
configs.forEach(config => { configs.forEach(config => {
if (config.link) { if (config.link) {
const displayType = config.type; const displayType = config.type;
@ -765,10 +773,10 @@
} }
$('#addUserModal').on('show.bs.modal', function (event) { $('#addUserModal').on('show.bs.modal', function (event) {
$('#addUsername').val(''); $('#addUserForm')[0].reset();
$('#addUsernameError').text('');
$('#addTrafficLimit').val('30'); $('#addTrafficLimit').val('30');
$('#addExpirationDays').val('30'); $('#addExpirationDays').val('30');
$('#addUsernameError').text('');
$('#addSubmitButton').prop('disabled', true); $('#addSubmitButton').prop('disabled', true);
}); });