feat: Add password editing to user modal
This commit is contained in:
19
core/cli.py
19
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')
|
@cli.command('edit-user')
|
||||||
@click.option('--username', '-u', required=True, help='Username for the user to edit', type=str)
|
@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-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-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('--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('--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('--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('--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)
|
@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:
|
try:
|
||||||
cli_api.kick_users_by_name(username)
|
cli_api.kick_users_by_name(username)
|
||||||
cli_api.traffic_status(display_output=False)
|
cli_api.traffic_status(display_output=False)
|
||||||
cli_api.edit_user(username, new_username, new_traffic_limit, new_expiration_days,
|
cli_api.edit_user(
|
||||||
renew_password, renew_creation_date, blocked, unlimited_ip, note)
|
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.")
|
click.echo(f"User '{username}' updated successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f'{e}', err=True)
|
click.echo(f'{e}', err=True)
|
||||||
|
|||||||
@ -311,7 +311,7 @@ def bulk_user_add(traffic_gb: float, expiration_days: int, count: int, prefix: s
|
|||||||
|
|
||||||
run_cmd(command)
|
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.
|
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:
|
if new_username:
|
||||||
command_args.extend(['--new-username', 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 is not None:
|
||||||
if new_traffic_limit < 0:
|
if new_traffic_limit < 0:
|
||||||
raise InvalidInputError('Error: traffic limit must be a non-negative number.')
|
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.')
|
raise InvalidInputError('Error: expiration days must be a non-negative number.')
|
||||||
command_args.extend(['--expiration-days', str(new_expiration_days)])
|
command_args.extend(['--expiration-days', str(new_expiration_days)])
|
||||||
|
|
||||||
if renew_password:
|
|
||||||
password = generate_password()
|
|
||||||
command_args.extend(['--password', password])
|
|
||||||
|
|
||||||
if renew_creation_date:
|
if renew_creation_date:
|
||||||
creation_date = datetime.now().strftime('%Y-%m-%d')
|
creation_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
command_args.extend(['--creation-date', creation_date])
|
command_args.extend(['--creation-date', creation_date])
|
||||||
|
|||||||
@ -10,6 +10,7 @@ $(function () {
|
|||||||
const USER_URI_URL_TEMPLATE = contentSection.dataset.userUriUrlTemplate;
|
const USER_URI_URL_TEMPLATE = contentSection.dataset.userUriUrlTemplate;
|
||||||
const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
|
const BULK_URI_URL = contentSection.dataset.bulkUriUrl;
|
||||||
const USERS_BASE_URL = contentSection.dataset.usersBaseUrl;
|
const USERS_BASE_URL = contentSection.dataset.usersBaseUrl;
|
||||||
|
const GET_USER_URL_TEMPLATE = contentSection.dataset.getUserUrlTemplate;
|
||||||
|
|
||||||
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
||||||
let cachedUserData = [];
|
let cachedUserData = [];
|
||||||
@ -35,6 +36,15 @@ $(function () {
|
|||||||
return null;
|
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() {
|
function checkIpLimitServiceStatus() {
|
||||||
$.getJSON(SERVICE_STATUS_URL)
|
$.getJSON(SERVICE_STATUS_URL)
|
||||||
.done(data => {
|
.done(data => {
|
||||||
@ -141,12 +151,12 @@ $(function () {
|
|||||||
|
|
||||||
$("#editUserModal").on("show.bs.modal", function (event) {
|
$("#editUserModal").on("show.bs.modal", function (event) {
|
||||||
const user = $(event.relatedTarget).data("user");
|
const user = $(event.relatedTarget).data("user");
|
||||||
const clickedRow = $(event.relatedTarget).closest("tr");
|
const dataRow = $(event.relatedTarget).closest("tr.user-main-row");
|
||||||
const dataRow = clickedRow.hasClass('user-main-row') ? clickedRow : clickedRow.prev('.user-main-row');
|
const url = GET_USER_URL_TEMPLATE.replace('U', user);
|
||||||
|
|
||||||
const trafficText = dataRow.find("td:eq(4)").text();
|
const trafficText = dataRow.find("td:eq(4)").text();
|
||||||
const expiryText = dataRow.find("td:eq(6)").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);
|
$("#originalUsername").val(user);
|
||||||
$("#editUsername").val(user);
|
$("#editUsername").val(user);
|
||||||
@ -155,6 +165,24 @@ $(function () {
|
|||||||
$("#editNote").val(note || '');
|
$("#editNote").val(note || '');
|
||||||
$("#editBlocked").prop("checked", !dataRow.find("td:eq(8) i").hasClass("text-success"));
|
$("#editBlocked").prop("checked", !dataRow.find("td:eq(8) i").hasClass("text-success"));
|
||||||
$("#editUnlimitedIp").prop("checked", dataRow.find(".unlimited-ip-cell i").hasClass("text-primary"));
|
$("#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) {
|
$("#editUserForm").on("submit", function (e) {
|
||||||
@ -361,4 +389,5 @@ $(function () {
|
|||||||
|
|
||||||
initializeLimitSelector();
|
initializeLimitSelector();
|
||||||
checkIpLimitServiceStatus();
|
checkIpLimitServiceStatus();
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
});
|
});
|
||||||
@ -56,14 +56,15 @@ class AddBulkUsersInputBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class EditUserInputBody(BaseModel):
|
class EditUserInputBody(BaseModel):
|
||||||
new_username: Optional[str] = None
|
new_username: Optional[str] = Field(None, description="The new username for the user.")
|
||||||
new_traffic_limit: Optional[int] = None
|
new_password: Optional[str] = Field(None, description="The new password for the user. Leave empty to keep the current one.")
|
||||||
new_expiration_days: Optional[int] = None
|
new_traffic_limit: Optional[int] = Field(None, description="The new traffic limit in GB.")
|
||||||
renew_password: bool = False
|
new_expiration_days: Optional[int] = Field(None, description="The new expiration in days.")
|
||||||
renew_creation_date: bool = False
|
renew_password: bool = Field(False, description="Whether to renew the user's password. Used by legacy clients like the bot.")
|
||||||
blocked: Optional[bool] = None
|
renew_creation_date: bool = Field(False, description="Whether to renew the user's account creation date.")
|
||||||
unlimited_ip: Optional[bool] = None
|
blocked: Optional[bool] = Field(None, description="Whether the user is blocked.")
|
||||||
note: Optional[str] = None
|
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')
|
@field_validator('new_username')
|
||||||
def validate_new_username(cls, v):
|
def validate_new_username(cls, v):
|
||||||
|
|||||||
@ -171,7 +171,7 @@ async def edit_user_api(username: str, body: EditUserInputBody):
|
|||||||
try:
|
try:
|
||||||
cli_api.kick_users_by_name([username])
|
cli_api.kick_users_by_name([username])
|
||||||
cli_api.traffic_status(display_output=False)
|
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)
|
body.renew_password, body.renew_creation_date, body.blocked, body.unlimited_ip, body.note)
|
||||||
return DetailResponse(detail=f'User {username} has been edited.')
|
return DetailResponse(detail=f'User {username} has been edited.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -48,7 +48,8 @@
|
|||||||
data-reset-user-url-template="{{ url_for('reset_user_api', username='U') }}"
|
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-user-uri-url-template="{{ url_for('show_user_uri_api', username='U') }}"
|
||||||
data-bulk-uri-url="{{ url_for('show_multiple_user_uris_api') }}"
|
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="container-fluid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -177,7 +178,7 @@
|
|||||||
<i class="fas fa-times-circle text-danger"></i>
|
<i class="fas fa-times-circle text-danger"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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 %}
|
{% if user.note %}
|
||||||
<span title="{{ user.note }}">{{ user.note | truncate(20, True) }}</span>
|
<span title="{{ user.note }}">{{ user.note | truncate(20, True) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -511,6 +512,24 @@
|
|||||||
<input type="text" class="form-control" id="editUsername" readonly>
|
<input type="text" class="form-control" id="editUsername" readonly>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="editTrafficLimit">Traffic Limit (GB)</label>
|
<label for="editTrafficLimit">Traffic Limit (GB)</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user