feat: Add password editing to user modal
This commit is contained in:
@ -10,6 +10,7 @@ $(function () {
|
||||
const USER_URI_URL_TEMPLATE = contentSection.dataset.userUriUrlTemplate;
|
||||
const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
|
||||
const USERS_BASE_URL = contentSection.dataset.usersBaseUrl;
|
||||
const GET_USER_URL_TEMPLATE = contentSection.dataset.getUserUrlTemplate;
|
||||
|
||||
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
||||
let cachedUserData = [];
|
||||
@ -35,6 +36,15 @@ $(function () {
|
||||
return null;
|
||||
}
|
||||
|
||||
function generatePassword(length = 32) {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function checkIpLimitServiceStatus() {
|
||||
$.getJSON(SERVICE_STATUS_URL)
|
||||
.done(data => {
|
||||
@ -141,13 +151,13 @@ $(function () {
|
||||
|
||||
$("#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 dataRow = $(event.relatedTarget).closest("tr.user-main-row");
|
||||
const url = GET_USER_URL_TEMPLATE.replace('U', user);
|
||||
|
||||
const trafficText = dataRow.find("td:eq(4)").text();
|
||||
const expiryText = dataRow.find("td:eq(6)").text();
|
||||
const note = dataRow.find(".note-cell").data('note');
|
||||
|
||||
const note = dataRow.data('note');
|
||||
|
||||
$("#originalUsername").val(user);
|
||||
$("#editUsername").val(user);
|
||||
$("#editTrafficLimit").val(parseFloat(trafficText.split('/')[1]) || 0);
|
||||
@ -155,6 +165,24 @@ $(function () {
|
||||
$("#editNote").val(note || '');
|
||||
$("#editBlocked").prop("checked", !dataRow.find("td:eq(8) i").hasClass("text-success"));
|
||||
$("#editUnlimitedIp").prop("checked", dataRow.find(".unlimited-ip-cell i").hasClass("text-primary"));
|
||||
|
||||
const passwordInput = $("#editPassword");
|
||||
passwordInput.val("Loading...").prop("disabled", true);
|
||||
|
||||
$.getJSON(url)
|
||||
.done(userData => {
|
||||
passwordInput.val(userData.password || '');
|
||||
})
|
||||
.fail(() => {
|
||||
passwordInput.val("").attr("placeholder", "Failed to load password");
|
||||
})
|
||||
.always(() => {
|
||||
passwordInput.prop("disabled", false);
|
||||
});
|
||||
});
|
||||
|
||||
$('#editUserModal').on('click', '#generatePasswordBtn', function() {
|
||||
$('#editPassword').val(generatePassword());
|
||||
});
|
||||
|
||||
$("#editUserForm").on("submit", function (e) {
|
||||
@ -361,4 +389,5 @@ $(function () {
|
||||
|
||||
initializeLimitSelector();
|
||||
checkIpLimitServiceStatus();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
@ -56,14 +56,15 @@ class AddBulkUsersInputBody(BaseModel):
|
||||
|
||||
|
||||
class EditUserInputBody(BaseModel):
|
||||
new_username: Optional[str] = None
|
||||
new_traffic_limit: Optional[int] = None
|
||||
new_expiration_days: Optional[int] = None
|
||||
renew_password: bool = False
|
||||
renew_creation_date: bool = False
|
||||
blocked: Optional[bool] = None
|
||||
unlimited_ip: Optional[bool] = None
|
||||
note: Optional[str] = None
|
||||
new_username: Optional[str] = Field(None, description="The new username for the user.")
|
||||
new_password: Optional[str] = Field(None, description="The new password for the user. Leave empty to keep the current one.")
|
||||
new_traffic_limit: Optional[int] = Field(None, description="The new traffic limit in GB.")
|
||||
new_expiration_days: Optional[int] = Field(None, description="The new expiration in days.")
|
||||
renew_password: bool = Field(False, description="Whether to renew the user's password. Used by legacy clients like the bot.")
|
||||
renew_creation_date: bool = Field(False, description="Whether to renew the user's account creation date.")
|
||||
blocked: Optional[bool] = Field(None, description="Whether the user is blocked.")
|
||||
unlimited_ip: Optional[bool] = Field(None, description="Whether the user has unlimited IP access.")
|
||||
note: Optional[str] = Field(None, description="A note for the user.")
|
||||
|
||||
@field_validator('new_username')
|
||||
def validate_new_username(cls, v):
|
||||
|
||||
@ -171,7 +171,7 @@ async def edit_user_api(username: str, body: EditUserInputBody):
|
||||
try:
|
||||
cli_api.kick_users_by_name([username])
|
||||
cli_api.traffic_status(display_output=False)
|
||||
cli_api.edit_user(username, body.new_username, body.new_traffic_limit, body.new_expiration_days,
|
||||
cli_api.edit_user(username, body.new_username, body.new_password, body.new_traffic_limit, body.new_expiration_days,
|
||||
body.renew_password, body.renew_creation_date, body.blocked, body.unlimited_ip, body.note)
|
||||
return DetailResponse(detail=f'User {username} has been edited.')
|
||||
except Exception as e:
|
||||
|
||||
@ -48,7 +48,8 @@
|
||||
data-reset-user-url-template="{{ url_for('reset_user_api', username='U') }}"
|
||||
data-user-uri-url-template="{{ url_for('show_user_uri_api', username='U') }}"
|
||||
data-bulk-uri-url="{{ url_for('show_multiple_user_uris_api') }}"
|
||||
data-users-base-url="{{ url_for('users') }}">
|
||||
data-users-base-url="{{ url_for('users') }}"
|
||||
data-get-user-url-template="{{ url_for('get_user_api', username='U') }}">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@ -177,7 +178,7 @@
|
||||
<i class="fas fa-times-circle text-danger"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell note-cell" data-note="{{ user.note or '' }}">
|
||||
<td class="d-none d-md-table-cell note-cell">
|
||||
{% if user.note %}
|
||||
<span title="{{ user.note }}">{{ user.note | truncate(20, True) }}</span>
|
||||
{% endif %}
|
||||
@ -511,6 +512,24 @@
|
||||
<input type="text" class="form-control" id="editUsername" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editPassword">Password
|
||||
<i class="fas fa-info-circle text-muted ml-1"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title="Changing the password disconnects the user and invalidates all existing configuration and subscription links. New links must be shared with the user to restore access.">
|
||||
</i>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="editPassword" name="new_password" placeholder="Leave empty to keep current password">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" id="generatePasswordBtn"><i class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editTrafficLimit">Traffic Limit (GB)</label>
|
||||
<div class="input-group">
|
||||
|
||||
Reference in New Issue
Block a user