Merge pull request #258 from ReturnFI/hold
Bulk User Export, On-Hold Logic & Performance Optimizations
This commit is contained in:
28
changelog
28
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**
|
* New API endpoint `/api/v1/users/uri/bulk` to fetch multiple user links in a single call
|
||||||
* ⚡ Removed old command-based auth system
|
* 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**
|
#### 🐛 Fixes & Refactors
|
||||||
* 📡 **API Endpoint**
|
|
||||||
* 💻 **CLI Command**
|
* 🤖 **Bot**: Properly handle escaped underscores in usernames
|
||||||
* 📜 **Automation Script**
|
* 🛠️ **Webpanel**: Improved handling of malformed user data & more accurate status for on-hold users
|
||||||
* 🔍 New **Online User Filter & Sort** on the Users page
|
* 🐛 Show Go installation correctly
|
||||||
* 🐛 Fixed: underscores now supported in usernames
|
* 🔄 Refactored on-hold user logic into `traffic.py` for central management
|
||||||
|
|||||||
@ -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('--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/--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.')
|
@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):
|
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:
|
||||||
|
|||||||
@ -18,7 +18,7 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date
|
|||||||
traffic_gb (str): The traffic limit in GB.
|
traffic_gb (str): The traffic limit in GB.
|
||||||
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. Defaults to None.
|
||||||
unlimited_user (bool, optional): If True, user is exempt from IP limits. Defaults to False.
|
unlimited_user (bool, optional): If True, user is exempt from IP limits. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
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.")
|
print("Error: Failed to generate password. Please install 'pwgen' or ensure /proc access.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if not creation_date:
|
if creation_date:
|
||||||
creation_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
else:
|
|
||||||
if not re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", 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.")
|
print("Invalid date format. Expected YYYY-MM-DD.")
|
||||||
return 1
|
return 1
|
||||||
@ -59,6 +57,8 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
print("Invalid date. Please provide a valid date in YYYY-MM-DD format.")
|
print("Invalid date. Please provide a valid date in YYYY-MM-DD format.")
|
||||||
return 1
|
return 1
|
||||||
|
else:
|
||||||
|
creation_date = None
|
||||||
|
|
||||||
if not re.match(r"^[a-zA-Z0-9_]+$", username):
|
if not re.match(r"^[a-zA-Z0-9_]+$", username):
|
||||||
print("Error: Username can only contain letters, numbers, and underscores.")
|
print("Error: Username can only contain letters, numbers, and underscores.")
|
||||||
|
|||||||
@ -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}
|
existing_users_lower = {u.lower() for u in users_data}
|
||||||
new_users_to_add = {}
|
new_users_to_add = {}
|
||||||
creation_date = datetime.now().strftime("%Y-%m-%d")
|
creation_date = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
password_process = subprocess.run(['pwgen', '-s', '32', str(count)], capture_output=True, text=True, check=True)
|
password_process = subprocess.run(['pwgen', '-s', '32', str(count)], capture_output=True, text=True, check=True)
|
||||||
|
|||||||
@ -1,65 +1,128 @@
|
|||||||
import subprocess
|
#!/usr/bin/env python3
|
||||||
import concurrent.futures
|
|
||||||
import re
|
import os
|
||||||
import json
|
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
from init_paths import *
|
from init_paths import *
|
||||||
from paths import *
|
from paths import *
|
||||||
|
|
||||||
DEFAULT_ARGS = ["-a", "-n", "-s"]
|
@lru_cache(maxsize=None)
|
||||||
|
def load_json_file(file_path: str) -> Any:
|
||||||
def run_show_uri(username):
|
if not os.path.exists(file_path):
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
cmd = ["python3", CLI_PATH, "show-user-uri", "-u", username] + DEFAULT_ARGS
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
content = f.read()
|
||||||
output = result.stdout
|
return json.loads(content) if content else None
|
||||||
if "Invalid username" in output:
|
except (json.JSONDecodeError, IOError):
|
||||||
return {"username": username, "error": "User not found"}
|
return None
|
||||||
return parse_output(username, output)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return {"username": username, "error": e.stderr.strip()}
|
|
||||||
|
|
||||||
def parse_output(username, output):
|
@lru_cache(maxsize=None)
|
||||||
ipv4 = None
|
def load_env_file(env_file: str) -> Dict[str, str]:
|
||||||
ipv6 = None
|
env_vars = {}
|
||||||
normal_sub = None
|
if os.path.exists(env_file):
|
||||||
nodes = []
|
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)
|
def generate_uri(username: str, auth_password: str, ip: str, port: str,
|
||||||
ipv6_match = re.search(r"IPv6:\s*(hy2://[^\s]+)", output)
|
uri_params: Dict[str, str], ip_version: int, fragment_tag: str) -> str:
|
||||||
normal_sub_match = re.search(r"Normal-SUB Sublink:\s*(https?://[^\s]+)", output)
|
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:
|
def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]:
|
||||||
ipv4 = ipv4_match.group(1)
|
config = load_json_file(CONFIG_FILE)
|
||||||
if ipv6_match:
|
all_users = load_json_file(USERS_FILE)
|
||||||
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)
|
if not config or not all_users:
|
||||||
for name, uri in node_matches:
|
print("Error: Could not load Hysteria2 configuration or user files.", file=sys.stderr)
|
||||||
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 ...")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
usernames = sys.argv[1:]
|
nodes = load_json_file(NODES_JSON_PATH) or []
|
||||||
output_list = batch_show_uri(usernames)
|
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))
|
print(json.dumps(output_list, indent=2))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,12 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import schedule
|
import schedule
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import fcntl
|
import fcntl
|
||||||
import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from paths import *
|
from paths import *
|
||||||
|
|
||||||
@ -23,7 +20,6 @@ logger = logging.getLogger("HysteriaScheduler")
|
|||||||
# Constants
|
# Constants
|
||||||
BASE_DIR = Path("/etc/hysteria")
|
BASE_DIR = Path("/etc/hysteria")
|
||||||
VENV_ACTIVATE = BASE_DIR / "hysteria2_venv/bin/activate"
|
VENV_ACTIVATE = BASE_DIR / "hysteria2_venv/bin/activate"
|
||||||
# CLI_PATH = BASE_DIR / "core/cli.py"
|
|
||||||
LOCK_FILE = "/tmp/hysteria_scheduler.lock"
|
LOCK_FILE = "/tmp/hysteria_scheduler.lock"
|
||||||
|
|
||||||
def acquire_lock():
|
def acquire_lock():
|
||||||
@ -41,7 +37,6 @@ def release_lock(lock_fd):
|
|||||||
lock_fd.close()
|
lock_fd.close()
|
||||||
|
|
||||||
def run_command(command, log_success=False):
|
def run_command(command, log_success=False):
|
||||||
|
|
||||||
activate_cmd = f"source {VENV_ACTIVATE}"
|
activate_cmd = f"source {VENV_ACTIVATE}"
|
||||||
full_cmd = f"{activate_cmd} && {command}"
|
full_cmd = f"{activate_cmd} && {command}"
|
||||||
|
|
||||||
@ -94,6 +89,7 @@ def main():
|
|||||||
schedule.every(1).minutes.do(check_traffic_status)
|
schedule.every(1).minutes.do(check_traffic_status)
|
||||||
schedule.every(6).hours.do(backup_hysteria)
|
schedule.every(6).hours.do(backup_hysteria)
|
||||||
|
|
||||||
|
check_traffic_status()
|
||||||
backup_hysteria()
|
backup_hysteria()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import qrcode
|
import qrcode
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from telebot import types
|
from telebot import types
|
||||||
from utils.command import *
|
from utils.command import *
|
||||||
from utils.common import create_main_markup
|
from utils.common import create_main_markup
|
||||||
|
|
||||||
|
def escape_markdown(text):
|
||||||
|
return str(text).replace('_', '\\_').replace('*', '\\*').replace('`', '\\`')
|
||||||
|
|
||||||
def create_cancel_markup(back_step=None):
|
def create_cancel_markup(back_step=None):
|
||||||
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
|
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')
|
@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'Add User')
|
||||||
def add_user(message):
|
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)
|
bot.register_next_step_handler(msg, process_add_user_step1)
|
||||||
|
|
||||||
def process_add_user_step1(message):
|
def process_add_user_step1(message):
|
||||||
@ -24,6 +27,12 @@ def process_add_user_step1(message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
username = message.text.strip()
|
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:
|
if not username:
|
||||||
bot.reply_to(message, "Username cannot be empty. Please enter a valid username:", reply_markup=create_cancel_markup())
|
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)
|
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)
|
users_data = json.loads(result)
|
||||||
existing_users = {user_key.lower() for user_key in users_data.keys()}
|
existing_users = {user_key.lower() for user_key in users_data.keys()}
|
||||||
if username.lower() in existing_users:
|
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)
|
bot.register_next_step_handler(message, process_add_user_step1)
|
||||||
return
|
return
|
||||||
except json.JSONDecodeError:
|
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())
|
bot.reply_to(message, "Process canceled.", reply_markup=create_main_markup())
|
||||||
return
|
return
|
||||||
if message.text == "⬅️ Back":
|
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)
|
bot.register_next_step_handler(msg, process_add_user_step1)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -96,8 +105,7 @@ def process_add_user_step3(message, username, traffic_limit):
|
|||||||
|
|
||||||
bot.send_chat_action(message.chat.id, 'typing')
|
bot.send_chat_action(message.chat.id, 'typing')
|
||||||
|
|
||||||
lower_username = username.lower()
|
uri_info_command = f"python3 {CLI_PATH} show-user-uri -u \"{username}\" -ip 4 -n"
|
||||||
uri_info_command = f"python3 {CLI_PATH} show-user-uri -u \"{lower_username}\" -ip 4 -n"
|
|
||||||
uri_info_output = run_cli_command(uri_info_command)
|
uri_info_output = run_cli_command(uri_info_command)
|
||||||
|
|
||||||
direct_uri = None
|
direct_uri = None
|
||||||
@ -121,18 +129,20 @@ def process_add_user_step3(message, username, traffic_limit):
|
|||||||
except (IndexError, AttributeError):
|
except (IndexError, AttributeError):
|
||||||
pass
|
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_to_generate_qr_for = None
|
||||||
link_type_for_caption = ""
|
link_type_for_caption = ""
|
||||||
|
|
||||||
if normal_sub_link:
|
if normal_sub_link:
|
||||||
link_to_generate_qr_for = normal_sub_link
|
link_to_generate_qr_for = normal_sub_link
|
||||||
link_type_for_caption = "Normal Subscription 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:
|
elif direct_uri:
|
||||||
link_to_generate_qr_for = direct_uri
|
link_to_generate_qr_for = direct_uri
|
||||||
link_type_for_caption = "Hysteria2 IPv4 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:
|
if link_to_generate_qr_for:
|
||||||
qr_img = qrcode.make(link_to_generate_qr_for)
|
qr_img = qrcode.make(link_to_generate_qr_for)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
#show and edituser file
|
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@ -8,6 +6,9 @@ from utils.command import *
|
|||||||
from utils.common 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")
|
@bot.callback_query_handler(func=lambda call: call.data == "cancel_show_user")
|
||||||
def handle_cancel_show_user(call):
|
def handle_cancel_show_user(call):
|
||||||
bot.edit_message_text("Operation canceled.", chat_id=call.message.chat.id, message_id=call.message.message_id)
|
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()}
|
existing_users = {user.lower(): user for user in users.keys()}
|
||||||
|
|
||||||
if username not in existing_users:
|
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
|
return
|
||||||
|
|
||||||
actual_username = existing_users[username]
|
actual_username = existing_users[username]
|
||||||
@ -54,8 +55,8 @@ def process_show_user(message):
|
|||||||
if upload_bytes is None or download_bytes is None:
|
if upload_bytes is None or download_bytes is None:
|
||||||
traffic_message = "**Traffic Data:**\nUser not active or no traffic data available."
|
traffic_message = "**Traffic Data:**\nUser not active or no traffic data available."
|
||||||
else:
|
else:
|
||||||
upload_gb = upload_bytes / (1024 ** 3) # Convert bytes to GB
|
upload_gb = upload_bytes / (1024 ** 3)
|
||||||
download_gb = download_bytes / (1024 ** 3) # Convert bytes to GB
|
download_gb = download_bytes / (1024 ** 3)
|
||||||
totalusage = upload_gb + download_gb
|
totalusage = upload_gb + download_gb
|
||||||
|
|
||||||
traffic_message = (
|
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.")
|
bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
display_username = escape_markdown(actual_username)
|
||||||
|
|
||||||
formatted_details = (
|
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"📊 Traffic Limit: {user_details['max_download_bytes'] / (1024 ** 3):.2f} GB\n"
|
||||||
f"📅 Days: {user_details['expiration_days']}\n"
|
f"📅 Days: {user_details['expiration_days']}\n"
|
||||||
f"⏳ Creation: {user_details['account_creation_date']}\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')
|
result_lines = combined_result.strip().split('\n')
|
||||||
|
|
||||||
uri_v4 = ""
|
uri_v4 = ""
|
||||||
# singbox_sublink = ""
|
|
||||||
normal_sub_sublink = ""
|
normal_sub_sublink = ""
|
||||||
|
|
||||||
for line in result_lines:
|
for line in result_lines:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line.startswith("hy2://"):
|
if line.startswith("hy2://"):
|
||||||
uri_v4 = line
|
uri_v4 = line
|
||||||
# elif line.startswith("Singbox Sublink:"):
|
|
||||||
# singbox_sublink = result_lines[result_lines.index(line) + 1].strip()
|
|
||||||
elif line.startswith("Normal-SUB Sublink:"):
|
elif line.startswith("Normal-SUB Sublink:"):
|
||||||
normal_sub_sublink = result_lines[result_lines.index(line) + 1].strip()
|
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}"))
|
types.InlineKeyboardButton("Block User", callback_data=f"block_user:{actual_username}"))
|
||||||
|
|
||||||
caption = f"{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`"
|
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:
|
if normal_sub_sublink:
|
||||||
caption += f"\n\n**Normal SUB:**\n{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_'))
|
@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):
|
def handle_edit_callback(call):
|
||||||
action, username = call.data.split(':')
|
action, username = call.data.split(':')
|
||||||
|
display_username = escape_markdown(username)
|
||||||
|
|
||||||
if action == 'edit_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)
|
bot.register_next_step_handler(msg, process_edit_username, username)
|
||||||
elif action == 'edit_traffic':
|
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)
|
bot.register_next_step_handler(msg, process_edit_traffic, username)
|
||||||
elif action == 'edit_expiration':
|
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)
|
bot.register_next_step_handler(msg, process_edit_expiration, username)
|
||||||
elif action == 'renew_password':
|
elif action == 'renew_password':
|
||||||
command = f"python3 {CLI_PATH} edit-user -u {username} -rp"
|
command = f"python3 {CLI_PATH} edit-user -u {username} -rp"
|
||||||
@ -156,7 +156,7 @@ def handle_edit_callback(call):
|
|||||||
markup = types.InlineKeyboardMarkup()
|
markup = types.InlineKeyboardMarkup()
|
||||||
markup.add(types.InlineKeyboardButton("True", callback_data=f"confirm_block:{username}:true"),
|
markup.add(types.InlineKeyboardButton("True", callback_data=f"confirm_block:{username}:true"),
|
||||||
types.InlineKeyboardButton("False", callback_data=f"confirm_block:{username}:false"))
|
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':
|
elif action == 'reset_user':
|
||||||
command = f"python3 {CLI_PATH} reset-user -u {username}"
|
command = f"python3 {CLI_PATH} reset-user -u {username}"
|
||||||
result = run_cli_command(command)
|
result = run_cli_command(command)
|
||||||
@ -177,7 +177,7 @@ def handle_edit_callback(call):
|
|||||||
bot.send_photo(
|
bot.send_photo(
|
||||||
call.message.chat.id,
|
call.message.chat.id,
|
||||||
bio_v6,
|
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"
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
bot.answer_inline_query(query.id, results=[], switch_pm_text="Error retrieving users.", switch_pm_user_id=query.from_user.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
query_text = query.query.lower()
|
query_text = query.query.lower().replace('\\_', '_')
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
if query_text == "block":
|
if query_text == "block":
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class UserInfoResponse(BaseModel):
|
|||||||
class UserListResponse(RootModel):
|
class UserListResponse(RootModel):
|
||||||
root: dict[str, UserInfoResponse]
|
root: dict[str, UserInfoResponse]
|
||||||
|
|
||||||
|
class UsernamesRequest(BaseModel):
|
||||||
|
usernames: List[str]
|
||||||
|
|
||||||
class AddUserInputBody(BaseModel):
|
class AddUserInputBody(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import json
|
import json
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from .schema.user import (
|
||||||
from .schema.user import UserListResponse, UserInfoResponse, AddUserInputBody, EditUserInputBody, UserUriResponse, AddBulkUsersInputBody
|
UserListResponse,
|
||||||
|
UserInfoResponse,
|
||||||
|
AddUserInputBody,
|
||||||
|
EditUserInputBody,
|
||||||
|
UserUriResponse,
|
||||||
|
AddBulkUsersInputBody,
|
||||||
|
UsernamesRequest
|
||||||
|
)
|
||||||
from .schema.response import DetailResponse
|
from .schema.response import DetailResponse
|
||||||
import cli_api
|
import cli_api
|
||||||
|
|
||||||
@ -79,6 +87,29 @@ async def add_bulk_users_api(body: AddBulkUsersInputBody):
|
|||||||
raise HTTPException(status_code=500,
|
raise HTTPException(status_code=500,
|
||||||
detail=f"An unexpected error occurred while adding bulk users: {str(e)}")
|
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)
|
@router.get('/{username}', response_model=UserInfoResponse)
|
||||||
async def get_user_api(username: str):
|
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.kick_user_by_name(username)
|
||||||
cli_api.traffic_status(display_output=False)
|
cli_api.traffic_status(display_output=False)
|
||||||
cli_api.remove_user(username)
|
cli_api.remove_user(username)
|
||||||
|
cli_api.traffic_status(display_output=False)
|
||||||
return DetailResponse(detail=f'User {username} has been removed.')
|
return DetailResponse(detail=f'User {username} has been removed.')
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
||||||
|
|||||||
@ -20,30 +20,49 @@ class User(BaseModel):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __parse_user_data(user_data: dict) -> dict:
|
def __parse_user_data(user_data: dict) -> dict:
|
||||||
|
essential_keys = [
|
||||||
|
'password',
|
||||||
|
'max_download_bytes',
|
||||||
|
'expiration_days',
|
||||||
|
'blocked',
|
||||||
|
'unlimited_user'
|
||||||
|
]
|
||||||
|
|
||||||
|
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)
|
expiration_days = user_data.get('expiration_days', 0)
|
||||||
|
creation_date_str = user_data.get("account_creation_date")
|
||||||
|
|
||||||
if expiration_days > 0:
|
if not creation_date_str:
|
||||||
creation_date_str = user_data.get("account_creation_date")
|
display_expiry_days = "On-hold"
|
||||||
display_expiry_days = str(expiration_days)
|
display_expiry_date = "On-hold"
|
||||||
|
elif expiration_days <= 0:
|
||||||
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:
|
|
||||||
display_expiry_days = "Unlimited"
|
display_expiry_days = "Unlimited"
|
||||||
display_expiry_date = "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)
|
used_bytes = user_data.get("download_bytes", 0) + user_data.get("upload_bytes", 0)
|
||||||
quota_bytes = user_data.get('max_download_bytes', 0)
|
quota_bytes = user_data.get('max_download_bytes', 0)
|
||||||
|
|
||||||
used_formatted = User.__format_traffic(used_bytes)
|
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
|
percentage = 0
|
||||||
if quota_bytes > 0:
|
if quota_bytes > 0:
|
||||||
@ -64,7 +83,7 @@ class User(BaseModel):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __format_traffic(traffic_bytes) -> str:
|
def __format_traffic(traffic_bytes) -> str:
|
||||||
if traffic_bytes == 0:
|
if traffic_bytes <= 0:
|
||||||
return "0 B"
|
return "0 B"
|
||||||
if traffic_bytes < 1024:
|
if traffic_bytes < 1024:
|
||||||
return f'{traffic_bytes} B'
|
return f'{traffic_bytes} B'
|
||||||
|
|||||||
@ -26,8 +26,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-2 mb-2">
|
<div class="mr-2 mb-2">
|
||||||
<button type="button" class="btn btn-sm btn-default filter-button" data-filter="not-active">
|
<button type="button" class="btn btn-sm btn-warning filter-button" data-filter="on-hold">
|
||||||
<i class="fas fa-exclamation-triangle"></i> NA
|
<i class="fas fa-pause-circle"></i> Hold
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-2 mb-2">
|
<div class="mr-2 mb-2">
|
||||||
@ -57,14 +57,17 @@
|
|||||||
<button type="button" class="btn btn-sm btn-primary ml-2" data-toggle="modal" data-target="#addUserModal">
|
<button type="button" class="btn btn-sm btn-primary ml-2" data-toggle="modal" data-target="#addUserModal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-info ml-2" id="showSelectedLinks">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-danger ml-2" id="deleteSelected">
|
<button type="button" class="btn btn-sm btn-danger ml-2" id="deleteSelected">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body table-responsive p-0">
|
<div class="card-body table-responsive p-0">
|
||||||
{% if users|length == 0 %}
|
{% if not users %}
|
||||||
<div class="alert alert-warning" role="alert" style="margin: 20px;">
|
<div class="alert alert-warning m-3" role="alert">
|
||||||
No users found.
|
No users found.
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -98,6 +101,10 @@
|
|||||||
<i class="fas fa-circle text-success"></i> Online
|
<i class="fas fa-circle text-success"></i> Online
|
||||||
{% elif user['status'] == "Offline" %}
|
{% elif user['status'] == "Offline" %}
|
||||||
<i class="fas fa-circle text-secondary"></i> Offline
|
<i class="fas fa-circle text-secondary"></i> Offline
|
||||||
|
{% elif user['status'] == "On-hold" %}
|
||||||
|
<i class="fas fa-circle text-warning"></i> On-hold
|
||||||
|
{% elif user['status'] == "Conflict" %}
|
||||||
|
<i class="fas fa-circle text-danger"></i> Conflict
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fas fa-circle text-danger"></i> {{ user['status'] }}
|
<i class="fas fa-circle text-danger"></i> {{ user['status'] }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -146,13 +153,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-sm-12 col-md-7">
|
|
||||||
<div class="dataTables_paginate paging_simple_numbers" id="userTable_paginate">
|
|
||||||
{# {{ pagination.links }} #}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,7 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
||||||
<input type="checkbox" class="form-check-input" id="addUnlimited" name="unlimited">
|
<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>
|
<label class="form-check-label" for="addUnlimited">Unlimited IP</label>
|
||||||
</div>
|
</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>
|
||||||
@ -230,7 +230,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
||||||
<input type="checkbox" class="form-check-input" id="addBulkUnlimited" name="unlimited">
|
<input type="checkbox" class="form-check-input" id="addBulkUnlimited" name="unlimited">
|
||||||
<label class="form-check-label" for="addBulkUnlimited">Unlimited IP (Exempt from IP limit checks)</label>
|
<label class="form-check-label" for="addBulkUnlimited">Unlimited IP</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary" id="addBulkSubmitButton">Add Bulk Users</button>
|
<button type="submit" class="btn btn-primary" id="addBulkSubmitButton">Add Bulk Users</button>
|
||||||
</form>
|
</form>
|
||||||
@ -242,8 +242,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit User Modal -->
|
<!-- Edit User Modal -->
|
||||||
<div class="modal fade" id="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel"
|
<div class="modal fade" id="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
||||||
aria-hidden="true">
|
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -273,7 +272,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
<div class="form-check mb-3 requires-iplimit-service" style="display: none;">
|
||||||
<input type="checkbox" class="form-check-input" id="editUnlimitedIp" name="unlimited_ip">
|
<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>
|
<label class="form-check-label" for="editUnlimitedIp">Unlimited IP</label>
|
||||||
</div>
|
</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>
|
||||||
@ -283,8 +282,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- QR Code Modal -->
|
<!-- QR Code Modal -->
|
||||||
<div class="modal fade" id="qrcodeModal" tabindex="-1" role="dialog" aria-labelledby="qrcodeModalLabel"
|
<div class="modal fade" id="qrcodeModal" tabindex="-1" role="dialog" aria-labelledby="qrcodeModalLabel" aria-hidden="true">
|
||||||
aria-hidden="true">
|
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -299,619 +297,265 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Show Links Modal -->
|
||||||
|
<div class="modal fade" id="showLinksModal" tabindex="-1" role="dialog" aria-labelledby="showLinksModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="showLinksModalLabel">Selected User Links</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Here are the Normal-SUB links for the selected users:</p>
|
||||||
|
<textarea id="linksTextarea" class="form-control" rows="10" readonly></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="copyLinksButton">Copy All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
<!-- Include qr-code-styling library -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qr-code-styling/lib/qr-code-styling.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qr-code-styling/lib/qr-code-styling.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
|
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
||||||
|
|
||||||
function checkIpLimitServiceStatus() {
|
function checkIpLimitServiceStatus() {
|
||||||
fetch('{{ url_for("server_services_status_api") }}')
|
$.getJSON('{{ url_for("server_services_status_api") }}')
|
||||||
.then(response => response.json())
|
.done(data => {
|
||||||
.then(data => {
|
|
||||||
if (data.hysteria_iplimit === true) {
|
if (data.hysteria_iplimit === true) {
|
||||||
$('.requires-iplimit-service').show();
|
$('.requires-iplimit-service').show();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching IP limit service status:', error));
|
.fail(() => console.error('Error fetching IP limit service status.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIpLimitServiceStatus();
|
function validateUsername(inputElement, errorElement) {
|
||||||
|
const username = $(inputElement).val();
|
||||||
const usernameRegex = /^[a-zA-Z0-9_]+$/;
|
|
||||||
|
|
||||||
function validateUsername(username, errorElementId) {
|
|
||||||
const errorElement = $("#" + errorElementId);
|
|
||||||
if (!username) {
|
|
||||||
errorElement.text("");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const isValid = usernameRegex.test(username);
|
const isValid = usernameRegex.test(username);
|
||||||
|
$(errorElement).text(isValid ? "" : "Usernames can only contain letters, numbers, and underscores.");
|
||||||
if (!isValid) {
|
$(inputElement).closest('form').find('button[type="submit"]').prop('disabled', !isValid);
|
||||||
errorElement.text("Usernames can only contain letters, numbers, and underscores.");
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
errorElement.text("");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#addSubmitButton").prop("disabled", true);
|
$('#addUsername, #addBulkPrefix, #editUsername').on('input', function() {
|
||||||
|
validateUsername(this, `#${this.id}Error`);
|
||||||
$("#addUsername").on("input", function () {
|
|
||||||
const username = $(this).val();
|
|
||||||
const isValid = validateUsername(username, "addUsernameError");
|
|
||||||
$("#addSubmitButton").prop("disabled", !isValid);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#addBulkPrefix").on("input", function () {
|
|
||||||
const prefix = $(this).val();
|
|
||||||
const isValid = validateUsername(prefix, "addBulkPrefixError");
|
|
||||||
$("#addBulkSubmitButton").prop("disabled", !isValid);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#editUsername").on("input", function () {
|
|
||||||
const username = $(this).val();
|
|
||||||
const isValid = validateUsername(username, "editUsernameError");
|
|
||||||
$("#editSubmitButton").prop("disabled", !isValid);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".filter-button").on("click", function () {
|
$(".filter-button").on("click", function () {
|
||||||
const filter = $(this).data("filter");
|
const filter = $(this).data("filter");
|
||||||
|
|
||||||
$("#selectAll").prop("checked", false);
|
$("#selectAll").prop("checked", false);
|
||||||
|
|
||||||
$("#userTable tbody tr").each(function () {
|
$("#userTable tbody tr").each(function () {
|
||||||
let showRow = true;
|
let showRow;
|
||||||
|
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case "all":
|
case "on-hold": showRow = $(this).find("td:eq(2) i").hasClass("text-warning"); break;
|
||||||
showRow = true;
|
case "online": showRow = $(this).find("td:eq(2) i").hasClass("text-success"); break;
|
||||||
break;
|
case "enable": showRow = $(this).find("td:eq(7) i").hasClass("text-success"); break;
|
||||||
case "not-active":
|
case "disable": showRow = $(this).find("td:eq(7) i").hasClass("text-danger"); break;
|
||||||
showRow = $(this).find("td:eq(2) i").hasClass("text-danger");
|
default: showRow = true;
|
||||||
break;
|
|
||||||
case "online":
|
|
||||||
showRow = $(this).find("td:eq(2) i").hasClass("text-success");
|
|
||||||
break;
|
|
||||||
case "enable":
|
|
||||||
showRow = $(this).find("td:eq(7) i").hasClass("text-success");
|
|
||||||
break;
|
|
||||||
case "disable":
|
|
||||||
showRow = $(this).find("td:eq(7) i").hasClass("text-danger");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
$(this).toggle(showRow).find(".user-checkbox").prop("checked", false);
|
||||||
if (showRow) {
|
|
||||||
$(this).show();
|
|
||||||
} else {
|
|
||||||
$(this).hide();
|
|
||||||
}
|
|
||||||
$(this).find(".user-checkbox").prop("checked", false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#selectAll").on("change", function () {
|
$("#selectAll").on("change", function () {
|
||||||
$("#userTable tbody tr:visible .user-checkbox").prop("checked", $(this).prop("checked"));
|
$("#userTable tbody tr:visible .user-checkbox").prop("checked", this.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#deleteSelected").on("click", function () {
|
$("#deleteSelected").on("click", function () {
|
||||||
const selectedUsers = $(".user-checkbox:checked").map(function () {
|
const selectedUsers = $(".user-checkbox:checked").map((_, el) => $(el).val()).get();
|
||||||
return $(this).val();
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
if (selectedUsers.length === 0) {
|
if (selectedUsers.length === 0) {
|
||||||
Swal.fire({
|
return Swal.fire("Warning!", "Please select at least one user to delete.", "warning");
|
||||||
title: "Warning!",
|
|
||||||
text: "Please select at least one user to delete.",
|
|
||||||
icon: "warning",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Are you sure?",
|
title: "Are you sure?",
|
||||||
html: `This will delete the selected users: <b>${selectedUsers.join(", ")}</b>.<br>This action cannot be undone!`,
|
html: `This will delete: <b>${selectedUsers.join(", ")}</b>.<br>This action cannot be undone!`,
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: "#3085d6",
|
confirmButtonColor: "#d33",
|
||||||
cancelButtonColor: "#d33",
|
|
||||||
confirmButtonText: "Yes, delete them!",
|
confirmButtonText: "Yes, delete them!",
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (!result.isConfirmed) return;
|
||||||
Promise.all(selectedUsers.map(username => {
|
const urlTemplate = "{{ url_for('remove_user_api', username='U') }}";
|
||||||
const removeUserUrl = "{{ url_for('remove_user_api', username='USERNAME_PLACEHOLDER') }}";
|
const promises = selectedUsers.map(user => $.ajax({ url: urlTemplate.replace('U', user), method: "DELETE" }));
|
||||||
const url = removeUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
Promise.all(promises)
|
||||||
return $.ajax({
|
.then(() => Swal.fire("Success!", "Selected users deleted.", "success").then(() => location.reload()))
|
||||||
url: url,
|
.catch(() => Swal.fire("Error!", "An error occurred while deleting users.", "error"));
|
||||||
method: "DELETE",
|
|
||||||
contentType: "application/json",
|
|
||||||
data: JSON.stringify({ username: username }),
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
.then(responses => {
|
|
||||||
const allSuccessful = responses.every(response => response.detail);
|
|
||||||
if (allSuccessful) {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Success!",
|
|
||||||
text: "Selected users deleted successfully!",
|
|
||||||
icon: "success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: "Failed to delete some users.",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error("Error deleting users:", error);
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: "An error occurred while deleting users.",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#addUserForm").on("submit", function (e) {
|
$("#addUserForm, #addBulkUsersForm").on("submit", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!validateUsername($("#addUsername").val(), "addUsernameError")) {
|
const form = $(this);
|
||||||
$("#addSubmitButton").prop("disabled", true);
|
const isBulk = form.attr('id') === 'addBulkUsersForm';
|
||||||
return;
|
const url = isBulk ? "{{ url_for('add_bulk_users_api') }}" : "{{ url_for('add_user_api') }}";
|
||||||
}
|
const button = form.find('button[type="submit"]').prop('disabled', true);
|
||||||
$("#addSubmitButton").prop("disabled", true);
|
|
||||||
|
|
||||||
const jsonData = {
|
const formData = new FormData(this);
|
||||||
username: $("#addUsername").val(),
|
const jsonData = Object.fromEntries(formData.entries());
|
||||||
traffic_limit: $("#addTrafficLimit").val(),
|
|
||||||
expiration_days: $("#addExpirationDays").val(),
|
jsonData.unlimited = jsonData.unlimited === 'on';
|
||||||
unlimited: $("#addUnlimited").is(":checked")
|
|
||||||
};
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: " {{ url_for('add_user_api') }} ",
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
data: JSON.stringify(jsonData),
|
data: JSON.stringify(jsonData),
|
||||||
success: function (response) {
|
})
|
||||||
Swal.fire({
|
.done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload()))
|
||||||
title: "Success!",
|
.fail(err => Swal.fire("Error!", err.responseJSON?.detail || "An error occurred.", "error"))
|
||||||
text: response.detail || "User added successfully!",
|
.always(() => button.prop('disabled', false));
|
||||||
icon: "success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
|
||||||
let errorMessage = "An error occurred while adding user.";
|
|
||||||
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
|
|
||||||
errorMessage = jqXHR.responseJSON.detail;
|
|
||||||
} else if (jqXHR.status === 409) {
|
|
||||||
errorMessage = "User '" + jsonData.username + "' already exists.";
|
|
||||||
$("#addUsernameError").text(errorMessage);
|
|
||||||
} else if (jqXHR.status === 422) {
|
|
||||||
errorMessage = jqXHR.responseJSON.detail || "Invalid input provided.";
|
|
||||||
}
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: errorMessage,
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
$("#addSubmitButton").prop("disabled", false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#addBulkUsersForm").on("submit", function(e) {
|
$("#editUserModal").on("show.bs.modal", function (event) {
|
||||||
e.preventDefault();
|
const user = $(event.relatedTarget).data("user");
|
||||||
if (!validateUsername($("#addBulkPrefix").val(), "addBulkPrefixError")) {
|
const row = $(event.relatedTarget).closest("tr");
|
||||||
$("#addBulkSubmitButton").prop("disabled", true);
|
const trafficText = row.find("td:eq(4)").text();
|
||||||
return;
|
const expiryText = row.find("td:eq(6)").text();
|
||||||
}
|
|
||||||
$("#addBulkSubmitButton").prop("disabled", true);
|
|
||||||
|
|
||||||
const jsonData = {
|
$("#originalUsername").val(user);
|
||||||
prefix: $("#addBulkPrefix").val(),
|
$("#editUsername").val(user);
|
||||||
count: parseInt($("#addBulkCount").val()),
|
$("#editTrafficLimit").val(parseFloat(trafficText.split('/')[1]) || 0);
|
||||||
start_number: parseInt($("#addBulkStartNumber").val()),
|
$("#editExpirationDays").val(parseInt(expiryText) || 0);
|
||||||
traffic_gb: parseFloat($("#addBulkTrafficLimit").val()),
|
$("#editBlocked").prop("checked", !row.find("td:eq(7) i").hasClass("text-success"));
|
||||||
expiration_days: parseInt($("#addBulkExpirationDays").val()),
|
$("#editUnlimitedIp").prop("checked", row.find(".unlimited-ip-cell i").hasClass("text-primary"));
|
||||||
unlimited: $("#addBulkUnlimited").is(":checked")
|
validateUsername('#editUsername', '#editUsernameError');
|
||||||
};
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "{{ url_for('add_bulk_users_api') }}",
|
|
||||||
method: "POST",
|
|
||||||
contentType: "application/json",
|
|
||||||
data: JSON.stringify(jsonData),
|
|
||||||
success: function(response) {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Success!",
|
|
||||||
text: response.detail || "Bulk user creation started successfully!",
|
|
||||||
icon: "success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: function(jqXHR) {
|
|
||||||
let errorMessage = "An error occurred during bulk user creation.";
|
|
||||||
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
|
|
||||||
errorMessage = jqXHR.responseJSON.detail;
|
|
||||||
}
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: errorMessage,
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
$("#addBulkSubmitButton").prop("disabled", false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on("click", ".edit-user", function () {
|
|
||||||
const username = $(this).data("user");
|
|
||||||
const row = $(this).closest("tr");
|
|
||||||
const trafficUsageText = row.find("td:eq(4)").text().trim();
|
|
||||||
const expiryDaysText = row.find("td:eq(6)").text().trim();
|
|
||||||
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);
|
|
||||||
|
|
||||||
let trafficLimitValue = 0;
|
|
||||||
if (!trafficUsageText.toLowerCase().includes('/unlimited')) {
|
|
||||||
const parts = trafficUsageText.split('/');
|
|
||||||
if (parts.length > 1) {
|
|
||||||
const limitPart = parts[1].trim();
|
|
||||||
const match = limitPart.match(/^[\d.]+/);
|
|
||||||
if (match) {
|
|
||||||
trafficLimitValue = parseFloat(match[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#originalUsername").val(username);
|
|
||||||
$("#editUsername").val(username);
|
|
||||||
$("#editTrafficLimit").val(trafficLimitValue);
|
|
||||||
$("#editExpirationDays").val(expiryDaysValue);
|
|
||||||
$("#editBlocked").prop("checked", blocked);
|
|
||||||
$("#editUnlimitedIp").prop("checked", unlimited_ip);
|
|
||||||
|
|
||||||
const isValid = validateUsername(username, "editUsernameError");
|
|
||||||
$("#editUserForm button[type='submit']").prop("disabled", !isValid);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#editUserForm").on("submit", function (e) {
|
$("#editUserForm").on("submit", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!validateUsername($("#editUsername").val(), "editUsernameError")) {
|
const button = $("#editSubmitButton").prop("disabled", true);
|
||||||
return;
|
const originalUsername = $("#originalUsername").val();
|
||||||
}
|
const url = "{{ url_for('edit_user_api', username='U') }}".replace('U', originalUsername);
|
||||||
$("#editSubmitButton").prop("disabled", true);
|
|
||||||
|
|
||||||
const jsonData = {
|
const formData = new FormData(this);
|
||||||
new_username: $("#editUsername").val(),
|
const jsonData = Object.fromEntries(formData.entries());
|
||||||
new_traffic_limit: $("#editTrafficLimit").val() || null,
|
jsonData.blocked = jsonData.blocked === 'on';
|
||||||
new_expiration_days: $("#editExpirationDays").val() || null,
|
jsonData.unlimited_ip = jsonData.unlimited_ip === 'on';
|
||||||
blocked: $("#editBlocked").is(":checked"),
|
if (jsonData.new_username === originalUsername) delete jsonData.new_username;
|
||||||
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 url = editUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent($("#originalUsername").val()));
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
data: JSON.stringify(jsonData),
|
data: JSON.stringify(jsonData),
|
||||||
success: function (response) {
|
})
|
||||||
if (response && response.detail) {
|
.done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload()))
|
||||||
$("#editUserModal").modal("hide");
|
.fail(err => Swal.fire("Error!", err.responseJSON?.detail, "error"))
|
||||||
Swal.fire({
|
.always(() => button.prop('disabled', false));
|
||||||
title: "Success!",
|
|
||||||
text: response.detail,
|
|
||||||
icon: "success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$("#editUserModal").modal("hide");
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: (response && response.error) || "An unknown error occurred.",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
$("#editSubmitButton").prop("disabled", false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function (jqXHR) {
|
|
||||||
let errorMessage = "An error occurred while updating user.";
|
|
||||||
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
|
|
||||||
errorMessage = jqXHR.responseJSON.detail;
|
|
||||||
}
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: errorMessage,
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
$("#editSubmitButton").prop("disabled", false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset User Button Click
|
$("#userTable").on("click", ".reset-user, .delete-user", function () {
|
||||||
$("#userTable").on("click", ".reset-user", function () {
|
const button = $(this);
|
||||||
const username = $(this).data("user");
|
const username = button.data("user");
|
||||||
|
const isDelete = button.hasClass("delete-user");
|
||||||
|
const action = isDelete ? "delete" : "reset";
|
||||||
|
const urlTemplate = isDelete ? "{{ url_for('remove_user_api', username='U') }}" : "{{ url_for('reset_user_api', username='U') }}";
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Are you sure?",
|
title: `Are you sure you want to ${action}?`,
|
||||||
html: `This will reset <b>${username}</b>'s data.<br>This action cannot be undone!`,
|
html: `This will ${action} user <b>${username}</b>.`,
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: "#3085d6",
|
confirmButtonColor: "#d33",
|
||||||
cancelButtonColor: "#d33",
|
confirmButtonText: `Yes, ${action} it!`,
|
||||||
confirmButtonText: "Yes, reset it!",
|
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (!result.isConfirmed) return;
|
||||||
const resetUserUrl = "{{ url_for('reset_user_api', username='USERNAME_PLACEHOLDER') }}";
|
$.ajax({
|
||||||
const url = resetUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
url: urlTemplate.replace("U", encodeURIComponent(username)),
|
||||||
$.ajax({
|
method: isDelete ? "DELETE" : "GET",
|
||||||
url: url,
|
})
|
||||||
method: "GET",
|
.done(res => Swal.fire("Success!", res.detail, "success").then(() => location.reload()))
|
||||||
contentType: "application/json",
|
.fail(() => Swal.fire("Error!", `Failed to ${action} user.`, "error"));
|
||||||
data: JSON.stringify({ username: username }),
|
|
||||||
success: function (response) {
|
|
||||||
if (response.detail) {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Success!",
|
|
||||||
text: response.detail,
|
|
||||||
icon: "success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: response.error || "Failed to reset user",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: "An error occurred while resetting user",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete User Button Click
|
|
||||||
$("#userTable").on("click", ".delete-user", function () {
|
|
||||||
const username = $(this).data("user");
|
|
||||||
|
|
||||||
Swal.fire({
|
|
||||||
title: "Are you sure?",
|
|
||||||
html: `This will delete the user <b>${username}</b>.<br>This action cannot be undone!`,
|
|
||||||
icon: "warning",
|
|
||||||
showCancelButton: true,
|
|
||||||
confirmButtonColor: "#3085d6",
|
|
||||||
cancelButtonColor: "#d33",
|
|
||||||
confirmButtonText: "Yes, delete it!",
|
|
||||||
}).then((result) => {
|
|
||||||
if (result.isConfirmed) {
|
|
||||||
const removeUserUrl = "{{ url_for('remove_user_api', username='USERNAME_PLACEHOLDER') }}";
|
|
||||||
const url = removeUserUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
method: "DELETE",
|
|
||||||
contentType: "application/json",
|
|
||||||
data: JSON.stringify({ username: username }),
|
|
||||||
success: function (response) {
|
|
||||||
if (response.detail) {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Success!",
|
|
||||||
text: response.detail,
|
|
||||||
icon: "success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: response.error || "Failed to delete user",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: "An error occurred while deleting user",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// QR Code Modal
|
|
||||||
$("#qrcodeModal").on("show.bs.modal", function (event) {
|
$("#qrcodeModal").on("show.bs.modal", function (event) {
|
||||||
const button = $(event.relatedTarget);
|
const username = $(event.relatedTarget).data("username");
|
||||||
const username = button.data("username");
|
const qrcodesContainer = $("#qrcodesContainer").empty();
|
||||||
const qrcodesContainer = $("#qrcodesContainer");
|
const url = "{{ url_for('show_user_uri_api', username='U') }}".replace("U", encodeURIComponent(username));
|
||||||
qrcodesContainer.empty();
|
$.getJSON(url, response => {
|
||||||
|
[
|
||||||
|
{ type: "IPv4", link: response.ipv4 },
|
||||||
|
{ type: "IPv6", link: response.ipv6 },
|
||||||
|
{ type: "Normal-SUB", link: response.normal_sub }
|
||||||
|
].forEach(config => {
|
||||||
|
if (!config.link) return;
|
||||||
|
const qrId = `qrcode-${config.type}`;
|
||||||
|
const card = $(`<div class="card d-inline-block m-2"><div class="card-body"><div id="${qrId}" class="mx-auto" style="cursor: pointer;"></div><div class="mt-2 text-center small text-body font-weight-bold">${config.type}</div></div></div>`);
|
||||||
|
qrcodesContainer.append(card);
|
||||||
|
new QRCodeStyling({ width: 200, height: 200, data: config.link, margin: 2 }).append(document.getElementById(qrId));
|
||||||
|
card.on("click", () => navigator.clipboard.writeText(config.link).then(() => Swal.fire({ icon: "success", title: `${config.type} link copied!`, showConfirmButton: false, timer: 1200 })));
|
||||||
|
});
|
||||||
|
}).fail(() => Swal.fire("Error!", "Failed to fetch user configuration.", "error"));
|
||||||
|
});
|
||||||
|
|
||||||
const userUriApiUrl = "{{ url_for('show_user_uri_api', username='USERNAME_PLACEHOLDER') }}";
|
$("#showSelectedLinks").on("click", function () {
|
||||||
const url = userUriApiUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
const selectedUsers = $(".user-checkbox:checked").map((_, el) => $(el).val()).get();
|
||||||
|
if (selectedUsers.length === 0) {
|
||||||
|
return Swal.fire("Warning!", "Please select at least one user.", "warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({ title: 'Fetching links...', text: 'Please wait.', allowOutsideClick: false, didOpen: () => Swal.showLoading() });
|
||||||
|
|
||||||
|
const bulkUriApiUrl = "{{ url_for('show_multiple_user_uris_api') }}";
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: bulkUriApiUrl,
|
||||||
method: "GET",
|
method: 'POST',
|
||||||
dataType: 'json',
|
contentType: 'application/json',
|
||||||
success: function (response) {
|
data: JSON.stringify({ usernames: selectedUsers }),
|
||||||
const configs = [
|
}).done(results => {
|
||||||
{ type: "IPv4", link: response.ipv4 },
|
Swal.close();
|
||||||
{ type: "IPv6", link: response.ipv6 },
|
const allLinks = results.map(res => res.normal_sub).filter(Boolean);
|
||||||
{ type: "Normal-SUB", link: response.normal_sub }
|
const failedCount = selectedUsers.length - allLinks.length;
|
||||||
];
|
if (failedCount > 0) {
|
||||||
|
Swal.fire('Warning', `Could not fetch links for ${failedCount} user(s), but others were successful.`, 'warning');
|
||||||
configs.forEach(config => {
|
|
||||||
if (config.link) {
|
|
||||||
const displayType = config.type;
|
|
||||||
const configLink = config.link;
|
|
||||||
const qrCodeId = `qrcode-${displayType}-${Math.random().toString(36).substring(2, 10)}`;
|
|
||||||
|
|
||||||
const card = $(`
|
|
||||||
<div class="card d-inline-block my-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="${qrCodeId}" class="mx-auto cursor-pointer"></div>
|
|
||||||
<br>
|
|
||||||
<div class="config-type-text mt-2 text-center">${displayType}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
qrcodesContainer.append(card);
|
|
||||||
|
|
||||||
const qrCodeStyling = new QRCodeStyling({
|
|
||||||
width: 180,
|
|
||||||
height: 180,
|
|
||||||
data: configLink,
|
|
||||||
margin: 5,
|
|
||||||
dotsOptions: {
|
|
||||||
color: "#212121",
|
|
||||||
type: "square"
|
|
||||||
},
|
|
||||||
cornersSquareOptions: {
|
|
||||||
color: "#212121",
|
|
||||||
type: "square"
|
|
||||||
},
|
|
||||||
backgroundOptions: {
|
|
||||||
color: "#FAFAFA",
|
|
||||||
},
|
|
||||||
imageOptions: {
|
|
||||||
hideBackgroundDots: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
qrCodeStyling.append(document.getElementById(qrCodeId));
|
|
||||||
|
|
||||||
card.on("click", function () {
|
|
||||||
navigator.clipboard.writeText(configLink)
|
|
||||||
.then(() => {
|
|
||||||
Swal.fire({
|
|
||||||
icon: "success",
|
|
||||||
title: displayType + " link copied!",
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 1500,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Failed to copy link: ", err);
|
|
||||||
Swal.fire({
|
|
||||||
icon: "error",
|
|
||||||
title: "Failed to copy link",
|
|
||||||
text: "Please copy manually.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function (error) {
|
|
||||||
console.error("Error fetching user URI:", error);
|
|
||||||
Swal.fire({
|
|
||||||
title: "Error!",
|
|
||||||
text: "Failed to fetch user configuration URIs.",
|
|
||||||
icon: "error",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
if (allLinks.length > 0) {
|
||||||
|
$("#linksTextarea").val(allLinks.join('\n'));
|
||||||
|
$("#showLinksModal").modal("show");
|
||||||
|
} else {
|
||||||
|
Swal.fire('Error', `Could not fetch links for any of the selected users.`, 'error');
|
||||||
|
}
|
||||||
|
}).fail(() => Swal.fire('Error!', 'An error occurred while fetching the links.', 'error'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#qrcodeModal .modal-content").on("click", function (e) {
|
$("#copyLinksButton").on("click", () => {
|
||||||
e.stopPropagation();
|
navigator.clipboard.writeText($("#linksTextarea").val())
|
||||||
});
|
.then(() => Swal.fire({ icon: "success", title: "All links copied!", showConfirmButton: false, timer: 1200 }));
|
||||||
|
|
||||||
$("#qrcodeModal").on("hidden.bs.modal", function () {
|
|
||||||
$("#qrcodesContainer").empty();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#qrcodeModal .close").on("click", function () {
|
|
||||||
$("#qrcodeModal").modal("hide");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function filterUsers() {
|
function filterUsers() {
|
||||||
const searchText = $("#searchInput").val().toLowerCase();
|
const searchText = $("#searchInput").val().toLowerCase();
|
||||||
|
|
||||||
$("#userTable tbody tr").each(function () {
|
$("#userTable tbody tr").each(function () {
|
||||||
const username = $(this).find("td:eq(3)").text().toLowerCase();
|
const username = $(this).find("td:eq(3)").text().toLowerCase();
|
||||||
if (username.includes(searchText)) {
|
$(this).toggle(username.includes(searchText));
|
||||||
$(this).show();
|
|
||||||
} else {
|
|
||||||
$(this).hide();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#addUserModal').on('show.bs.modal', function (event) {
|
$('#addUserModal').on('show.bs.modal', function () {
|
||||||
$('#addUserForm')[0].reset();
|
$('#addUserForm, #addBulkUsersForm').trigger('reset');
|
||||||
$('#addBulkUsersForm')[0].reset();
|
$('#addUsernameError, #addBulkPrefixError').text('');
|
||||||
$('#addUsernameError').text('');
|
Object.assign(document.getElementById('addTrafficLimit'), {value: 30});
|
||||||
$('#addBulkPrefixError').text('');
|
Object.assign(document.getElementById('addExpirationDays'), {value: 30});
|
||||||
$('#addTrafficLimit').val('30');
|
Object.assign(document.getElementById('addBulkTrafficLimit'), {value: 30});
|
||||||
$('#addExpirationDays').val('30');
|
Object.assign(document.getElementById('addBulkExpirationDays'), {value: 30});
|
||||||
$('#addBulkTrafficLimit').val('30');
|
$('#addSubmitButton, #addBulkSubmitButton').prop('disabled', true);
|
||||||
$('#addBulkExpirationDays').val('30');
|
|
||||||
$('#addBulkStartNumber').val('1');
|
|
||||||
$('#addSubmitButton').prop('disabled', true);
|
|
||||||
$('#addBulkSubmitButton').prop('disabled', true);
|
|
||||||
$('#addUserModal a[data-toggle="tab"]').first().tab('show');
|
$('#addUserModal a[data-toggle="tab"]').first().tab('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#searchButton").on("click", filterUsers);
|
$("#searchButton, #searchInput").on("click keyup", filterUsers);
|
||||||
$("#searchInput").on("keyup", filterUsers);
|
|
||||||
|
|
||||||
|
checkIpLimitServiceStatus();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
151
core/traffic.py
151
core/traffic.py
@ -16,16 +16,6 @@ LOCKFILE = "/tmp/kick.lock"
|
|||||||
BACKUP_FILE = f"{USERS_FILE}.bak"
|
BACKUP_FILE = f"{USERS_FILE}.bak"
|
||||||
MAX_WORKERS = 8
|
MAX_WORKERS = 8
|
||||||
|
|
||||||
# import logging
|
|
||||||
# logging.basicConfig(
|
|
||||||
# level=logging.INFO,
|
|
||||||
# format='%(asctime)s: [%(levelname)s] %(message)s',
|
|
||||||
# datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
# )
|
|
||||||
# logger = logging.getLogger()
|
|
||||||
# null_handler = logging.NullHandler()
|
|
||||||
# logger.handlers = [null_handler]
|
|
||||||
|
|
||||||
def acquire_lock():
|
def acquire_lock():
|
||||||
"""Acquires a lock file to prevent concurrent execution"""
|
"""Acquires a lock file to prevent concurrent execution"""
|
||||||
try:
|
try:
|
||||||
@ -36,14 +26,7 @@ def acquire_lock():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def traffic_status(no_gui=False):
|
def traffic_status(no_gui=False):
|
||||||
"""Updates and retrieves traffic statistics for all users.
|
"""Updates and retrieves traffic statistics for all users."""
|
||||||
|
|
||||||
Args:
|
|
||||||
no_gui (bool): If True, suppresses output to console
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: User data including upload/download bytes and status
|
|
||||||
"""
|
|
||||||
green = '\033[0;32m'
|
green = '\033[0;32m'
|
||||||
cyan = '\033[0;36m'
|
cyan = '\033[0;36m'
|
||||||
NC = '\033[0m'
|
NC = '\033[0m'
|
||||||
@ -90,8 +73,7 @@ def traffic_status(no_gui=False):
|
|||||||
users_data[user_id]["status"] = "Online" if status.is_online else "Offline"
|
users_data[user_id]["status"] = "Online" if status.is_online else "Offline"
|
||||||
else:
|
else:
|
||||||
users_data[user_id] = {
|
users_data[user_id] = {
|
||||||
"upload_bytes": 0,
|
"upload_bytes": 0, "download_bytes": 0,
|
||||||
"download_bytes": 0,
|
|
||||||
"status": "Online" if status.is_online else "Offline"
|
"status": "Online" if status.is_online else "Offline"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,11 +84,23 @@ def traffic_status(no_gui=False):
|
|||||||
else:
|
else:
|
||||||
online = user_id in online_status and online_status[user_id].is_online
|
online = user_id in online_status and online_status[user_id].is_online
|
||||||
users_data[user_id] = {
|
users_data[user_id] = {
|
||||||
"upload_bytes": stats.upload_bytes,
|
"upload_bytes": stats.upload_bytes, "download_bytes": stats.download_bytes,
|
||||||
"download_bytes": stats.download_bytes,
|
|
||||||
"status": "Online" if online else "Offline"
|
"status": "Online" if online else "Offline"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
today_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
for username, user_data in users_data.items():
|
||||||
|
is_on_hold = not user_data.get("account_creation_date")
|
||||||
|
|
||||||
|
if is_on_hold:
|
||||||
|
is_online = user_data.get("status") == "Online"
|
||||||
|
has_traffic = user_data.get("download_bytes", 0) > 0 or user_data.get("upload_bytes", 0) > 0
|
||||||
|
|
||||||
|
if is_online or has_traffic:
|
||||||
|
user_data["account_creation_date"] = today_date
|
||||||
|
else:
|
||||||
|
user_data["status"] = "On-hold"
|
||||||
|
|
||||||
with open(USERS_FILE, 'w') as users_file:
|
with open(USERS_FILE, 'w') as users_file:
|
||||||
json.dump(users_data, users_file, indent=4)
|
json.dump(users_data, users_file, indent=4)
|
||||||
|
|
||||||
@ -137,66 +131,49 @@ def display_traffic_data(data, green, cyan, NC):
|
|||||||
print(f"{user:<15} {green}{formatted_tx:<15}{NC} {cyan}{formatted_rx:<15}{NC} {status:<10}")
|
print(f"{user:<15} {green}{formatted_tx:<15}{NC} {cyan}{formatted_rx:<15}{NC} {status:<10}")
|
||||||
print("-------------------------------------------------")
|
print("-------------------------------------------------")
|
||||||
|
|
||||||
def format_bytes(bytes):
|
def format_bytes(bytes_val):
|
||||||
"""Format bytes as human-readable string"""
|
"""Format bytes as human-readable string"""
|
||||||
if bytes < 1024:
|
if bytes_val < 1024: return f"{bytes_val}B"
|
||||||
return f"{bytes}B"
|
elif bytes_val < 1048576: return f"{bytes_val / 1024:.2f}KB"
|
||||||
elif bytes < 1048576:
|
elif bytes_val < 1073741824: return f"{bytes_val / 1048576:.2f}MB"
|
||||||
return f"{bytes / 1024:.2f}KB"
|
elif bytes_val < 1099511627776: return f"{bytes_val / 1073741824:.2f}GB"
|
||||||
elif bytes < 1073741824:
|
else: return f"{bytes_val / 1099511627776:.2f}TB"
|
||||||
return f"{bytes / 1048576:.2f}MB"
|
|
||||||
elif bytes < 1099511627776:
|
|
||||||
return f"{bytes / 1073741824:.2f}GB"
|
|
||||||
else:
|
|
||||||
return f"{bytes / 1099511627776:.2f}TB"
|
|
||||||
|
|
||||||
def kick_users(usernames, secret):
|
def kick_users(usernames, secret):
|
||||||
"""Kicks specified users from the server"""
|
"""Kicks specified users from the server"""
|
||||||
try:
|
try:
|
||||||
client = Hysteria2Client(
|
client = Hysteria2Client(base_url=API_BASE_URL, secret=secret)
|
||||||
base_url=API_BASE_URL,
|
|
||||||
secret=secret
|
|
||||||
)
|
|
||||||
|
|
||||||
client.kick_clients(usernames)
|
client.kick_clients(usernames)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def process_user(username, user_data, config_secret, users_data):
|
def process_user(username, user_data, users_data):
|
||||||
"""Process a single user to check if they should be kicked"""
|
"""Process a single user to check if they should be kicked"""
|
||||||
blocked = user_data.get('blocked', False)
|
if user_data.get('blocked', False): return None
|
||||||
|
|
||||||
if blocked:
|
account_creation_date = user_data.get('account_creation_date')
|
||||||
return None
|
if not account_creation_date: return None
|
||||||
|
|
||||||
max_download_bytes = user_data.get('max_download_bytes', 0)
|
max_download_bytes = user_data.get('max_download_bytes', 0)
|
||||||
expiration_days = user_data.get('expiration_days', 0)
|
expiration_days = user_data.get('expiration_days', 0)
|
||||||
account_creation_date = user_data.get('account_creation_date')
|
total_bytes = user_data.get('download_bytes', 0) + user_data.get('upload_bytes', 0)
|
||||||
current_download_bytes = user_data.get('download_bytes', 0)
|
|
||||||
current_upload_bytes = user_data.get('upload_bytes', 0)
|
|
||||||
|
|
||||||
total_bytes = current_download_bytes + current_upload_bytes
|
|
||||||
|
|
||||||
if not account_creation_date:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
should_block = False
|
||||||
try:
|
try:
|
||||||
current_date = datetime.datetime.now().timestamp()
|
if expiration_days > 0:
|
||||||
creation_date = datetime.datetime.fromisoformat(account_creation_date.replace('Z', '+00:00'))
|
creation_date = datetime.datetime.strptime(account_creation_date, "%Y-%m-%d")
|
||||||
expiration_date = (creation_date + datetime.timedelta(days=expiration_days)).timestamp()
|
expiration_date = creation_date + datetime.timedelta(days=expiration_days)
|
||||||
|
if datetime.datetime.now() >= expiration_date:
|
||||||
should_block = False
|
|
||||||
|
|
||||||
if max_download_bytes > 0 and total_bytes >= 0 and expiration_days > 0:
|
|
||||||
if total_bytes >= max_download_bytes or current_date >= expiration_date:
|
|
||||||
should_block = True
|
should_block = True
|
||||||
|
|
||||||
if should_block:
|
if not should_block and max_download_bytes > 0 and total_bytes >= max_download_bytes:
|
||||||
users_data[username]['blocked'] = True
|
should_block = True
|
||||||
return username
|
|
||||||
|
|
||||||
except Exception:
|
if should_block:
|
||||||
|
users_data[username]['blocked'] = True
|
||||||
|
return username
|
||||||
|
except (ValueError, TypeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -206,59 +183,39 @@ def kick_expired_users():
|
|||||||
lock_file = acquire_lock()
|
lock_file = acquire_lock()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not os.path.exists(USERS_FILE): return
|
||||||
shutil.copy2(USERS_FILE, BACKUP_FILE)
|
shutil.copy2(USERS_FILE, BACKUP_FILE)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(CONFIG_FILE, 'r') as f:
|
with open(CONFIG_FILE, 'r') as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
secret = config.get('trafficStats', {}).get('secret', '')
|
secret = config.get('trafficStats', {}).get('secret', '')
|
||||||
if not secret:
|
if not secret: sys.exit(1)
|
||||||
sys.exit(1)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
shutil.copy2(BACKUP_FILE, USERS_FILE)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
with open(USERS_FILE, 'r') as f:
|
||||||
with open(USERS_FILE, 'r') as f:
|
users_data = json.load(f)
|
||||||
users_data = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
shutil.copy2(BACKUP_FILE, USERS_FILE)
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception:
|
|
||||||
shutil.copy2(BACKUP_FILE, USERS_FILE)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
users_to_kick = []
|
users_to_kick = []
|
||||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||||
future_to_user = {
|
futures = [executor.submit(process_user, u, d, users_data) for u, d in users_data.items()]
|
||||||
executor.submit(process_user, username, user_data, secret, users_data): username
|
for future in futures:
|
||||||
for username, user_data in users_data.items()
|
result = future.result()
|
||||||
}
|
if result:
|
||||||
|
users_to_kick.append(result)
|
||||||
for future in future_to_user:
|
|
||||||
username = future.result()
|
|
||||||
if username:
|
|
||||||
users_to_kick.append(username)
|
|
||||||
|
|
||||||
if users_to_kick:
|
if users_to_kick:
|
||||||
for retry in range(3):
|
with open(USERS_FILE, 'w') as f:
|
||||||
try:
|
json.dump(users_data, f, indent=4)
|
||||||
with open(USERS_FILE, 'w') as f:
|
|
||||||
json.dump(users_data, f, indent=2)
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
time.sleep(1)
|
|
||||||
if retry == 2:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if users_to_kick:
|
for i in range(0, len(users_to_kick), 50):
|
||||||
batch_size = 50
|
batch = users_to_kick[i:i+50]
|
||||||
for i in range(0, len(users_to_kick), batch_size):
|
|
||||||
batch = users_to_kick[i:i+batch_size]
|
|
||||||
kick_users(batch, secret)
|
kick_users(batch, secret)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
shutil.copy2(BACKUP_FILE, USERS_FILE)
|
if os.path.exists(BACKUP_FILE):
|
||||||
|
shutil.copy2(BACKUP_FILE, USERS_FILE)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
||||||
|
|||||||
Reference in New Issue
Block a user