From b898db944e2972430a4b86299a13247d57fb8029 Mon Sep 17 00:00:00 2001 From: ReturnFI <151555003+ReturnFI@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:14:44 +0000 Subject: [PATCH] feat: Add password editing to user modal --- core/cli.py | 19 ++++++++-- core/cli_api.py | 15 +++++--- core/scripts/webpanel/assets/js/users.js | 37 +++++++++++++++++-- .../webpanel/routers/api/v1/schema/user.py | 17 +++++---- core/scripts/webpanel/routers/api/v1/user.py | 2 +- core/scripts/webpanel/templates/users.html | 23 +++++++++++- 6 files changed, 89 insertions(+), 24 deletions(-) diff --git a/core/cli.py b/core/cli.py index bab7f88..244f97d 100644 --- a/core/cli.py +++ b/core/cli.py @@ -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) diff --git a/core/cli_api.py b/core/cli_api.py index 729898e..2a92182 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -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]) diff --git a/core/scripts/webpanel/assets/js/users.js b/core/scripts/webpanel/assets/js/users.js index fdba27f..a0205c3 100644 --- a/core/scripts/webpanel/assets/js/users.js +++ b/core/scripts/webpanel/assets/js/users.js @@ -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(); }); \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/user.py b/core/scripts/webpanel/routers/api/v1/schema/user.py index 311033c..4ad486d 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/user.py +++ b/core/scripts/webpanel/routers/api/v1/schema/user.py @@ -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): diff --git a/core/scripts/webpanel/routers/api/v1/user.py b/core/scripts/webpanel/routers/api/v1/user.py index 79b8894..3216d85 100644 --- a/core/scripts/webpanel/routers/api/v1/user.py +++ b/core/scripts/webpanel/routers/api/v1/user.py @@ -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: diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index 099f8c5..632f8e2 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -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') }}">