Merge pull request #247 from ReturnFI/beta
Per-user unlimited IP support & web panel integration
This commit is contained in:
38
changelog
38
changelog
@ -1,10 +1,32 @@
|
|||||||
# [1.13.0] - 2025-08-10
|
# [1.14.0] - 2025-08-13
|
||||||
|
|
||||||
#### ✨ UI Enhancements
|
#### ✨ New Features
|
||||||
|
|
||||||
* ✨ feat(core): Implement external node management system
|
* 🌐 **Per-User Unlimited IP Option**:
|
||||||
* 🌐 feat(api): Add external node management endpoints
|
|
||||||
* 🔗 feat(normalsub): Add external node URIs to subscriptions
|
* Added `unlimited_user` flag to exempt specific users from concurrent IP limits
|
||||||
* 💾 feat: Backup nodes.json
|
* Works in both CLI and web panel
|
||||||
* 🛠️ fix: Robust parsing, unlimited user handling, and various script/UI issues
|
* Integrated into `limit.sh` for enforcement bypass
|
||||||
* 📦 chore: Dependency updates (pytelegrambotapi, aiohttp)
|
* 🖥️ **Web Panel Enhancements**:
|
||||||
|
|
||||||
|
* Unlimited IP control added to **Users** page
|
||||||
|
* IP limit UI now conditionally shown based on service status
|
||||||
|
* 🛠️ **CLI & Scripts**:
|
||||||
|
|
||||||
|
* Add unlimited IP option to user creation and editing
|
||||||
|
* Menu script now supports unlimited IP configuration
|
||||||
|
|
||||||
|
#### 🔄 Improvements
|
||||||
|
|
||||||
|
* 📜 **API**:
|
||||||
|
|
||||||
|
* Updated user schemas to match CLI output and new unlimited IP feature
|
||||||
|
* 🐛 **Fixes**:
|
||||||
|
|
||||||
|
* Corrected type hints for optional parameters in `config_ip_limiter`
|
||||||
|
|
||||||
|
#### 📦 Dependency Updates
|
||||||
|
|
||||||
|
* ⬆️ `anyio` → 4.10.0
|
||||||
|
* ⬆️ `certbot` → 4.2.0
|
||||||
|
* ⬆️ `charset-normalizer` → 3.4.3
|
||||||
|
|||||||
12
core/cli.py
12
core/cli.py
@ -130,9 +130,10 @@ def get_user(username: str):
|
|||||||
@click.option('--expiration-days', '-e', required=True, help='Expiration days for the new user', type=int)
|
@click.option('--expiration-days', '-e', required=True, help='Expiration days for the new user', type=int)
|
||||||
@click.option('--password', '-p', required=False, help='Password for the user', type=str)
|
@click.option('--password', '-p', required=False, help='Password for the user', type=str)
|
||||||
@click.option('--creation-date', '-c', required=False, help='Creation date for the user (YYYY-MM-DD)', type=str)
|
@click.option('--creation-date', '-c', required=False, help='Creation date for the user (YYYY-MM-DD)', type=str)
|
||||||
def add_user(username: str, traffic_limit: int, expiration_days: int, password: str, creation_date: str):
|
@click.option('--unlimited', is_flag=True, default=False, help='Exempt user from IP limit checks.')
|
||||||
|
def add_user(username: str, traffic_limit: int, expiration_days: int, password: str, creation_date: str, unlimited: bool):
|
||||||
try:
|
try:
|
||||||
cli_api.add_user(username, traffic_limit, expiration_days, password, creation_date)
|
cli_api.add_user(username, traffic_limit, expiration_days, password, creation_date, unlimited)
|
||||||
click.echo(f"User '{username}' added successfully.")
|
click.echo(f"User '{username}' added successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f'{e}', err=True)
|
click.echo(f'{e}', err=True)
|
||||||
@ -145,13 +146,14 @@ def add_user(username: str, traffic_limit: int, expiration_days: int, password:
|
|||||||
@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')
|
||||||
@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', '-b', is_flag=True, help='Block the user')
|
@click.option('--blocked/--unblocked', 'blocked', default=None, help='Block or unblock the user.')
|
||||||
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):
|
@click.option('--unlimited-ip/--limited-ip', 'unlimited_ip', default=None, help='Set user to be exempt from or subject to IP limits.')
|
||||||
|
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):
|
||||||
try:
|
try:
|
||||||
cli_api.kick_user_by_name(username)
|
cli_api.kick_user_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(username, new_username, new_traffic_limit, new_expiration_days,
|
||||||
renew_password, renew_creation_date, blocked)
|
renew_password, renew_creation_date, blocked, unlimited_ip)
|
||||||
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)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import subprocess
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from dotenv import dotenv_values
|
from dotenv import dotenv_values
|
||||||
|
|
||||||
import traffic
|
import traffic
|
||||||
@ -266,18 +266,26 @@ def get_user(username: str) -> dict[str, Any] | None:
|
|||||||
return json.loads(res)
|
return json.loads(res)
|
||||||
|
|
||||||
|
|
||||||
def add_user(username: str, traffic_limit: int, expiration_days: int, password: str | None, creation_date: str | None):
|
def add_user(username: str, traffic_limit: int, expiration_days: int, password: str | None, creation_date: str | None, unlimited: bool):
|
||||||
'''
|
'''
|
||||||
Adds a new user with the given parameters.
|
Adds a new user with the given parameters, respecting positional argument requirements.
|
||||||
'''
|
'''
|
||||||
if not password:
|
command = ['python3', Command.ADD_USER.value, username, str(traffic_limit), str(expiration_days)]
|
||||||
password = generate_password()
|
|
||||||
if not creation_date:
|
if unlimited:
|
||||||
creation_date = datetime.now().strftime('%Y-%m-%d')
|
final_password = password if password else generate_password()
|
||||||
run_cmd(['python3', Command.ADD_USER.value, username, str(traffic_limit), str(expiration_days), password, creation_date])
|
final_creation_date = creation_date if creation_date else datetime.now().strftime('%Y-%m-%d')
|
||||||
|
command.extend([final_password, final_creation_date, 'true'])
|
||||||
|
elif creation_date:
|
||||||
|
final_password = password if password else generate_password()
|
||||||
|
command.extend([final_password, creation_date])
|
||||||
|
elif password:
|
||||||
|
command.append(password)
|
||||||
|
|
||||||
|
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):
|
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):
|
||||||
'''
|
'''
|
||||||
Edits an existing user's details.
|
Edits an existing user's details.
|
||||||
'''
|
'''
|
||||||
@ -289,15 +297,20 @@ def edit_user(username: str, new_username: str | None, new_traffic_limit: int |
|
|||||||
if new_expiration_days is not None and new_expiration_days < 0:
|
if new_expiration_days is not None and new_expiration_days < 0:
|
||||||
raise InvalidInputError('Error: expiration days must be a non-negative number.')
|
raise InvalidInputError('Error: expiration days must be a non-negative number.')
|
||||||
|
|
||||||
if renew_password:
|
password = generate_password() if renew_password else ''
|
||||||
password = generate_password()
|
creation_date = datetime.now().strftime('%Y-%m-%d') if renew_creation_date else ''
|
||||||
else:
|
|
||||||
password = ''
|
|
||||||
|
|
||||||
if renew_creation_date:
|
blocked_str = ''
|
||||||
creation_date = datetime.now().strftime('%Y-%m-%d')
|
if blocked is True:
|
||||||
else:
|
blocked_str = 'true'
|
||||||
creation_date = ''
|
elif blocked is False:
|
||||||
|
blocked_str = 'false'
|
||||||
|
|
||||||
|
unlimited_str = ''
|
||||||
|
if unlimited_ip is True:
|
||||||
|
unlimited_str = 'true'
|
||||||
|
elif unlimited_ip is False:
|
||||||
|
unlimited_str = 'false'
|
||||||
|
|
||||||
command_args = [
|
command_args = [
|
||||||
'bash',
|
'bash',
|
||||||
@ -308,7 +321,8 @@ def edit_user(username: str, new_username: str | None, new_traffic_limit: int |
|
|||||||
str(new_expiration_days) if new_expiration_days is not None else '',
|
str(new_expiration_days) if new_expiration_days is not None else '',
|
||||||
password,
|
password,
|
||||||
creation_date,
|
creation_date,
|
||||||
'true' if blocked else 'false'
|
blocked_str,
|
||||||
|
unlimited_str
|
||||||
]
|
]
|
||||||
run_cmd(command_args)
|
run_cmd(command_args)
|
||||||
|
|
||||||
@ -655,7 +669,7 @@ def stop_ip_limiter():
|
|||||||
'''Stops the IP limiter service.'''
|
'''Stops the IP limiter service.'''
|
||||||
run_cmd(['bash', Command.LIMIT_SCRIPT.value, 'stop'])
|
run_cmd(['bash', Command.LIMIT_SCRIPT.value, 'stop'])
|
||||||
|
|
||||||
def config_ip_limiter(block_duration: int = None, max_ips: int = None):
|
def config_ip_limiter(block_duration: Optional[int] = None, max_ips: Optional[int] = None):
|
||||||
'''Configures the IP limiter service.'''
|
'''Configures the IP limiter service.'''
|
||||||
if block_duration is not None and block_duration <= 0:
|
if block_duration is not None and block_duration <= 0:
|
||||||
raise InvalidInputError("Block duration must be greater than 0.")
|
raise InvalidInputError("Block duration must be greater than 0.")
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from datetime import datetime
|
|||||||
from init_paths import *
|
from init_paths import *
|
||||||
from paths import *
|
from paths import *
|
||||||
|
|
||||||
def add_user(username, traffic_gb, expiration_days, password=None, creation_date=None):
|
def add_user(username, traffic_gb, expiration_days, password=None, creation_date=None, unlimited_user=False):
|
||||||
"""
|
"""
|
||||||
Adds a new user to the USERS_FILE.
|
Adds a new user to the USERS_FILE.
|
||||||
|
|
||||||
@ -19,12 +19,13 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date
|
|||||||
expiration_days (str): The number of days until the account expires.
|
expiration_days (str): The number of days until the account expires.
|
||||||
password (str, optional): The user's password. If None, a random one is generated.
|
password (str, optional): The user's password. If None, a random one is generated.
|
||||||
creation_date (str, optional): The account creation date in YYYY-MM-DD format. If None, the current date is used.
|
creation_date (str, optional): The account creation date in YYYY-MM-DD format. If None, the current date is used.
|
||||||
|
unlimited_user (bool, optional): If True, user is exempt from IP limits. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: 0 on success, 1 on failure.
|
int: 0 on success, 1 on failure.
|
||||||
"""
|
"""
|
||||||
if not username or not traffic_gb or not expiration_days:
|
if not username or not traffic_gb or not expiration_days:
|
||||||
print(f"Usage: {sys.argv[0]} <username> <traffic_limit_GB> <expiration_days> [password] [creation_date]")
|
print(f"Usage: {sys.argv[0]} <username> <traffic_limit_GB> <expiration_days> [password] [creation_date] [unlimited_user (true/false)]")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -45,6 +46,7 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date
|
|||||||
password = subprocess.check_output(['cat', '/proc/sys/kernel/random/uuid'], text=True).strip()
|
password = subprocess.check_output(['cat', '/proc/sys/kernel/random/uuid'], text=True).strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Error: Failed to generate password. Please install 'pwgen' or ensure /proc access.")
|
print("Error: Failed to generate password. Please install 'pwgen' or ensure /proc access.")
|
||||||
|
return 1
|
||||||
|
|
||||||
if not creation_date:
|
if not creation_date:
|
||||||
creation_date = datetime.now().strftime("%Y-%m-%d")
|
creation_date = datetime.now().strftime("%Y-%m-%d")
|
||||||
@ -88,7 +90,8 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date
|
|||||||
"max_download_bytes": traffic_bytes,
|
"max_download_bytes": traffic_bytes,
|
||||||
"expiration_days": expiration_days,
|
"expiration_days": expiration_days,
|
||||||
"account_creation_date": creation_date,
|
"account_creation_date": creation_date,
|
||||||
"blocked": False
|
"blocked": False,
|
||||||
|
"unlimited_user": unlimited_user
|
||||||
}
|
}
|
||||||
|
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
@ -103,8 +106,8 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) not in [4, 6]:
|
if len(sys.argv) < 4 or len(sys.argv) > 7:
|
||||||
print(f"Usage: {sys.argv[0]} <username> <traffic_limit_GB> <expiration_days> [password] [creation_date]")
|
print(f"Usage: {sys.argv[0]} <username> <traffic_limit_GB> <expiration_days> [password] [creation_date] [unlimited_user (true/false)]")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
username = sys.argv[1]
|
username = sys.argv[1]
|
||||||
@ -112,6 +115,8 @@ if __name__ == "__main__":
|
|||||||
expiration_days = sys.argv[3]
|
expiration_days = sys.argv[3]
|
||||||
password = sys.argv[4] if len(sys.argv) > 4 else None
|
password = sys.argv[4] if len(sys.argv) > 4 else None
|
||||||
creation_date = sys.argv[5] if len(sys.argv) > 5 else None
|
creation_date = sys.argv[5] if len(sys.argv) > 5 else None
|
||||||
|
unlimited_user_str = sys.argv[6] if len(sys.argv) > 6 else "false"
|
||||||
|
unlimited_user = unlimited_user_str.lower() == 'true'
|
||||||
|
|
||||||
exit_code = add_user(username, traffic_gb, expiration_days, password, creation_date)
|
exit_code = add_user(username, traffic_gb, expiration_days, password, creation_date, unlimited_user)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
@ -8,7 +8,7 @@ readonly GB_TO_BYTES=$((1024 * 1024 * 1024))
|
|||||||
validate_username() {
|
validate_username() {
|
||||||
local username=$1
|
local username=$1
|
||||||
if [ -z "$username" ]; then
|
if [ -z "$username" ]; then
|
||||||
return 0 # Optional value is valid
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! [[ "$username" =~ ^[a-zA-Z0-9]+$ ]]; then
|
if ! [[ "$username" =~ ^[a-zA-Z0-9]+$ ]]; then
|
||||||
echo "Username can only contain letters and numbers."
|
echo "Username can only contain letters and numbers."
|
||||||
@ -21,7 +21,7 @@ validate_username() {
|
|||||||
validate_traffic_limit() {
|
validate_traffic_limit() {
|
||||||
local traffic_limit=$1
|
local traffic_limit=$1
|
||||||
if [ -z "$traffic_limit" ]; then
|
if [ -z "$traffic_limit" ]; then
|
||||||
return 0 # Optional value is valid
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! [[ "$traffic_limit" =~ ^[0-9]+$ ]]; then
|
if ! [[ "$traffic_limit" =~ ^[0-9]+$ ]]; then
|
||||||
echo "Error: Traffic limit must be a valid non-negative number (use 0 for unlimited)."
|
echo "Error: Traffic limit must be a valid non-negative number (use 0 for unlimited)."
|
||||||
@ -33,7 +33,7 @@ validate_traffic_limit() {
|
|||||||
validate_expiration_days() {
|
validate_expiration_days() {
|
||||||
local expiration_days=$1
|
local expiration_days=$1
|
||||||
if [ -z "$expiration_days" ]; then
|
if [ -z "$expiration_days" ]; then
|
||||||
return 0 # Optional value is valid
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! [[ "$expiration_days" =~ ^[0-9]+$ ]]; then
|
if ! [[ "$expiration_days" =~ ^[0-9]+$ ]]; then
|
||||||
echo "Error: Expiration days must be a valid non-negative number (use 0 for unlimited)."
|
echo "Error: Expiration days must be a valid non-negative number (use 0 for unlimited)."
|
||||||
@ -45,7 +45,7 @@ validate_expiration_days() {
|
|||||||
validate_date() {
|
validate_date() {
|
||||||
local date_str=$1
|
local date_str=$1
|
||||||
if [ -z "$date_str" ]; then
|
if [ -z "$date_str" ]; then
|
||||||
return 0 # Optional value is valid
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! [[ "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
if ! [[ "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||||
echo "Invalid date format. Expected YYYY-MM-DD."
|
echo "Invalid date format. Expected YYYY-MM-DD."
|
||||||
@ -59,8 +59,8 @@ validate_date() {
|
|||||||
|
|
||||||
validate_blocked_status() {
|
validate_blocked_status() {
|
||||||
local status=$1
|
local status=$1
|
||||||
if [ -z "$status" ]; then
|
if [ -z "$status" ]; then
|
||||||
return 0 # Optional value is valid
|
return 0
|
||||||
fi
|
fi
|
||||||
if [ "$status" != "true" ] && [ "$status" != "false" ]; then
|
if [ "$status" != "true" ] && [ "$status" != "false" ]; then
|
||||||
echo "Blocked status must be 'true' or 'false'."
|
echo "Blocked status must be 'true' or 'false'."
|
||||||
@ -69,14 +69,26 @@ validate_blocked_status() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_unlimited_status() {
|
||||||
|
local status=$1
|
||||||
|
if [ -z "$status" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$status" != "true" ] && [ "$status" != "false" ]; then
|
||||||
|
echo "Unlimited status must be 'true' or 'false'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
convert_blocked_status() {
|
|
||||||
|
convert_boolean_status() {
|
||||||
local status=$1
|
local status=$1
|
||||||
case "$status" in
|
case "$status" in
|
||||||
y|Y) echo "true" ;;
|
y|Y) echo "true" ;;
|
||||||
n|N) echo "false" ;;
|
n|N) echo "false" ;;
|
||||||
true|false) echo "$status" ;;
|
true|false) echo "$status" ;;
|
||||||
*) echo "false" ;; # Default to false for safety
|
*) echo "false" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +105,7 @@ update_user_info() {
|
|||||||
local new_expiration_days=$5
|
local new_expiration_days=$5
|
||||||
local new_account_creation_date=$6
|
local new_account_creation_date=$6
|
||||||
local new_blocked=$7
|
local new_blocked=$7
|
||||||
|
local new_unlimited=$8
|
||||||
|
|
||||||
if [ ! -f "$USERS_FILE" ]; then
|
if [ ! -f "$USERS_FILE" ]; then
|
||||||
echo -e "${red}Error:${NC} File '$USERS_FILE' not found."
|
echo -e "${red}Error:${NC} File '$USERS_FILE' not found."
|
||||||
@ -120,30 +133,32 @@ update_user_info() {
|
|||||||
echo "Expiration Days: ${new_expiration_days:-(not changed)}"
|
echo "Expiration Days: ${new_expiration_days:-(not changed)}"
|
||||||
echo "Creation Date: ${new_account_creation_date:-(not changed)}"
|
echo "Creation Date: ${new_account_creation_date:-(not changed)}"
|
||||||
echo "Blocked: $new_blocked"
|
echo "Blocked: $new_blocked"
|
||||||
|
echo "Unlimited IP: $new_unlimited"
|
||||||
|
|
||||||
|
|
||||||
# Update user fields, only if new values are provided
|
|
||||||
jq \
|
jq \
|
||||||
--arg old_username "$old_username" \
|
--arg old_username "$old_username" \
|
||||||
--arg new_username "$new_username" \
|
--arg new_username "$new_username" \
|
||||||
--arg password "${new_password:-null}" \
|
--arg password "${new_password:-null}" \
|
||||||
--argjson max_download_bytes "${new_max_download_bytes:-null}" \
|
--argjson max_download_bytes "${new_max_download_bytes:-null}" \
|
||||||
--argjson expiration_days "${new_expiration_days:-null}" \
|
--argjson expiration_days "${new_expiration_days:-null}" \
|
||||||
--arg account_creation_date "${new_account_creation_date:-null}" \
|
--arg account_creation_date "${new_account_creation_date:-null}" \
|
||||||
--argjson blocked "$(convert_blocked_status "${new_blocked:-false}")" \
|
--argjson blocked "$new_blocked" \
|
||||||
--argjson upload_bytes "$upload_bytes" \
|
--argjson unlimited "$new_unlimited" \
|
||||||
|
--argjson upload_bytes "$upload_bytes" \
|
||||||
--argjson download_bytes "$download_bytes" \
|
--argjson download_bytes "$download_bytes" \
|
||||||
--arg status "$status" \
|
--arg status "$status" \
|
||||||
'
|
'
|
||||||
.[$new_username] = .[$old_username] |
|
.[$new_username] = .[$old_username] |
|
||||||
del(.[$old_username]) |
|
(if $old_username != $new_username then del(.[$old_username]) else . end) |
|
||||||
.[$new_username] |= (
|
.[$new_username] |= (
|
||||||
.password = ($password // .password) |
|
.password = ($password // .password) |
|
||||||
.max_download_bytes = ($max_download_bytes // .max_download_bytes) |
|
.max_download_bytes = ($max_download_bytes // .max_download_bytes) |
|
||||||
.expiration_days = ($expiration_days // .expiration_days) |
|
.expiration_days = ($expiration_days // .expiration_days) |
|
||||||
.account_creation_date = ($account_creation_date // .account_creation_date) |
|
.account_creation_date = ($account_creation_date // .account_creation_date) |
|
||||||
.blocked = $blocked |
|
.blocked = $blocked |
|
||||||
.upload_bytes = $upload_bytes |
|
.unlimited_user = $unlimited |
|
||||||
|
.upload_bytes = $upload_bytes |
|
||||||
.download_bytes = $download_bytes |
|
.download_bytes = $download_bytes |
|
||||||
.status = $status
|
.status = $status
|
||||||
)
|
)
|
||||||
@ -166,6 +181,7 @@ edit_user() {
|
|||||||
local new_password=$5
|
local new_password=$5
|
||||||
local new_creation_date=$6
|
local new_creation_date=$6
|
||||||
local new_blocked=$7
|
local new_blocked=$7
|
||||||
|
local new_unlimited=$8
|
||||||
|
|
||||||
|
|
||||||
local user_info=$(get_user_info "$username")
|
local user_info=$(get_user_info "$username")
|
||||||
@ -180,6 +196,7 @@ edit_user() {
|
|||||||
local expiration_days=$(echo "$user_info" | jq -r '.expiration_days')
|
local expiration_days=$(echo "$user_info" | jq -r '.expiration_days')
|
||||||
local creation_date=$(echo "$user_info" | jq -r '.account_creation_date')
|
local creation_date=$(echo "$user_info" | jq -r '.account_creation_date')
|
||||||
local blocked=$(echo "$user_info" | jq -r '.blocked')
|
local blocked=$(echo "$user_info" | jq -r '.blocked')
|
||||||
|
local unlimited_user=$(echo "$user_info" | jq -r '.unlimited_user // false')
|
||||||
|
|
||||||
if ! validate_username "$new_username"; then
|
if ! validate_username "$new_username"; then
|
||||||
echo "Invalid username: $new_username"
|
echo "Invalid username: $new_username"
|
||||||
@ -208,6 +225,10 @@ edit_user() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! validate_unlimited_status "$new_unlimited"; then
|
||||||
|
echo "Invalid unlimited status: $new_unlimited"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
new_username=${new_username:-$username}
|
new_username=${new_username:-$username}
|
||||||
@ -222,19 +243,18 @@ edit_user() {
|
|||||||
|
|
||||||
new_expiration_days=${new_expiration_days:-$expiration_days}
|
new_expiration_days=${new_expiration_days:-$expiration_days}
|
||||||
new_creation_date=${new_creation_date:-$creation_date}
|
new_creation_date=${new_creation_date:-$creation_date}
|
||||||
new_blocked=$(convert_blocked_status "${new_blocked:-$blocked}")
|
new_blocked=$(convert_boolean_status "${new_blocked:-$blocked}")
|
||||||
|
new_unlimited=$(convert_boolean_status "${new_unlimited:-$unlimited_user}")
|
||||||
|
|
||||||
# python3 $CLI_PATH restart-hysteria2
|
|
||||||
|
|
||||||
if ! update_user_info "$username" "$new_username" "$new_password" "$new_traffic_limit" "$new_expiration_days" "$new_creation_date" "$new_blocked"; then
|
if ! update_user_info "$username" "$new_username" "$new_password" "$new_traffic_limit" "$new_expiration_days" "$new_creation_date" "$new_blocked" "$new_unlimited"; then
|
||||||
return 1 # Update user failed
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "User updated successfully."
|
echo "User updated successfully."
|
||||||
return 0 # Operation complete without error.
|
return 0
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Run the script
|
edit_user "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8"
|
||||||
edit_user "$1" "$2" "$3" "$4" "$5" "$6" "$7"
|
|
||||||
@ -183,6 +183,23 @@ check_ip_limit() {
|
|||||||
local username="$1"
|
local username="$1"
|
||||||
local ips=()
|
local ips=()
|
||||||
|
|
||||||
|
local is_unlimited="false"
|
||||||
|
if [ -f "$USERS_FILE" ]; then
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
is_unlimited=$(jq -r --arg user "$username" '.[$user].unlimited_user // "false"' "$USERS_FILE" 2>/dev/null)
|
||||||
|
else
|
||||||
|
if grep -q "\"$username\"" "$USERS_FILE" && \
|
||||||
|
grep -A 5 "\"$username\"" "$USERS_FILE" | grep -q '"unlimited_user": true'; then
|
||||||
|
is_unlimited="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$is_unlimited" = "true" ]; then
|
||||||
|
log_message "INFO" "User $username is exempt from IP limit. Skipping check."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
# Get all IPs for this user
|
# Get all IPs for this user
|
||||||
if command -v jq &>/dev/null; then
|
if command -v jq &>/dev/null; then
|
||||||
readarray -t ips < <(jq -r --arg user "$username" '.[$user][]' "$CONNECTIONS_FILE" 2>/dev/null)
|
readarray -t ips < <(jq -r --arg user "$username" '.[$user][]' "$CONNECTIONS_FILE" 2>/dev/null)
|
||||||
|
|||||||
@ -1,23 +1,20 @@
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from pydantic import BaseModel, RootModel
|
from pydantic import BaseModel, RootModel, Field
|
||||||
|
|
||||||
|
|
||||||
# WE CAN'T USE SHARED SCHEMA BECAUSE THE CLI IS RETURNING SAME FIELD IN DIFFERENT NAMES SOMETIMES
|
|
||||||
# WHAT I'M SAYING IS THAT OUR CODE IN HERE WILL BE SPAGHETTI CODE IF WE WANT TO HAVE CONSISTENCY IN ALL RESPONSES OF THE APIs
|
|
||||||
# THE MAIN PROBLEM IS IN THE CLI CODE NOT IN THE WEB PANEL CODE (HERE)
|
|
||||||
|
|
||||||
class UserInfoResponse(BaseModel):
|
class UserInfoResponse(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
max_download_bytes: int
|
max_download_bytes: int
|
||||||
expiration_days: int
|
expiration_days: int
|
||||||
account_creation_date: str
|
account_creation_date: str
|
||||||
blocked: bool
|
blocked: bool
|
||||||
status: str | None = None
|
unlimited_ip: bool = Field(False, alias='unlimited_user')
|
||||||
upload_bytes: int | None = None
|
status: Optional[str] = None
|
||||||
download_bytes: int | None = None
|
upload_bytes: Optional[int] = None
|
||||||
|
download_bytes: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class UserListResponse(RootModel): # type: ignore
|
class UserListResponse(RootModel):
|
||||||
root: dict[str, UserInfoResponse]
|
root: dict[str, UserInfoResponse]
|
||||||
|
|
||||||
|
|
||||||
@ -25,18 +22,19 @@ class AddUserInputBody(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
traffic_limit: int
|
traffic_limit: int
|
||||||
expiration_days: int
|
expiration_days: int
|
||||||
password: str | None = None
|
password: Optional[str] = None
|
||||||
creation_date: str | None = None
|
creation_date: Optional[str] = None
|
||||||
|
unlimited: bool = False
|
||||||
|
|
||||||
|
|
||||||
class EditUserInputBody(BaseModel):
|
class EditUserInputBody(BaseModel):
|
||||||
# username: str
|
new_username: Optional[str] = None
|
||||||
new_username: str | None = None
|
new_traffic_limit: Optional[int] = None
|
||||||
new_traffic_limit: int | None = None
|
new_expiration_days: Optional[int] = None
|
||||||
new_expiration_days: int | None = None
|
|
||||||
renew_password: bool = False
|
renew_password: bool = False
|
||||||
renew_creation_date: bool = False
|
renew_creation_date: bool = False
|
||||||
blocked: bool = False
|
blocked: Optional[bool] = None
|
||||||
|
unlimited_ip: Optional[bool] = None
|
||||||
|
|
||||||
class NodeUri(BaseModel):
|
class NodeUri(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@ -39,7 +39,7 @@ async def add_user_api(body: AddUserInputBody):
|
|||||||
detail=f"{str(e)}")
|
detail=f"{str(e)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cli_api.add_user(body.username, body.traffic_limit, body.expiration_days, body.password, body.creation_date)
|
cli_api.add_user(body.username, body.traffic_limit, body.expiration_days, body.password, body.creation_date, body.unlimited)
|
||||||
return DetailResponse(detail=f'User {body.username} has been added.')
|
return DetailResponse(detail=f'User {body.username} has been added.')
|
||||||
except cli_api.CommandExecutionError as e:
|
except cli_api.CommandExecutionError as e:
|
||||||
if "User already exists" in str(e):
|
if "User already exists" in str(e):
|
||||||
@ -98,7 +98,7 @@ async def edit_user_api(username: str, body: EditUserInputBody):
|
|||||||
cli_api.kick_user_by_name(username)
|
cli_api.kick_user_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_traffic_limit, body.new_expiration_days,
|
||||||
body.renew_password, body.renew_creation_date, body.blocked)
|
body.renew_password, body.renew_creation_date, body.blocked, body.unlimited_ip)
|
||||||
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:
|
||||||
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class User(BaseModel):
|
|||||||
expiry_date: str
|
expiry_date: str
|
||||||
expiry_days: str
|
expiry_days: str
|
||||||
enable: bool
|
enable: bool
|
||||||
|
unlimited_ip: bool
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(username: str, user_data: dict):
|
def from_dict(username: str, user_data: dict):
|
||||||
@ -58,6 +59,7 @@ class User(BaseModel):
|
|||||||
'expiry_date': display_expiry_date,
|
'expiry_date': display_expiry_date,
|
||||||
'expiry_days': display_expiry_days,
|
'expiry_days': display_expiry_days,
|
||||||
'enable': not user_data.get('blocked', False),
|
'enable': not user_data.get('blocked', False),
|
||||||
|
'unlimited_ip': user_data.get('unlimited_user', False)
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -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 requires-iplimit-service" style="display: none;">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 class="unlimited-ip-cell requires-iplimit-service" style="display: none;">
|
||||||
|
{% 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 requires-iplimit-service" style="display: none;">
|
||||||
|
<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 requires-iplimit-service" style="display: none;">
|
||||||
|
<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>
|
||||||
@ -243,6 +259,19 @@
|
|||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
|
|
||||||
|
function checkIpLimitServiceStatus() {
|
||||||
|
fetch('{{ url_for("server_services_status_api") }}')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.hysteria_iplimit === true) {
|
||||||
|
$('.requires-iplimit-service').show();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching IP limit service status:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIpLimitServiceStatus();
|
||||||
|
|
||||||
const usernameRegex = /^[a-zA-Z0-9]+$/;
|
const usernameRegex = /^[a-zA-Z0-9]+$/;
|
||||||
|
|
||||||
function validateUsername(username, errorElementId) {
|
function validateUsername(username, errorElementId) {
|
||||||
@ -259,7 +288,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 +415,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 +464,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(".unlimited-ip-cell i").hasClass("text-primary");
|
||||||
|
|
||||||
const expiryDaysValue = (expiryDaysText.toLowerCase() === 'unlimited') ? 0 : parseInt(expiryDaysText, 10);
|
const expiryDaysValue = (expiryDaysText.toLowerCase() === 'unlimited') ? 0 : parseInt(expiryDaysText, 10);
|
||||||
|
|
||||||
@ -455,6 +485,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 +496,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 +519,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 +533,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 +677,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 +786,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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
58
menu.sh
58
menu.sh
@ -52,7 +52,7 @@ hysteria2_add_user_handler() {
|
|||||||
read -p "Enter the username: " username
|
read -p "Enter the username: " username
|
||||||
|
|
||||||
if [[ "$username" =~ ^[a-zA-Z0-9]+$ ]]; then
|
if [[ "$username" =~ ^[a-zA-Z0-9]+$ ]]; then
|
||||||
if [[ -n $(python3 $CLI_PATH get-user -u "$username") ]]; then
|
if [[ -n $(python3 $CLI_PATH get-user -u "$username" 2>/dev/null) ]]; then
|
||||||
echo -e "${red}Error:${NC} Username already exists. Please choose another username."
|
echo -e "${red}Error:${NC} Username already exists. Please choose another username."
|
||||||
else
|
else
|
||||||
break
|
break
|
||||||
@ -65,14 +65,24 @@ hysteria2_add_user_handler() {
|
|||||||
read -p "Enter the traffic limit (in GB): " traffic_limit_GB
|
read -p "Enter the traffic limit (in GB): " traffic_limit_GB
|
||||||
|
|
||||||
read -p "Enter the expiration days: " expiration_days
|
read -p "Enter the expiration days: " expiration_days
|
||||||
|
|
||||||
|
local unlimited_arg=""
|
||||||
|
while true; do
|
||||||
|
read -p "Exempt user from IP limit checks (unlimited IP)? (y/n) [n]: " unlimited_choice
|
||||||
|
case "$unlimited_choice" in
|
||||||
|
y|Y) unlimited_arg="--unlimited"; break ;;
|
||||||
|
n|N|"") break ;;
|
||||||
|
*) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
password=$(pwgen -s 32 1)
|
password=$(pwgen -s 32 1)
|
||||||
creation_date=$(date +%Y-%m-%d)
|
creation_date=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
python3 $CLI_PATH add-user --username "$username" --traffic-limit "$traffic_limit_GB" --expiration-days "$expiration_days" --password "$password" --creation-date "$creation_date"
|
python3 $CLI_PATH add-user --username "$username" --traffic-limit "$traffic_limit_GB" --expiration-days "$expiration_days" --password "$password" --creation-date "$creation_date" $unlimited_arg
|
||||||
}
|
}
|
||||||
|
|
||||||
hysteria2_edit_user_handler() {
|
hysteria2_edit_user_handler() {
|
||||||
# Function to prompt for user input with validation
|
|
||||||
prompt_for_input() {
|
prompt_for_input() {
|
||||||
local prompt_message="$1"
|
local prompt_message="$1"
|
||||||
local validation_regex="$2"
|
local validation_regex="$2"
|
||||||
@ -93,65 +103,69 @@ hysteria2_edit_user_handler() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prompt for username
|
|
||||||
prompt_for_input "Enter the username you want to edit: " '^[a-zA-Z0-9]+$' '' username
|
prompt_for_input "Enter the username you want to edit: " '^[a-zA-Z0-9]+$' '' username
|
||||||
|
|
||||||
# Check if user exists
|
|
||||||
user_exists_output=$(python3 $CLI_PATH get-user -u "$username" 2>&1)
|
user_exists_output=$(python3 $CLI_PATH get-user -u "$username" 2>&1)
|
||||||
if [[ -z "$user_exists_output" ]]; then
|
if [[ -z "$user_exists_output" ]]; then
|
||||||
echo -e "${red}Error:${NC} User '$username' not found or an error occurred."
|
echo -e "${red}Error:${NC} User '$username' not found or an error occurred."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Prompt for new username
|
|
||||||
prompt_for_input "Enter the new username (leave empty to keep the current username): " '^[a-zA-Z0-9]*$' '' new_username
|
prompt_for_input "Enter the new username (leave empty to keep the current username): " '^[a-zA-Z0-9]*$' '' new_username
|
||||||
|
|
||||||
# Prompt for new traffic limit
|
|
||||||
prompt_for_input "Enter the new traffic limit (in GB) (leave empty to keep the current limit): " '^[0-9]*$' '' new_traffic_limit_GB
|
prompt_for_input "Enter the new traffic limit (in GB) (leave empty to keep the current limit): " '^[0-9]*$' '' new_traffic_limit_GB
|
||||||
|
|
||||||
# Prompt for new expiration days
|
|
||||||
prompt_for_input "Enter the new expiration days (leave empty to keep the current expiration days): " '^[0-9]*$' '' new_expiration_days
|
prompt_for_input "Enter the new expiration days (leave empty to keep the current expiration days): " '^[0-9]*$' '' new_expiration_days
|
||||||
|
|
||||||
# Determine if we need to renew password
|
|
||||||
while true; do
|
while true; do
|
||||||
read -p "Do you want to generate a new password? (y/n): " renew_password
|
read -p "Do you want to generate a new password? (y/n) [n]: " renew_password
|
||||||
case "$renew_password" in
|
case "$renew_password" in
|
||||||
y|Y) renew_password=true; break ;;
|
y|Y) renew_password=true; break ;;
|
||||||
n|N) renew_password=false; break ;;
|
n|N|"") renew_password=false; break ;;
|
||||||
*) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;;
|
*) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Determine if we need to renew creation date
|
|
||||||
while true; do
|
while true; do
|
||||||
read -p "Do you want to generate a new creation date? (y/n): " renew_creation_date
|
read -p "Do you want to generate a new creation date? (y/n) [n]: " renew_creation_date
|
||||||
case "$renew_creation_date" in
|
case "$renew_creation_date" in
|
||||||
y|Y) renew_creation_date=true; break ;;
|
y|Y) renew_creation_date=true; break ;;
|
||||||
n|N) renew_creation_date=false; break ;;
|
n|N|"") renew_creation_date=false; break ;;
|
||||||
*) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;;
|
*) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Determine if user should be blocked
|
local blocked_arg=""
|
||||||
while true; do
|
while true; do
|
||||||
read -p "Do you want to block the user? (y/n): " block_user
|
read -p "Change user block status? ([b]lock/[u]nblock/[s]kip) [s]: " block_user
|
||||||
case "$block_user" in
|
case "$block_user" in
|
||||||
y|Y) blocked=true; break ;;
|
b|B) blocked_arg="--blocked"; break ;;
|
||||||
n|N) blocked=false; break ;;
|
u|U) blocked_arg="--unblocked"; break ;;
|
||||||
*) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;;
|
s|S|"") break ;;
|
||||||
|
*) echo -e "${red}Error:${NC} Please answer 'b', 'u', or 's'." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local ip_limit_arg=""
|
||||||
|
while true; do
|
||||||
|
read -p "Change IP limit status? ([u]nlimited/[l]imited/[s]kip) [s]: " ip_limit_status
|
||||||
|
case "$ip_limit_status" in
|
||||||
|
u|U) ip_limit_arg="--unlimited-ip"; break ;;
|
||||||
|
l|L) ip_limit_arg="--limited-ip"; break ;;
|
||||||
|
s|S|"") break ;;
|
||||||
|
*) echo -e "${red}Error:${NC} Please answer 'u', 'l', or 's'." ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Construct the arguments for the edit-user command
|
|
||||||
args=()
|
args=()
|
||||||
if [[ -n "$new_username" ]]; then args+=("--new-username" "$new_username"); fi
|
if [[ -n "$new_username" ]]; then args+=("--new-username" "$new_username"); fi
|
||||||
if [[ -n "$new_traffic_limit_GB" ]]; then args+=("--new-traffic-limit" "$new_traffic_limit_GB"); fi
|
if [[ -n "$new_traffic_limit_GB" ]]; then args+=("--new-traffic-limit" "$new_traffic_limit_GB"); fi
|
||||||
if [[ -n "$new_expiration_days" ]]; then args+=("--new-expiration-days" "$new_expiration_days"); fi
|
if [[ -n "$new_expiration_days" ]]; then args+=("--new-expiration-days" "$new_expiration_days"); fi
|
||||||
if [[ "$renew_password" == "true" ]]; then args+=("--renew-password"); fi
|
if [[ "$renew_password" == "true" ]]; then args+=("--renew-password"); fi
|
||||||
if [[ "$renew_creation_date" == "true" ]]; then args+=("--renew-creation-date"); fi
|
if [[ "$renew_creation_date" == "true" ]]; then args+=("--renew-creation-date"); fi
|
||||||
if [[ "$blocked" == "true" ]]; then args+=("--blocked"); fi
|
if [[ -n "$blocked_arg" ]]; then args+=("$blocked_arg"); fi
|
||||||
|
if [[ -n "$ip_limit_arg" ]]; then args+=("$ip_limit_arg"); fi
|
||||||
|
|
||||||
# Call the edit-user script with the constructed arguments
|
|
||||||
python3 $CLI_PATH edit-user --username "$username" "${args[@]}"
|
python3 $CLI_PATH edit-user --username "$username" "${args[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ aiosignal==1.4.0
|
|||||||
async-timeout==5.0.1
|
async-timeout==5.0.1
|
||||||
attrs==25.3.0
|
attrs==25.3.0
|
||||||
certifi==2025.7.14
|
certifi==2025.7.14
|
||||||
charset-normalizer==3.4.2
|
charset-normalizer==3.4.3
|
||||||
click==8.1.8
|
click==8.1.8
|
||||||
frozenlist==1.7.0
|
frozenlist==1.7.0
|
||||||
idna==3.10
|
idna==3.10
|
||||||
@ -25,7 +25,7 @@ schedule==1.2.2
|
|||||||
|
|
||||||
# webpanel
|
# webpanel
|
||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
anyio==4.9.0
|
anyio==4.10.0
|
||||||
fastapi==0.116.1
|
fastapi==0.116.1
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
@ -40,4 +40,4 @@ hypercorn==0.17.3
|
|||||||
pydantic-settings==2.10.1
|
pydantic-settings==2.10.1
|
||||||
|
|
||||||
# subs
|
# subs
|
||||||
certbot==4.1.1
|
certbot==4.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user