diff --git a/changelog b/changelog index ad97071..9135fa2 100644 --- a/changelog +++ b/changelog @@ -1,18 +1,22 @@ -# [1.17.0] - 2025-08-24 +# [1.18.0] - 2025-08-27 +#### β‘ Performance -#### β‘ Authentication +* β‘ **Optimized bulk user URI fetching**: -* π **Implemented Go HTTP Auth Server** for **maximum performance** -* β‘ Removed old command-based auth system + * New API endpoint `/api/v1/users/uri/bulk` to fetch multiple user links in a single call + * Eliminates N separate script executions β **huge speedup** π +* β‘ Refactored `wrapper_uri.py` for faster bulk processing & maintainability -#### π₯ User Management +#### β¨ Features -* β¨ **Bulk User Creation** added across: +* π€ **Bulk user link export** directly from the **Users Page** +* π¨ Distinct **color coding** for user statuses in Web Panel +* βΈοΈ **On-Hold User Activation** logic introduced in `traffic.py` (with `creation_date=None` default) - * π₯οΈ **Frontend UI** - * π‘ **API Endpoint** - * π» **CLI Command** - * π **Automation Script** -* π New **Online User Filter & Sort** on the Users page -* π Fixed: underscores now supported in usernames \ No newline at end of file +#### π Fixes & Refactors + +* π€ **Bot**: Properly handle escaped underscores in usernames +* π οΈ **Webpanel**: Improved handling of malformed user data & more accurate status for on-hold users +* π Show Go installation correctly +* π Refactored on-hold user logic into `traffic.py` for central management diff --git a/core/cli.py b/core/cli.py index 223ba0e..e3bef53 100644 --- a/core/cli.py +++ b/core/cli.py @@ -160,7 +160,7 @@ def bulk_user_add(traffic_gb: float, expiration_days: int, count: int, prefix: s @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-creation-date', '-rc', is_flag=True, help='Renew creation date for the user') -@click.option('--blocked/--unblocked', 'blocked', 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.') 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: diff --git a/core/scripts/hysteria2/add_user.py b/core/scripts/hysteria2/add_user.py index ec23b0a..d9853e7 100644 --- a/core/scripts/hysteria2/add_user.py +++ b/core/scripts/hysteria2/add_user.py @@ -18,7 +18,7 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date traffic_gb (str): The traffic limit in GB. 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. - 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. Defaults to None. unlimited_user (bool, optional): If True, user is exempt from IP limits. Defaults to False. Returns: @@ -48,9 +48,7 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date print("Error: Failed to generate password. Please install 'pwgen' or ensure /proc access.") return 1 - if not creation_date: - creation_date = datetime.now().strftime("%Y-%m-%d") - else: + if creation_date: if not re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", creation_date): print("Invalid date format. Expected YYYY-MM-DD.") return 1 @@ -59,6 +57,8 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date except ValueError: print("Invalid date. Please provide a valid date in YYYY-MM-DD format.") return 1 + else: + creation_date = None if not re.match(r"^[a-zA-Z0-9_]+$", username): print("Error: Username can only contain letters, numbers, and underscores.") diff --git a/core/scripts/hysteria2/bulk_users.py b/core/scripts/hysteria2/bulk_users.py index b1f4618..1fdd23f 100644 --- a/core/scripts/hysteria2/bulk_users.py +++ b/core/scripts/hysteria2/bulk_users.py @@ -35,7 +35,7 @@ def add_bulk_users(traffic_gb, expiration_days, count, prefix, start_number, unl existing_users_lower = {u.lower() for u in users_data} new_users_to_add = {} - creation_date = datetime.now().strftime("%Y-%m-%d") + creation_date = None try: password_process = subprocess.run(['pwgen', '-s', '32', str(count)], capture_output=True, text=True, check=True) diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py index 86ae88b..57a0d4a 100644 --- a/core/scripts/hysteria2/wrapper_uri.py +++ b/core/scripts/hysteria2/wrapper_uri.py @@ -1,65 +1,128 @@ -import subprocess -import concurrent.futures -import re -import json +#!/usr/bin/env python3 + +import os import sys +import json +import argparse +from functools import lru_cache +from typing import Dict, List, Any from init_paths import * from paths import * -DEFAULT_ARGS = ["-a", "-n", "-s"] - -def run_show_uri(username): +@lru_cache(maxsize=None) +def load_json_file(file_path: str) -> Any: + if not os.path.exists(file_path): + return None try: - cmd = ["python3", CLI_PATH, "show-user-uri", "-u", username] + DEFAULT_ARGS - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - output = result.stdout - if "Invalid username" in output: - return {"username": username, "error": "User not found"} - return parse_output(username, output) - except subprocess.CalledProcessError as e: - return {"username": username, "error": e.stderr.strip()} + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + return json.loads(content) if content else None + except (json.JSONDecodeError, IOError): + return None -def parse_output(username, output): - ipv4 = None - ipv6 = None - normal_sub = None - nodes = [] +@lru_cache(maxsize=None) +def load_env_file(env_file: str) -> Dict[str, str]: + env_vars = {} + if os.path.exists(env_file): + with open(env_file, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_vars[key] = value.strip() + return env_vars - ipv4_match = re.search(r"IPv4:\s*(hy2://[^\s]+)", output) - ipv6_match = re.search(r"IPv6:\s*(hy2://[^\s]+)", output) - normal_sub_match = re.search(r"Normal-SUB Sublink:\s*(https?://[^\s]+)", output) +def generate_uri(username: str, auth_password: str, ip: str, port: str, + uri_params: Dict[str, str], ip_version: int, fragment_tag: str) -> str: + ip_part = f"[{ip}]" if ip_version == 6 and ':' in ip else ip + uri_base = f"hy2://{username}:{auth_password}@{ip_part}:{port}" + query_string = "&".join([f"{k}={v}" for k, v in uri_params.items()]) + return f"{uri_base}?{query_string}#{fragment_tag}" - if ipv4_match: - ipv4 = ipv4_match.group(1) - if ipv6_match: - ipv6 = ipv6_match.group(1) - if normal_sub_match: - normal_sub = normal_sub_match.group(1) - - node_matches = re.findall(r"Node: (.+?) \(IPv[46]\):\s*(hy2://[^\s]+)", output) - for name, uri in node_matches: - nodes.append({"name": name.strip(), "uri": uri}) - - - return { - "username": username, - "ipv4": ipv4, - "ipv6": ipv6, - "nodes": nodes, - "normal_sub": normal_sub - } - -def batch_show_uri(usernames, max_workers=20): - with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - results = list(executor.map(run_show_uri, usernames)) - return results - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python3 show_uri_json.py user1 user2 ...") +def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: + config = load_json_file(CONFIG_FILE) + all_users = load_json_file(USERS_FILE) + + if not config or not all_users: + print("Error: Could not load Hysteria2 configuration or user files.", file=sys.stderr) sys.exit(1) - usernames = sys.argv[1:] - output_list = batch_show_uri(usernames) - print(json.dumps(output_list, indent=2)) \ No newline at end of file + nodes = load_json_file(NODES_JSON_PATH) or [] + port = config.get("listen", "").split(":")[-1] + tls_config = config.get("tls", {}) + hy2_env = load_env_file(CONFIG_ENV) + ns_env = load_env_file(NORMALSUB_ENV) + + base_uri_params = { + "insecure": "1" if tls_config.get("insecure", True) else "0", + "sni": hy2_env.get('SNI', '') + } + obfs_password = config.get("obfs", {}).get("salamander", {}).get("password") + if obfs_password: + base_uri_params["obfs"] = "salamander" + base_uri_params["obfs-password"] = obfs_password + + sha256 = tls_config.get("pinSHA256") + if sha256: + base_uri_params["pinSHA256"] = sha256 + + ip4 = hy2_env.get('IP4') + ip6 = hy2_env.get('IP6') + ns_domain, ns_port, ns_subpath = ns_env.get('HYSTERIA_DOMAIN'), ns_env.get('HYSTERIA_PORT'), ns_env.get('SUBPATH') + + results = [] + for username in target_usernames: + user_data = all_users.get(username) + if not user_data or "password" not in user_data: + results.append({"username": username, "error": "User not found or password not set"}) + continue + + auth_password = user_data["password"] + user_output = {"username": username, "ipv4": None, "ipv6": None, "nodes": [], "normal_sub": None} + + if ip4 and ip4 != "None": + user_output["ipv4"] = generate_uri(username, auth_password, ip4, port, base_uri_params, 4, f"{username}-IPv4") + if ip6 and ip6 != "None": + user_output["ipv6"] = generate_uri(username, auth_password, ip6, port, base_uri_params, 6, f"{username}-IPv6") + + for node in nodes: + if node_name := node.get("name"): + if node_ip := node.get("ip"): + ip_v = 6 if ':' in node_ip else 4 + tag = f"{username}-{node_name}" + uri = generate_uri(username, auth_password, node_ip, port, base_uri_params, ip_v, tag) + user_output["nodes"].append({"name": node_name, "uri": uri}) + + if ns_domain and ns_port and ns_subpath: + user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/sub/normal/{auth_password}#{username}" + + results.append(user_output) + + return results + +def main(): + parser = argparse.ArgumentParser(description="Efficiently generate Hysteria2 URIs for multiple users.") + parser.add_argument('usernames', nargs='*', help="A list of usernames to process.") + parser.add_argument('--all', action='store_true', help="Process all users from users.json.") + + args = parser.parse_args() + target_usernames = args.usernames + + if args.all: + all_users = load_json_file(USERS_FILE) + if all_users: + target_usernames = list(all_users.keys()) + else: + print("Error: Could not load users.json to process all users.", file=sys.stderr) + sys.exit(1) + + if not target_usernames: + parser.print_help() + sys.exit(1) + + output_list = process_users(target_usernames) + print(json.dumps(output_list, indent=2)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/core/scripts/scheduler.py b/core/scripts/scheduler.py index a733a1c..ff3b2e1 100644 --- a/core/scripts/scheduler.py +++ b/core/scripts/scheduler.py @@ -1,12 +1,9 @@ #!/usr/bin/env python3 -import os -import sys import time import schedule import logging import subprocess import fcntl -import datetime from pathlib import Path from paths import * @@ -23,7 +20,6 @@ logger = logging.getLogger("HysteriaScheduler") # Constants BASE_DIR = Path("/etc/hysteria") VENV_ACTIVATE = BASE_DIR / "hysteria2_venv/bin/activate" -# CLI_PATH = BASE_DIR / "core/cli.py" LOCK_FILE = "/tmp/hysteria_scheduler.lock" def acquire_lock(): @@ -41,7 +37,6 @@ def release_lock(lock_fd): lock_fd.close() def run_command(command, log_success=False): - activate_cmd = f"source {VENV_ACTIVATE}" full_cmd = f"{activate_cmd} && {command}" @@ -94,6 +89,7 @@ def main(): schedule.every(1).minutes.do(check_traffic_status) schedule.every(6).hours.do(backup_hysteria) + check_traffic_status() backup_hysteria() while True: diff --git a/core/scripts/telegrambot/utils/adduser.py b/core/scripts/telegrambot/utils/adduser.py index 7298f2e..b81d3c4 100644 --- a/core/scripts/telegrambot/utils/adduser.py +++ b/core/scripts/telegrambot/utils/adduser.py @@ -1,10 +1,13 @@ import qrcode import io import json +import re from telebot import types from utils.command import * from utils.common import create_main_markup +def escape_markdown(text): + return str(text).replace('_', '\\_').replace('*', '\\*').replace('`', '\\`') def create_cancel_markup(back_step=None): markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) @@ -15,7 +18,7 @@ def create_cancel_markup(back_step=None): @bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'Add User') def add_user(message): - msg = bot.reply_to(message, "Enter username:", reply_markup=create_cancel_markup()) + msg = bot.reply_to(message, "Enter username (only letters, numbers, and underscores are allowed):", reply_markup=create_cancel_markup()) bot.register_next_step_handler(msg, process_add_user_step1) def process_add_user_step1(message): @@ -24,6 +27,12 @@ def process_add_user_step1(message): return username = message.text.strip() + + if not re.match("^[a-zA-Z0-9_]*$", username): + bot.reply_to(message, "Invalid username. Only letters, numbers, and underscores are allowed. Please try again:", reply_markup=create_cancel_markup()) + bot.register_next_step_handler(message, process_add_user_step1) + return + if not username: bot.reply_to(message, "Username cannot be empty. Please enter a valid username:", reply_markup=create_cancel_markup()) bot.register_next_step_handler(message, process_add_user_step1) @@ -41,7 +50,7 @@ def process_add_user_step1(message): users_data = json.loads(result) existing_users = {user_key.lower() for user_key in users_data.keys()} if username.lower() in existing_users: - bot.reply_to(message, f"Username '{username}' already exists. Please choose a different username:", reply_markup=create_cancel_markup()) + bot.reply_to(message, f"Username '{escape_markdown(username)}' already exists. Please choose a different username:", reply_markup=create_cancel_markup()) bot.register_next_step_handler(message, process_add_user_step1) return except json.JSONDecodeError: @@ -59,7 +68,7 @@ def process_add_user_step2(message, username): bot.reply_to(message, "Process canceled.", reply_markup=create_main_markup()) return if message.text == "β¬ οΈ Back": - msg = bot.reply_to(message, "Enter username:", reply_markup=create_cancel_markup()) + msg = bot.reply_to(message, "Enter username (only letters, numbers, and underscores are allowed):", reply_markup=create_cancel_markup()) bot.register_next_step_handler(msg, process_add_user_step1) return @@ -96,8 +105,7 @@ def process_add_user_step3(message, username, traffic_limit): bot.send_chat_action(message.chat.id, 'typing') - lower_username = username.lower() - uri_info_command = f"python3 {CLI_PATH} show-user-uri -u \"{lower_username}\" -ip 4 -n" + uri_info_command = f"python3 {CLI_PATH} show-user-uri -u \"{username}\" -ip 4 -n" uri_info_output = run_cli_command(uri_info_command) direct_uri = None @@ -121,18 +129,20 @@ def process_add_user_step3(message, username, traffic_limit): except (IndexError, AttributeError): pass - caption_text = f"{add_user_feedback}\n" + display_username = escape_markdown(username) + escaped_feedback = escape_markdown(add_user_feedback) + caption_text = f"{escaped_feedback}\n" link_to_generate_qr_for = None link_type_for_caption = "" if normal_sub_link: link_to_generate_qr_for = normal_sub_link link_type_for_caption = "Normal Subscription Link" - caption_text += f"\n{link_type_for_caption} for `{username}`:\n`{normal_sub_link}`" + caption_text += f"\n{link_type_for_caption} for `{display_username}`:\n`{normal_sub_link}`" elif direct_uri: link_to_generate_qr_for = direct_uri link_type_for_caption = "Hysteria2 IPv4 URI" - caption_text += f"\n{link_type_for_caption} for `{username}`:\n`{direct_uri}`" + caption_text += f"\n{link_type_for_caption} for `{display_username}`:\n`{direct_uri}`" if link_to_generate_qr_for: qr_img = qrcode.make(link_to_generate_qr_for) diff --git a/core/scripts/telegrambot/utils/edituser.py b/core/scripts/telegrambot/utils/edituser.py index 537084c..bc9eed4 100644 --- a/core/scripts/telegrambot/utils/edituser.py +++ b/core/scripts/telegrambot/utils/edituser.py @@ -1,5 +1,3 @@ -#show and edituser file - import qrcode import io import json @@ -8,6 +6,9 @@ from utils.command import * from utils.common import * +def escape_markdown(text): + return str(text).replace('_', '\\_').replace('*', '\\*').replace('`', '\\`') + @bot.callback_query_handler(func=lambda call: call.data == "cancel_show_user") def handle_cancel_show_user(call): bot.edit_message_text("Operation canceled.", chat_id=call.message.chat.id, message_id=call.message.message_id) @@ -33,7 +34,7 @@ def process_show_user(message): existing_users = {user.lower(): user for user in users.keys()} if username not in existing_users: - bot.reply_to(message, f"Username '{message.text.strip()}' does not exist. Please enter a valid username.") + bot.reply_to(message, f"Username '{escape_markdown(message.text.strip())}' does not exist. Please enter a valid username.") return actual_username = existing_users[username] @@ -54,8 +55,8 @@ def process_show_user(message): if upload_bytes is None or download_bytes is None: traffic_message = "**Traffic Data:**\nUser not active or no traffic data available." else: - upload_gb = upload_bytes / (1024 ** 3) # Convert bytes to GB - download_gb = download_bytes / (1024 ** 3) # Convert bytes to GB + upload_gb = upload_bytes / (1024 ** 3) + download_gb = download_bytes / (1024 ** 3) totalusage = upload_gb + download_gb traffic_message = ( @@ -68,8 +69,10 @@ def process_show_user(message): bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.") return + display_username = escape_markdown(actual_username) + formatted_details = ( - f"\nπ Name: {actual_username}\n" + f"\nπ Name: {display_username}\n" f"π Traffic Limit: {user_details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" f"π Days: {user_details['expiration_days']}\n" f"β³ Creation: {user_details['account_creation_date']}\n" @@ -87,15 +90,12 @@ def process_show_user(message): result_lines = combined_result.strip().split('\n') uri_v4 = "" - # singbox_sublink = "" normal_sub_sublink = "" for line in result_lines: line = line.strip() if line.startswith("hy2://"): uri_v4 = line - # elif line.startswith("Singbox Sublink:"): - # singbox_sublink = result_lines[result_lines.index(line) + 1].strip() elif line.startswith("Normal-SUB Sublink:"): normal_sub_sublink = result_lines[result_lines.index(line) + 1].strip() @@ -119,8 +119,6 @@ def process_show_user(message): types.InlineKeyboardButton("Block User", callback_data=f"block_user:{actual_username}")) caption = f"{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`" - # if singbox_sublink: - # caption += f"\n\n**SingBox SUB:**\n{singbox_sublink}" if normal_sub_sublink: caption += f"\n\n**Normal SUB:**\n{normal_sub_sublink}" @@ -135,14 +133,16 @@ def process_show_user(message): @bot.callback_query_handler(func=lambda call: call.data.startswith('edit_') or call.data.startswith('renew_') or call.data.startswith('block_') or call.data.startswith('reset_') or call.data.startswith('ipv6_')) def handle_edit_callback(call): action, username = call.data.split(':') + display_username = escape_markdown(username) + if action == 'edit_username': - msg = bot.send_message(call.message.chat.id, f"Enter new username for {username}:") + msg = bot.send_message(call.message.chat.id, f"Enter new username for {display_username}:") bot.register_next_step_handler(msg, process_edit_username, username) elif action == 'edit_traffic': - msg = bot.send_message(call.message.chat.id, f"Enter new traffic limit (GB) for {username}:") + msg = bot.send_message(call.message.chat.id, f"Enter new traffic limit (GB) for {display_username}:") bot.register_next_step_handler(msg, process_edit_traffic, username) elif action == 'edit_expiration': - msg = bot.send_message(call.message.chat.id, f"Enter new expiration days for {username}:") + msg = bot.send_message(call.message.chat.id, f"Enter new expiration days for {display_username}:") bot.register_next_step_handler(msg, process_edit_expiration, username) elif action == 'renew_password': command = f"python3 {CLI_PATH} edit-user -u {username} -rp" @@ -156,7 +156,7 @@ def handle_edit_callback(call): markup = types.InlineKeyboardMarkup() markup.add(types.InlineKeyboardButton("True", callback_data=f"confirm_block:{username}:true"), types.InlineKeyboardButton("False", callback_data=f"confirm_block:{username}:false")) - bot.send_message(call.message.chat.id, f"Set block status for {username}:", reply_markup=markup) + bot.send_message(call.message.chat.id, f"Set block status for {display_username}:", reply_markup=markup) elif action == 'reset_user': command = f"python3 {CLI_PATH} reset-user -u {username}" result = run_cli_command(command) @@ -177,7 +177,7 @@ def handle_edit_callback(call): bot.send_photo( call.message.chat.id, bio_v6, - caption=f"**IPv6 URI for {username}:**\n\n`{uri_v6}`", + caption=f"**IPv6 URI for {display_username}:**\n\n`{uri_v6}`", parse_mode="Markdown" ) diff --git a/core/scripts/telegrambot/utils/search.py b/core/scripts/telegrambot/utils/search.py index 31821e0..684c87d 100644 --- a/core/scripts/telegrambot/utils/search.py +++ b/core/scripts/telegrambot/utils/search.py @@ -11,7 +11,7 @@ def handle_inline_query(query): bot.answer_inline_query(query.id, results=[], switch_pm_text="Error retrieving users.", switch_pm_user_id=query.from_user.id) return - query_text = query.query.lower() + query_text = query.query.lower().replace('\\_', '_') results = [] if query_text == "block": diff --git a/core/scripts/webpanel/routers/api/v1/schema/user.py b/core/scripts/webpanel/routers/api/v1/schema/user.py index 4f69289..64dc768 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/user.py +++ b/core/scripts/webpanel/routers/api/v1/schema/user.py @@ -18,6 +18,8 @@ class UserInfoResponse(BaseModel): class UserListResponse(RootModel): root: dict[str, UserInfoResponse] +class UsernamesRequest(BaseModel): + usernames: List[str] class AddUserInputBody(BaseModel): username: str diff --git a/core/scripts/webpanel/routers/api/v1/user.py b/core/scripts/webpanel/routers/api/v1/user.py index 2164a48..4d57a31 100644 --- a/core/scripts/webpanel/routers/api/v1/user.py +++ b/core/scripts/webpanel/routers/api/v1/user.py @@ -1,7 +1,15 @@ import json +from typing import List from fastapi import APIRouter, HTTPException - -from .schema.user import UserListResponse, UserInfoResponse, AddUserInputBody, EditUserInputBody, UserUriResponse, AddBulkUsersInputBody +from .schema.user import ( + UserListResponse, + UserInfoResponse, + AddUserInputBody, + EditUserInputBody, + UserUriResponse, + AddBulkUsersInputBody, + UsernamesRequest +) from .schema.response import DetailResponse import cli_api @@ -79,6 +87,29 @@ async def add_bulk_users_api(body: AddBulkUsersInputBody): raise HTTPException(status_code=500, detail=f"An unexpected error occurred while adding bulk users: {str(e)}") +@router.post('/uri/bulk', response_model=List[UserUriResponse]) +async def show_multiple_user_uris_api(request: UsernamesRequest): + """ + Get URI information for multiple users in a single request for efficiency. + """ + if not request.usernames: + return [] + + try: + uri_data_list = cli_api.show_user_uri_json(request.usernames) + if not uri_data_list: + raise HTTPException(status_code=404, detail='No URI data found for the provided users.') + + valid_responses = [data for data in uri_data_list if not data.get('error')] + + return valid_responses + except cli_api.ScriptNotFoundError as e: + raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}') + except cli_api.CommandExecutionError as e: + raise HTTPException(status_code=400, detail=f'Error executing script: {str(e)}') + except Exception as e: + raise HTTPException(status_code=400, detail=f'Unexpected error: {str(e)}') + @router.get('/{username}', response_model=UserInfoResponse) async def get_user_api(username: str): @@ -149,6 +180,7 @@ async def remove_user_api(username: str): cli_api.kick_user_by_name(username) cli_api.traffic_status(display_output=False) cli_api.remove_user(username) + cli_api.traffic_status(display_output=False) return DetailResponse(detail=f'User {username} has been removed.') except HTTPException: diff --git a/core/scripts/webpanel/routers/user/viewmodel.py b/core/scripts/webpanel/routers/user/viewmodel.py index 121410b..eed9cee 100644 --- a/core/scripts/webpanel/routers/user/viewmodel.py +++ b/core/scripts/webpanel/routers/user/viewmodel.py @@ -20,30 +20,49 @@ class User(BaseModel): @staticmethod def __parse_user_data(user_data: dict) -> dict: - expiration_days = user_data.get('expiration_days', 0) + essential_keys = [ + 'password', + 'max_download_bytes', + 'expiration_days', + 'blocked', + 'unlimited_user' + ] - if expiration_days > 0: - creation_date_str = user_data.get("account_creation_date") - display_expiry_days = str(expiration_days) - - if isinstance(creation_date_str, str): - try: - creation_date = datetime.strptime(creation_date_str, "%Y-%m-%d") - expiry_dt_obj = creation_date + timedelta(days=expiration_days) - display_expiry_date = expiry_dt_obj.strftime("%Y-%m-%d") - except ValueError: - display_expiry_date = "Error" - else: - display_expiry_date = "Error" - else: + if not all(key in user_data for key in essential_keys): + return { + 'username': user_data.get('username', 'Unknown'), + 'status': 'Conflict', + 'quota': 'N/A', + 'traffic_used': 'N/A', + 'expiry_date': 'N/A', + 'expiry_days': 'N/A', + 'enable': False, + 'unlimited_ip': False + } + + expiration_days = user_data.get('expiration_days', 0) + creation_date_str = user_data.get("account_creation_date") + + if not creation_date_str: + display_expiry_days = "On-hold" + display_expiry_date = "On-hold" + elif expiration_days <= 0: display_expiry_days = "Unlimited" display_expiry_date = "Unlimited" + else: + display_expiry_days = str(expiration_days) + try: + creation_date = datetime.strptime(creation_date_str, "%Y-%m-%d") + expiry_dt_obj = creation_date + timedelta(days=expiration_days) + display_expiry_date = expiry_dt_obj.strftime("%Y-%m-%d") + except (ValueError, TypeError): + display_expiry_date = "Error" used_bytes = user_data.get("download_bytes", 0) + user_data.get("upload_bytes", 0) quota_bytes = user_data.get('max_download_bytes', 0) used_formatted = User.__format_traffic(used_bytes) - quota_formatted = "Unlimited" if quota_bytes == 0 else User.__format_traffic(quota_bytes) + quota_formatted = "Unlimited" if quota_bytes <= 0 else User.__format_traffic(quota_bytes) percentage = 0 if quota_bytes > 0: @@ -64,7 +83,7 @@ class User(BaseModel): @staticmethod def __format_traffic(traffic_bytes) -> str: - if traffic_bytes == 0: + if traffic_bytes <= 0: return "0 B" if traffic_bytes < 1024: return f'{traffic_bytes} B' diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index a22154f..873e1c8 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -26,8 +26,8 @@