feat: Add password editing to user modal

This commit is contained in:
ReturnFI
2025-11-05 20:14:44 +00:00
parent 4d33dc9c12
commit b898db944e
6 changed files with 89 additions and 24 deletions

View File

@ -157,19 +157,30 @@ def bulk_user_add(traffic_gb: float, expiration_days: int, count: int, prefix: s
@cli.command('edit-user')
@click.option('--username', '-u', required=True, help='Username for the user to edit', type=str)
@click.option('--new-username', '-nu', required=False, help='New username for the user', type=str)
@click.option('--new-password', '-np', required=False, help='New password for the user. If not set, use --renew-password to generate one.', type=str)
@click.option('--new-traffic-limit', '-nt', required=False, help='Traffic limit for the new user in GB', type=int)
@click.option('--new-expiration-days', '-ne', required=False, help='Expiration days for the new user', type=int)
@click.option('--renew-password', '-rp', is_flag=True, help='Renew password for the user')
@click.option('--renew-password', '-rp', is_flag=True, help='Renew password for the user (generates a random one)')
@click.option('--renew-creation-date', '-rc', is_flag=True, help='Renew creation date for the user')
@click.option('--blocked/--unblocked', 'blocked', '-b', default=None, help='Block or unblock the user.')
@click.option('--unlimited-ip/--limited-ip', 'unlimited_ip', default=None, help='Set user to be exempt from or subject to IP limits.')
@click.option('--note', '-n', required=False, help='New note for the user.', type=str)
def edit_user(username: str, new_username: str, new_traffic_limit: int, new_expiration_days: int, renew_password: bool, renew_creation_date: bool, blocked: bool | None, unlimited_ip: bool | None, note: str | None):
def edit_user(username: str, new_username: str, new_password: str, new_traffic_limit: int, new_expiration_days: int, renew_password: bool, renew_creation_date: bool, blocked: bool | None, unlimited_ip: bool | None, note: str | None):
try:
cli_api.kick_users_by_name(username)
cli_api.traffic_status(display_output=False)
cli_api.edit_user(username, new_username, new_traffic_limit, new_expiration_days,
renew_password, renew_creation_date, blocked, unlimited_ip, note)
cli_api.edit_user(
username=username,
new_username=new_username,
new_password=new_password,
new_traffic_limit=new_traffic_limit,
new_expiration_days=new_expiration_days,
renew_password=renew_password,
renew_creation_date=renew_creation_date,
blocked=blocked,
unlimited_ip=unlimited_ip,
note=note
)
click.echo(f"User '{username}' updated successfully.")
except Exception as e:
click.echo(f'{e}', err=True)

View File

@ -311,7 +311,7 @@ def bulk_user_add(traffic_gb: float, expiration_days: int, count: int, prefix: s
run_cmd(command)
def edit_user(username: str, new_username: str | None, new_traffic_limit: int | None, new_expiration_days: int | None, renew_password: bool, renew_creation_date: bool, blocked: bool | None, unlimited_ip: bool | None, note: str | None):
def edit_user(username: str, new_username: str | None, new_password: str | None, new_traffic_limit: int | None, new_expiration_days: int | None, renew_password: bool, renew_creation_date: bool, blocked: bool | None, unlimited_ip: bool | None, note: str | None):
'''
Edits an existing user's details by calling the new edit_user.py script with named flags.
'''
@ -323,6 +323,15 @@ def edit_user(username: str, new_username: str | None, new_traffic_limit: int |
if new_username:
command_args.extend(['--new-username', new_username])
password_to_set = None
if new_password:
password_to_set = new_password
elif renew_password:
password_to_set = generate_password()
if password_to_set:
command_args.extend(['--password', password_to_set])
if new_traffic_limit is not None:
if new_traffic_limit < 0:
raise InvalidInputError('Error: traffic limit must be a non-negative number.')
@ -333,10 +342,6 @@ def edit_user(username: str, new_username: str | None, new_traffic_limit: int |
raise InvalidInputError('Error: expiration days must be a non-negative number.')
command_args.extend(['--expiration-days', str(new_expiration_days)])
if renew_password:
password = generate_password()
command_args.extend(['--password', password])
if renew_creation_date:
creation_date = datetime.now().strftime('%Y-%m-%d')
command_args.extend(['--creation-date', creation_date])

View File

@ -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();
});

View File

@ -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):

View File

@ -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:

View File

@ -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">