From 7dfd68bf50f322c5d90597f4abc8a3edc4c46aa3 Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 12:02:20 +0330 Subject: [PATCH 1/7] refactor(user): Improve user retrieval queries and update datetime handling --- app/db/crud/user.py | 212 +++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 93 deletions(-) diff --git a/app/db/crud/user.py b/app/db/crud/user.py index 611ee2de4..09a701d41 100644 --- a/app/db/crud/user.py +++ b/app/db/crud/user.py @@ -1,12 +1,12 @@ import asyncio +from collections.abc import Sequence from copy import deepcopy -from datetime import UTC, datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from enum import Enum -from typing import List, Optional, Sequence from sqlalchemy import and_, case, delete, desc, func, not_, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.sql.functions import coalesce from app.db.compiles_types import DateDiff @@ -39,9 +39,8 @@ async def load_user_attrs(user: User): await user.awaitable_attrs.groups -async def get_user(db: AsyncSession, username: str) -> Optional[User]: - """ - Retrieves a user by username. +async def get_user(db: AsyncSession, username: str) -> User | None: + """Retrieves a user by username. Args: db (AsyncSession): Database session. @@ -49,18 +48,25 @@ async def get_user(db: AsyncSession, username: str) -> Optional[User]: Returns: Optional[User]: The user object if found, else None. + """ - stmt = select(User).where(User.username == username) + stmt = ( + select(User) + .options( + selectinload(User.groups), + joinedload(User.admin), + joinedload(User.next_plan), + selectinload(User.usage_logs), + ) + .where(User.username == username) + ) user = (await db.execute(stmt)).unique().scalar_one_or_none() - if user: - await load_user_attrs(user) return user async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None: - """ - Retrieves a user by user ID. + """Retrieves a user by user ID. Args: db (AsyncSession): Database session. @@ -68,12 +74,20 @@ async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None: Returns: Optional[User]: The user object if found, else None. + """ - stmt = select(User).where(User.id == user_id) + stmt = ( + select(User) + .options( + selectinload(User.groups), + joinedload(User.admin), + joinedload(User.next_plan), + selectinload(User.usage_logs), + ) + .where(User.id == user_id) + ) user = (await db.execute(stmt)).unique().scalar_one_or_none() - if user: - await load_user_attrs(user) return user @@ -111,8 +125,7 @@ async def get_users( return_with_count: bool = False, group_ids: list[int] | None = None, ) -> list[User] | tuple[list[User], int]: - """ - Retrieves users based on various filters. + """Retrieves users based on various filters. Args: db: Database session. @@ -130,8 +143,14 @@ async def get_users( Returns: List of users or tuple with (users, count) if return_with_count is True. + """ - stmt = select(User) + stmt = select(User).options( + selectinload(User.groups), + joinedload(User.admin), + joinedload(User.next_plan), + selectinload(User.usage_logs), + ) filters = [] if usernames: @@ -179,9 +198,6 @@ async def get_users( result = await db.execute(stmt) users = list(result.unique().scalars().all()) - for user in users: - await load_user_attrs(user) - if return_with_count: return users, total return users @@ -223,9 +239,7 @@ async def get_on_hold_to_active_users(db: AsyncSession) -> list[User]: async def get_users_to_reset_data_usage(db: AsyncSession) -> list[User]: - """ - Retrieves users whose data usage needs to be reset based on their reset strategy. - """ + """Retrieves users whose data usage needs to be reset based on their reset strategy.""" last_reset_subq = ( select( UserUsageResetLogs.user_id, @@ -263,8 +277,7 @@ async def get_users_to_reset_data_usage(db: AsyncSession) -> list[User]: async def get_usage_percentage_reached_users(db: AsyncSession, percentage: int) -> list[User]: - """ - Get active users who have reached or exceeded the specified usage percentage threshold + """Get active users who have reached or exceeded the specified usage percentage threshold and don't have an existing notification reminder for this threshold. """ # Subquery to check for existing notification reminders @@ -280,21 +293,24 @@ async def get_usage_percentage_reached_users(db: AsyncSession, percentage: int) stmt = ( select(User) - .options(joinedload(User.notification_reminders)) + .options( + selectinload(User.groups), + joinedload(User.admin), + joinedload(User.next_plan), + selectinload(User.usage_logs), + joinedload(User.notification_reminders), + ) .where(User.status == UserStatus.active) .where(User.usage_percentage >= percentage) .where(not_(existing_reminder_subq)) # Only users without existing reminders ) users = list((await db.execute(stmt)).unique().scalars().all()) - for user in users: - await load_user_attrs(user) return users async def get_days_left_reached_users(db: AsyncSession, days: int) -> list[User]: - """ - Get active users who have reached or exceeded the specified days left threshold + """Get active users who have reached or exceeded the specified days left threshold and don't have an existing notification reminder for this threshold. """ # Subquery to check for existing notification reminders @@ -332,10 +348,7 @@ async def get_user_usages( node_id: int | None = None, group_by_node: bool = False, ) -> UserUsageStatsList: - """ - Retrieves user usages within a specified date range. - """ - + """Retrieves user usages within a specified date range.""" # Build the appropriate truncation expression trunc_expr = _build_trunc_expression(period, NodeUserUsage.created_at) @@ -383,15 +396,16 @@ async def get_user_usages( async def get_users_count(db: AsyncSession, status: UserStatus = None, admin_id: int = None) -> int: - """ - Gets the total count of users with optional filters. + """Gets the total count of users with optional filters. Args: db (AsyncSession): Database session. status (UserStatus, optional): Filter by user status. admin_id (int, optional): Filter by admin. + Returns: int: Total count of users. + """ stmt = select(func.count(User.id)) @@ -409,17 +423,20 @@ async def get_users_count(db: AsyncSession, status: UserStatus = None, admin_id: async def get_users_count_by_status( - db: AsyncSession, statuses: list[UserStatus], admin_id: int = None + db: AsyncSession, + statuses: list[UserStatus], + admin_id: int = None, ) -> dict[str, int]: - """ - Gets count of users grouped by status in a single query. + """Gets count of users grouped by status in a single query. Args: db (AsyncSession): Database session. statuses (list[UserStatus]): List of statuses to count. admin_id (int, optional): Filter by admin. + Returns: dict[str, int]: Dictionary with status counts and total. + """ stmt = select(User.status, func.count(User.id).label("count")) @@ -442,8 +459,7 @@ async def get_users_count_by_status( async def create_user(db: AsyncSession, new_user: UserCreate, groups: list[Group], admin: Admin) -> User: - """ - Creates a new user. + """Creates a new user. Args: db (AsyncSession): Database session. @@ -453,9 +469,10 @@ async def create_user(db: AsyncSession, new_user: UserCreate, groups: list[Group Returns: User: Created user object. + """ db_user = User( - **new_user.model_dump(exclude={"group_ids", "expire", "proxy_settings", "next_plan", "on_hold_timeout"}) + **new_user.model_dump(exclude={"group_ids", "expire", "proxy_settings", "next_plan", "on_hold_timeout"}), ) db_user.admin = admin db_user.groups = groups @@ -478,8 +495,7 @@ async def create_user(db: AsyncSession, new_user: UserCreate, groups: list[Group async def remove_user(db: AsyncSession, db_user: User) -> User: - """ - Removes a user from the database. + """Removes a user from the database. Args: db (AsyncSession): Database session. @@ -487,6 +503,7 @@ async def remove_user(db: AsyncSession, db_user: User) -> User: Returns: User: Removed user object. + """ await db.delete(db_user) await db.commit() @@ -494,21 +511,19 @@ async def remove_user(db: AsyncSession, db_user: User) -> User: async def remove_users(db: AsyncSession, db_users: list[User]): - """ - Removes multiple users from the database. + """Removes multiple users from the database. Args: db (AsyncSession): Database session. dbusers (list[User]): List of user objects to be removed. - """ + """ await asyncio.gather(*[db.delete(user) for user in db_users]) await db.commit() async def modify_user(db: AsyncSession, db_user: User, modify: UserModify) -> User: - """ - Modify a user's information. + """Modify a user's information. Args: db (AsyncSession): Database session. @@ -517,6 +532,7 @@ async def modify_user(db: AsyncSession, db_user: User, modify: UserModify) -> Us Returns: User: Updated user object. + """ remove_usage_reminder = False remove_expiration_reminder = False @@ -542,7 +558,7 @@ async def modify_user(db: AsyncSession, db_user: User, modify: UserModify) -> Us elif modify.expire is not None: db_user.expire = modify.expire if db_user.status in [UserStatus.active, UserStatus.expired]: - if not db_user.expire or db_user.expire.replace(tzinfo=timezone.utc) > datetime.now(timezone.utc): + if not db_user.expire or db_user.expire.replace(tzinfo=UTC) > datetime.now(UTC): db_user.status = UserStatus.active remove_expiration_reminder = True @@ -585,7 +601,7 @@ async def modify_user(db: AsyncSession, db_user: User, modify: UserModify) -> Us elif db_user.next_plan is not None: await db.delete(db_user.next_plan) - db_user.edit_at = datetime.now(timezone.utc) + db_user.edit_at = datetime.now(UTC) if remove_usage_reminder or remove_expiration_reminder: id = db_user.id @@ -622,8 +638,7 @@ async def _reset_user_traffic_and_log(db: AsyncSession, db_user: User): async def reset_user_data_usage(db: AsyncSession, db_user: User) -> User: - """ - Resets the data usage of a user and logs the reset. + """Resets the data usage of a user and logs the reset. Args: db (AsyncSession): Database session. @@ -631,6 +646,7 @@ async def reset_user_data_usage(db: AsyncSession, db_user: User) -> User: Returns: User: The updated user object. + """ await _reset_user_traffic_and_log(db, db_user) @@ -644,8 +660,7 @@ async def reset_user_data_usage(db: AsyncSession, db_user: User) -> User: async def bulk_reset_user_data_usage(db: AsyncSession, users: list[User]) -> list[User]: - """ - Resets the data usage for a list of users and logs the reset. + """Resets the data usage for a list of users and logs the reset. Args: db (AsyncSession): Database session. @@ -653,6 +668,7 @@ async def bulk_reset_user_data_usage(db: AsyncSession, users: list[User]) -> lis Returns: list[User]: The updated list of user objects. + """ for db_user in users: await _reset_user_traffic_and_log(db, db_user) @@ -666,8 +682,7 @@ async def bulk_reset_user_data_usage(db: AsyncSession, users: list[User]) -> lis async def reset_user_by_next(db: AsyncSession, db_user: User) -> User: - """ - Resets the data usage of a user based on next user. + """Resets the data usage of a user based on next user. Args: db (AsyncSession): Database session. @@ -675,6 +690,7 @@ async def reset_user_by_next(db: AsyncSession, db_user: User) -> User: Returns: User: The updated user object. + """ remaining_traffic = (db_user.data_limit or 0) - db_user.used_traffic if db_user.next_plan.user_template_id is None: @@ -728,8 +744,7 @@ async def reset_user_by_next(db: AsyncSession, db_user: User) -> User: async def revoke_user_sub(db: AsyncSession, db_user: User) -> User: - """ - Revokes the subscription of a user and updates proxies settings. + """Revokes the subscription of a user and updates proxies settings. Args: db (AsyncSession): Database session. @@ -737,11 +752,12 @@ async def revoke_user_sub(db: AsyncSession, db_user: User) -> User: Returns: User: The updated user object. + """ stmt = ( update(User) .where(User.id == db_user.id) - .values(sub_revoked_at=datetime.now(timezone.utc), proxy_settings=ProxyTable().dict()) + .values(sub_revoked_at=datetime.now(UTC), proxy_settings=ProxyTable().dict()) ) await db.execute(stmt) await db.commit() @@ -751,8 +767,7 @@ async def revoke_user_sub(db: AsyncSession, db_user: User) -> User: async def user_sub_update(db: AsyncSession, user_id: User, user_agent: str) -> User: - """ - Updates the user's subscription details. + """Updates the user's subscription details. Args: db (AsyncSession): Database session. @@ -766,7 +781,10 @@ async def user_sub_update(db: AsyncSession, user_id: User, user_agent: str) -> U async def get_user_sub_update_list( - db: AsyncSession, user_id: int, offset: int = 0, limit: int = 10 + db: AsyncSession, + user_id: int, + offset: int = 0, + limit: int = 10, ) -> tuple[Sequence[UserSubscriptionUpdate], int]: stmt = ( select(UserSubscriptionUpdate) @@ -788,10 +806,10 @@ async def get_user_sub_update_list( async def autodelete_expired_users( - db: AsyncSession, include_limited_users: bool = False + db: AsyncSession, + include_limited_users: bool = False, ) -> list[UserNotificationResponse]: - """ - Deletes expired (optionally also limited) users whose auto-delete time has passed. + """Deletes expired (optionally also limited) users whose auto-delete time has passed. Args: db (AsyncSession): Database session @@ -800,6 +818,7 @@ async def autodelete_expired_users( Returns: list[UserNotificationResponse]: List of deleted users. + """ target_status = [UserStatus.expired] if not include_limited_users else [UserStatus.expired, UserStatus.limited] @@ -820,8 +839,7 @@ async def autodelete_expired_users( expired_users = [ user for (user, auto_delete) in (await db.execute(query)).unique() - if user.last_status_change.replace(tzinfo=timezone.utc) + timedelta(days=auto_delete) - <= datetime.now(timezone.utc) + if user.last_status_change.replace(tzinfo=UTC) + timedelta(days=auto_delete) <= datetime.now(UTC) ] result: list[UserNotificationResponse] = [] @@ -844,8 +862,7 @@ async def get_all_users_usages( node_id: int | None = None, group_by_node: bool = False, ) -> UserUsageStatsList: - """ - Retrieves aggregated usage data for all users of an admin within a specified time range, + """Retrieves aggregated usage data for all users of an admin within a specified time range, grouped by the specified time period. Args: @@ -858,6 +875,7 @@ async def get_all_users_usages( Returns: UserUsageStatsList: Aggregated usage data for each period. + """ admin_users = {user.id for user in await get_users(db=db, admins=admin)} @@ -907,8 +925,7 @@ async def get_all_users_usages( async def update_users_status(db: AsyncSession, users: list[User], status: UserStatus) -> list[User]: - """ - Updates a users status and records the time of change. + """Updates a users status and records the time of change. Args: db (AsyncSession): Database session. @@ -917,11 +934,10 @@ async def update_users_status(db: AsyncSession, users: list[User], status: UserS Returns: User: The updated user object. + """ user_ids = [user.id for user in users] - stmt = ( - update(User).where(User.id.in_(user_ids)).values(status=status, last_status_change=datetime.now(timezone.utc)) - ) + stmt = update(User).where(User.id.in_(user_ids)).values(status=status, last_status_change=datetime.now(UTC)) await db.execute(stmt) await db.commit() for user in users: @@ -931,8 +947,7 @@ async def update_users_status(db: AsyncSession, users: list[User], status: UserS async def set_owner(db: AsyncSession, db_user: User, admin: Admin) -> User: - """ - Sets the owner (admin) of a user. + """Sets the owner (admin) of a user. Args: db (AsyncSession): Database session. @@ -941,6 +956,7 @@ async def set_owner(db: AsyncSession, db_user: User, admin: Admin) -> User: Returns: User: The updated user object. + """ stmt = update(User).where(User.id == db_user.id).values(admin_id=admin.id) await db.execute(stmt) @@ -951,8 +967,7 @@ async def set_owner(db: AsyncSession, db_user: User, admin: Admin) -> User: async def start_users_expire(db: AsyncSession, users: list[User]) -> list[User]: - """ - Starts the expiration timer for a user. + """Starts the expiration timer for a user. Args: db (AsyncSession): Database session. @@ -960,8 +975,9 @@ async def start_users_expire(db: AsyncSession, users: list[User]) -> list[User]: Returns: list[User]: The updated users list. + """ - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for user in users: expire_time = now + timedelta(seconds=user.on_hold_expire_duration) stmt = ( @@ -979,10 +995,13 @@ async def start_users_expire(db: AsyncSession, users: list[User]) -> list[User]: async def create_notification_reminder( - db: AsyncSession, reminder_type: ReminderType, expires_at: datetime, user_id: int, threshold: int | None = None + db: AsyncSession, + reminder_type: ReminderType, + expires_at: datetime, + user_id: int, + threshold: int | None = None, ) -> NotificationReminder: - """ - Creates a new notification reminder. + """Creates a new notification reminder. Args: db (AsyncSession): The database session. @@ -993,6 +1012,7 @@ async def create_notification_reminder( Returns: NotificationReminder: The newly created NotificationReminder object. + """ reminder = NotificationReminder(type=reminder_type, expires_at=expires_at, user_id=user_id) if threshold is not None: @@ -1003,13 +1023,13 @@ async def create_notification_reminder( return reminder -async def bulk_create_notification_reminders(db: AsyncSession, reminder_data: List[dict]) -> None: - """ - Bulk creates notification reminders. +async def bulk_create_notification_reminders(db: AsyncSession, reminder_data: list[dict]) -> None: + """Bulk creates notification reminders. Args: db (AsyncSession): The database session. reminder_data (List[dict]): List of reminder data dicts with keys: user_id, type, threshold, expires_at + """ if not reminder_data: return @@ -1017,7 +1037,10 @@ async def bulk_create_notification_reminders(db: AsyncSession, reminder_data: Li reminders = [] for data in reminder_data: reminder = NotificationReminder( - type=data["type"], expires_at=data["expires_at"], user_id=data["user_id"], threshold=data.get("threshold") + type=data["type"], + expires_at=data["expires_at"], + user_id=data["user_id"], + threshold=data.get("threshold"), ) reminders.append(reminder) @@ -1026,16 +1049,19 @@ async def bulk_create_notification_reminders(db: AsyncSession, reminder_data: Li async def delete_user_passed_notification_reminders( - db: AsyncSession, user_id: int, type: ReminderType, threshold: int + db: AsyncSession, + user_id: int, + type: ReminderType, + threshold: int, ) -> None: - """ - Deletes user reminders passed. + """Deletes user reminders passed. Args: db (AsyncSession): The database session. user_id (int): The ID of the user. reminder_type (ReminderType): The type of reminder to delete. threshold (int): The threshold to delete (e.g., days left or usage percent). + """ conditions = [NotificationReminder.user_id == user_id, NotificationReminder.type == type] @@ -1049,8 +1075,7 @@ async def delete_user_passed_notification_reminders( async def count_online_users(db: AsyncSession, time_delta: timedelta, admin_id: int | None = None): - """ - Counts the number of users who have been online within the specified time delta. + """Counts the number of users who have been online within the specified time delta. Args: db (AsyncSession): The database session. @@ -1059,8 +1084,9 @@ async def count_online_users(db: AsyncSession, time_delta: timedelta, admin_id: Returns: int: The number of users who have been online within the specified time period. + """ - twenty_four_hours_ago = datetime.now(timezone.utc) - time_delta + twenty_four_hours_ago = datetime.now(UTC) - time_delta query = select(func.count(User.id)).where(User.online_at.isnot(None), User.online_at >= twenty_four_hours_ago) if admin_id: query = query.where(User.admin_id == admin_id) From 7ebcbac527f477ab8dae6dee6fe2a36d8924ba5f Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 12:15:06 +0330 Subject: [PATCH 2/7] refactor(init): Simplify lifespan function and improve logging format refactor(main): Replace os.path with pathlib for file checks and enhance IP validation logic --- app/__init__.py | 16 +++++++--------- main.py | 48 +++++++++++++++++++++--------------------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 4a1b66598..fb12d901d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -37,11 +37,10 @@ async def lifespan(app: FastAPI): await func(app) else: await func() + elif "app" in func.__code__.co_varnames: + func(app) else: - if "app" in func.__code__.co_varnames: - func(app) - else: - func() + func() yield for func in shutdown_functions: @@ -51,11 +50,10 @@ async def lifespan(app: FastAPI): await func(app) else: await func() + elif "app" in func.__code__.co_varnames: + func(app) else: - if "app" in func.__code__.co_varnames: - func(app) - else: - func() + func() app = FastAPI( @@ -102,7 +100,7 @@ def validate_paths(): on_startup(scheduler.start) on_shutdown(scheduler.shutdown) -on_startup(lambda: logger.info(f"PasarGuard v{__version__}")) +on_startup(lambda: logger.info("PasarGuard v%s", __version__)) @app.exception_handler(RequestValidationError) diff --git a/main.py b/main.py index e3043aa60..1f8e527af 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ import ipaddress import logging -import os +import pathlib import socket import ssl @@ -24,8 +24,7 @@ def check_and_modify_ip(ip_address: str) -> str: - """ - Check if an IP address is private. If not, return localhost. + """Check if an IP address is private. If not, return localhost. IPv4 Private range = [ "192.168.0.0", @@ -44,6 +43,7 @@ def check_and_modify_ip(ip_address: str) -> str: Raises: ValueError: If the provided IP address is invalid, return localhost. + """ try: # Attempt to resolve hostname to IP address @@ -54,19 +54,18 @@ def check_and_modify_ip(ip_address: str) -> str: if ip == ipaddress.ip_address("0.0.0.0"): return "localhost" - elif ip.is_private: + if ip.is_private: return ip_address - else: - return "localhost" + return "localhost" except ValueError: return "localhost" def validate_cert_and_key(cert_file_path, key_file_path): - if not os.path.isfile(cert_file_path): + if not pathlib.Path(cert_file_path).is_file(): raise ValueError(f"SSL certificate file '{cert_file_path}' does not exist.") - if not os.path.isfile(key_file_path): + if not pathlib.Path(key_file_path).is_file(): raise ValueError(f"SSL key file '{key_file_path}' does not exist.") try: @@ -95,30 +94,26 @@ def validate_cert_and_key(cert_file_path, key_file_path): if UVICORN_SSL_CERTFILE and UVICORN_SSL_KEYFILE: validate_cert_and_key(UVICORN_SSL_CERTFILE, UVICORN_SSL_KEYFILE) + bind_args.update( + ssl_certfile=UVICORN_SSL_CERTFILE, + ssl_keyfile=UVICORN_SSL_KEYFILE, + ) - bind_args["ssl_certfile"] = UVICORN_SSL_CERTFILE - bind_args["ssl_keyfile"] = UVICORN_SSL_KEYFILE - - if UVICORN_UDS: - bind_args["uds"] = UVICORN_UDS - else: - bind_args["host"] = UVICORN_HOST - bind_args["port"] = UVICORN_PORT - + # Set host/port or UDS based on configuration + if UVICORN_UDS: + bind_args["uds"] = UVICORN_UDS + elif UVICORN_SSL_CERTFILE and UVICORN_SSL_KEYFILE: + bind_args.update(host=UVICORN_HOST, port=UVICORN_PORT) else: - if UVICORN_UDS: - bind_args["uds"] = UVICORN_UDS - else: - ip = check_and_modify_ip(UVICORN_HOST) - - logger.warning(f""" + ip = check_and_modify_ip(UVICORN_HOST) + logger.warning(f""" {click.style("IMPORTANT!", blink=True, bold=True, fg="yellow")} You're running PasarGuard without specifying {click.style("UVICORN_SSL_CERTFILE", italic=True, fg="magenta")} and {click.style("UVICORN_SSL_KEYFILE", italic=True, fg="magenta")}. The application will only be accessible through localhost. This means that {click.style("Marzban and subscription URLs will not be accessible externally", bold=True)}. If you need external access, please provide the SSL files to allow the server to bind to 0.0.0.0. Alternatively, you can run the server on localhost or a Unix socket and use a reverse proxy, such as Nginx or Caddy, to handle SSL termination and provide external access. -If you wish to continue without SSL, you can use SSH port forwarding to access the application from your machine. note that in this case, subscription functionality will not work. +If you wish to continue without SSL, you can use SSH port forwarding to access the application from your machine. note that in this case, subscription functionality will not work. Use the following command: @@ -126,9 +121,8 @@ def validate_cert_and_key(cert_file_path, key_file_path): Then, navigate to {click.style(f"http://{ip}:{UVICORN_PORT}", bold=True)} on your computer. """) - - bind_args["host"] = ip - bind_args["port"] = UVICORN_PORT + bind_args["host"] = ip + bind_args["port"] = UVICORN_PORT if DEBUG: bind_args["uds"] = None From 10c0c02d4f56dfaaa341df38fd8a88be559ae440 Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 13:11:11 +0330 Subject: [PATCH 3/7] feat(user): Add BulkOperationResponse model for standardized bulk operation responses --- app/models/user.py | 7 ++++ app/operation/group.py | 22 ++++++++---- app/operation/user.py | 72 ++++++++++++++++++++++++++----------- app/routers/group.py | 52 +++++++++++++++++---------- app/routers/user.py | 80 ++++++++++++++++++++++++++++-------------- app/utils/crypto.py | 2 +- 6 files changed, 160 insertions(+), 75 deletions(-) diff --git a/app/models/user.py b/app/models/user.py index f98c525bc..3ca393d14 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -146,6 +146,13 @@ class UserSubscriptionUpdateList(BaseModel): count: int +class BulkOperationResponse(BaseModel): + """Standardized response for bulk operations.""" + + detail: str + affected_count: int + + class RemoveUsersResponse(BaseModel): users: list[str] count: int diff --git a/app/operation/group.py b/app/operation/group.py index 8ad4ae807..8c898b403 100644 --- a/app/operation/group.py +++ b/app/operation/group.py @@ -7,6 +7,7 @@ from app.db.crud.user import get_users from app.db.models import Admin from app.models.group import BulkGroup, Group, GroupCreate, GroupModify, GroupResponse, GroupsResponse +from app.models.user import BulkOperationResponse from app.node import node_manager from app.operation import BaseOperation, OperatorType from app.utils.logger import get_logger @@ -28,7 +29,10 @@ async def create_group(self, db: AsyncSession, new_group: GroupCreate, admin: Ad return group async def get_all_groups( - self, db: AsyncSession, offset: int | None = None, limit: int | None = None + self, + db: AsyncSession, + offset: int | None = None, + limit: int | None = None, ) -> GroupsResponse: db_groups, count = await get_group(db, offset, limit) return GroupsResponse(groups=db_groups, total=count) @@ -64,18 +68,19 @@ async def remove_group(self, db: AsyncSession, group_id: int, admin: Admin) -> N asyncio.create_task(notification.remove_group(db_group.id, admin.username)) - async def bulk_add_groups(self, db: AsyncSession, bulk_model: BulkGroup): - await self.validate_all_groups(db, bulk_model) - + async def bulk_add_groups(self, db: AsyncSession, bulk_model: BulkGroup) -> BulkOperationResponse | int: users, users_count = await add_groups_to_users(db, bulk_model) await node_manager.update_users(users) if self.operator_type in (OperatorType.API, OperatorType.WEB): - return {"detail": f"operation has been successfuly done on {users_count} users"} + return BulkOperationResponse( + detail=f"operation has been successfully done on {users_count} users", + affected_count=users_count, + ) return users_count - async def bulk_remove_groups(self, db: AsyncSession, bulk_model: BulkGroup): + async def bulk_remove_groups(self, db: AsyncSession, bulk_model: BulkGroup) -> BulkOperationResponse | int: await self.validate_all_groups(db, bulk_model) users, users_count = await remove_groups_from_users(db, bulk_model) @@ -83,5 +88,8 @@ async def bulk_remove_groups(self, db: AsyncSession, bulk_model: BulkGroup): await node_manager.update_users(users) if self.operator_type in (OperatorType.API, OperatorType.WEB): - return {"detail": f"operation has been successfuly done on {users_count} users"} + return BulkOperationResponse( + detail=f"operation has been successfully done on {users_count} users", + affected_count=users_count, + ) return users_count diff --git a/app/operation/user.py b/app/operation/user.py index 02e61e93b..08decc923 100644 --- a/app/operation/user.py +++ b/app/operation/user.py @@ -1,6 +1,6 @@ import asyncio import secrets -from datetime import datetime as dt, timedelta as td, timezone as tz +from datetime import UTC, datetime as dt, timedelta as td from pydantic import ValidationError from sqlalchemy.exc import IntegrityError @@ -34,6 +34,7 @@ from app.models.admin import AdminDetails from app.models.stats import Period, UserUsageStatsList from app.models.user import ( + BulkOperationResponse, BulkUser, BulkUsersProxy, CreateUserFromTemplate, @@ -106,7 +107,11 @@ async def create_user(self, db: AsyncSession, new_user: UserCreate, admin: Admin return user async def _modify_user( - self, db: AsyncSession, db_user: User, modified_user: UserModify, admin: AdminDetails + self, + db: AsyncSession, + db_user: User, + modified_user: UserModify, + admin: AdminDetails, ) -> UserResponse: if modified_user.group_ids: await self.validate_all_groups(db, modified_user) @@ -131,7 +136,11 @@ async def _modify_user( return user async def modify_user( - self, db: AsyncSession, username: str, modified_user: UserModify, admin: AdminDetails + self, + db: AsyncSession, + username: str, + modified_user: UserModify, + admin: AdminDetails, ) -> UserResponse: db_user = await self.get_validated_user(db, username, admin) @@ -209,7 +218,11 @@ async def active_next_plan(self, db: AsyncSession, username: str, admin: AdminDe return user async def set_owner( - self, db: AsyncSession, username: str, admin_username: str, admin: AdminDetails + self, + db: AsyncSession, + username: str, + admin_username: str, + admin: AdminDetails, ) -> UserResponse: """Set a new owner (admin) for a user.""" new_admin = await self.get_validated_admin(db, username=admin_username) @@ -293,7 +306,7 @@ async def get_users( tasks = [self.generate_subscription_url(user) for user in users] urls = await asyncio.gather(*tasks) - for user, url in zip(users, urls): + for user, url in zip(users, urls, strict=False): user.subscription_url = url response = UsersResponse(users=users, total=count) @@ -331,7 +344,7 @@ async def get_users_usage( @staticmethod async def remove_users_logger(users: list[str], by: str): for user in users: - logger.info(f'User "{user}" deleted by admin "{by}"') + logger.info('User "%s" deleted by admin "%s"', user, by) async def get_expired_users( self, @@ -340,15 +353,13 @@ async def get_expired_users( expired_before: dt = None, admin_username: str = None, ) -> list[str]: - """ - Get users who have expired within the specified date range. + """Get users who have expired within the specified date range. - **expired_after** UTC datetime (optional) - **expired_before** UTC datetime (optional) - At least one of expired_after or expired_before must be provided for filtering - If both are omitted, returns all expired users """ - expired_after, expired_before = await self.validate_dates(expired_after, expired_before) if admin_username: admin_id = (await self.get_validated_admin(db, admin_username)).id @@ -365,14 +376,12 @@ async def delete_expired_users( expired_before: dt = None, admin_username: str = None, ) -> RemoveUsersResponse: - """ - Delete users who have expired within the specified date range. + """Delete users who have expired within the specified date range. - **expired_after** UTC datetime (optional) - **expired_before** UTC datetime (optional) - At least one of expired_after or expired_before must be provided """ - expired_after, expired_before = await self.validate_dates(expired_after, expired_before) if admin_username: admin_id = (await self.get_validated_admin(db, admin_username)).id @@ -397,14 +406,14 @@ def load_base_user_args(template: UserTemplate) -> dict: if template.status == UserStatus.active: if template.expire_duration: - user_args["expire"] = dt.now(tz.utc) + td(seconds=template.expire_duration) + user_args["expire"] = dt.now(UTC) + td(seconds=template.expire_duration) else: user_args["expire"] = 0 else: user_args["expire"] = 0 user_args["on_hold_expire_duration"] = template.expire_duration if template.on_hold_timeout: - user_args["on_hold_timeout"] = dt.now(tz.utc) + td(seconds=template.on_hold_timeout) + user_args["on_hold_timeout"] = dt.now(UTC) + td(seconds=template.on_hold_timeout) else: user_args["on_hold_timeout"] = 0 @@ -425,7 +434,10 @@ def apply_settings(user_args: UserCreate | UserModify, template: UserTemplate) - return user_args async def create_user_from_template( - self, db: AsyncSession, new_template_user: CreateUserFromTemplate, admin: AdminDetails + self, + db: AsyncSession, + new_template_user: CreateUserFromTemplate, + admin: AdminDetails, ) -> UserResponse: user_template = await self.get_validated_user_template(db, new_template_user.user_template_id) @@ -434,7 +446,7 @@ async def create_user_from_template( new_user_args = self.load_base_user_args(user_template) new_user_args["username"] = ( - f"{user_template.username_prefix if user_template.username_prefix else ''}{new_template_user.username}{user_template.username_suffix if user_template.username_suffix else ''}" + f"{user_template.username_prefix or ''}{new_template_user.username}{user_template.username_suffix or ''}" ) try: @@ -448,7 +460,11 @@ async def create_user_from_template( return await self.create_user(db, new_user, admin) async def modify_user_with_template( - self, db: AsyncSession, username: str, modified_template: ModifyUserByTemplate, admin: AdminDetails + self, + db: AsyncSession, + username: str, + modified_template: ModifyUserByTemplate, + admin: AdminDetails, ) -> UserResponse: db_user = await self.get_validated_user(db, username, admin) user_template = await self.get_validated_user_template(db, modified_template.user_template_id) @@ -478,7 +494,10 @@ async def bulk_modify_expire(self, db: AsyncSession, bulk_model: BulkUser): await node_manager.update_users(users) if self.operator_type in (OperatorType.API, OperatorType.WEB): - return {"detail": f"operation has been successfuly done on {users_count} users"} + return BulkOperationResponse( + detail=f"operation has been successfully done on {users_count} users", + affected_count=users_count, + ) return users_count async def bulk_modify_datalimit(self, db: AsyncSession, bulk_model: BulkUser): @@ -487,7 +506,10 @@ async def bulk_modify_datalimit(self, db: AsyncSession, bulk_model: BulkUser): await node_manager.update_users(users) if self.operator_type in (OperatorType.API, OperatorType.WEB): - return {"detail": f"operation has been successfuly done on {users_count} users"} + return BulkOperationResponse( + detail=f"operation has been successfully done on {users_count} users", + affected_count=users_count, + ) return users_count async def bulk_modify_proxy_settings(self, db: AsyncSession, bulk_model: BulkUsersProxy): @@ -496,11 +518,19 @@ async def bulk_modify_proxy_settings(self, db: AsyncSession, bulk_model: BulkUse await node_manager.update_users(users) if self.operator_type in (OperatorType.API, OperatorType.WEB): - return {"detail": f"operation has been successfuly done on {users_count} users"} + return BulkOperationResponse( + detail=f"operation has been successfully done on {users_count} users", + affected_count=users_count, + ) return users_count async def get_user_sub_update_list( - self, db: AsyncSession, username: str, admin: AdminDetails, offset: int = 0, limit: int = 10 + self, + db: AsyncSession, + username: str, + admin: AdminDetails, + offset: int = 0, + limit: int = 10, ) -> UserSubscriptionUpdateList: db_user = await self.get_validated_user(db, username, admin) user_sub_data, count = await get_user_sub_update_list(db, user_id=db_user.id, offset=offset, limit=limit) diff --git a/app/routers/group.py b/app/routers/group.py index 8343aad75..435d3d96c 100644 --- a/app/routers/group.py +++ b/app/routers/group.py @@ -3,6 +3,7 @@ from app.db import AsyncSession, get_db from app.models.admin import AdminDetails from app.models.group import BulkGroup, GroupCreate, GroupModify, GroupResponse, GroupsResponse +from app.models.user import BulkOperationResponse from app.operation import OperatorType from app.operation.group import GroupOperation from app.utils import responses @@ -21,10 +22,11 @@ description="Creates a new group in the system. Only sudo administrators can create groups.", ) async def create_group( - new_group: GroupCreate, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(check_sudo_admin) + new_group: GroupCreate, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(check_sudo_admin), ): - """ - Create a new group in the system. + """Create a new group in the system. The group model has the following properties: - **name**: String (3-64 chars) containing only a-z and 0-9 @@ -39,6 +41,7 @@ async def create_group( Raises: 401: Unauthorized - If not authenticated 403: Forbidden - If not sudo admin + """ return await group_operator.create_group(db, new_group, admin) @@ -50,10 +53,12 @@ async def create_group( description="Retrieves a paginated list of all groups in the system. Requires admin authentication.", ) async def get_all_groups( - offset: int = None, limit: int = None, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(get_current) + offset: int | None = None, + limit: int | None = None, + db: AsyncSession = Depends(get_db), + _: AdminDetails = Depends(get_current), ): - """ - Retrieve a list of all groups with optional pagination. + """Retrieve a list of all groups with optional pagination. The response includes: - **groups**: List of GroupResponse objects containing: @@ -69,6 +74,7 @@ async def get_all_groups( Raises: 401: Unauthorized - If not authenticated + """ return await group_operator.get_all_groups(db, offset, limit) @@ -81,8 +87,7 @@ async def get_all_groups( responses={404: responses._404}, ) async def get_group(group_id: int, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(get_current)): - """ - Get a specific group by its **ID**. + """Get a specific group by its **ID**. The response includes: - **id**: Unique identifier @@ -96,6 +101,7 @@ async def get_group(group_id: int, db: AsyncSession = Depends(get_db), _: AdminD Raises: 404: Not Found - If group doesn't exist + """ return await group_operator.get_validated_group(db, group_id) @@ -113,8 +119,7 @@ async def modify_group( db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(check_sudo_admin), ): - """ - Modify an existing group's information. + """Modify an existing group's information. The group model can be modified with: - **name**: String (3-64 chars) containing only a-z and 0-9 @@ -130,6 +135,7 @@ async def modify_group( 401: Unauthorized - If not authenticated 403: Forbidden - If not sudo admin 404: Not Found - If group doesn't exist + """ return await group_operator.modify_group(db, group_id, modified_group, admin) @@ -142,10 +148,11 @@ async def modify_group( responses={404: responses._404}, ) async def remove_group( - group_id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(check_sudo_admin) + group_id: int, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(check_sudo_admin), ): - """ - Remove a group by its **ID**. + """Remove a group by its **ID**. Returns: dict: Empty dictionary on successful deletion @@ -154,6 +161,7 @@ async def remove_group( 401: Unauthorized - If not authenticated 403: Forbidden - If not sudo admin 404: Not Found - If group doesn't exist + """ await group_operator.remove_group(db, group_id, admin) return {} @@ -161,14 +169,16 @@ async def remove_group( @router.post( "s/bulk/add", + response_model=BulkOperationResponse, summary="Bulk add groups to users", response_description="Success confirmation", ) async def bulk_add_groups_to_users( - bulk_group: BulkGroup, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(get_current) + bulk_group: BulkGroup, + db: AsyncSession = Depends(get_db), + _: AdminDetails = Depends(get_current), ): - """ - Bulk assign groups to multiple users, users under specific admins, or all users. + """Bulk assign groups to multiple users, users under specific admins, or all users. - **group_ids**: List of group IDs to add (required) - **users**: Optional list of user IDs to assign the groups to @@ -178,20 +188,23 @@ async def bulk_add_groups_to_users( - If neither 'users' nor 'admins' are provided, groups will be added to *all users* - Existing user-group associations will be ignored (no duplication) - Returns list of affected users (those who received new group associations) + """ return await group_operator.bulk_add_groups(db, bulk_group) @router.post( "s/bulk/remove", + response_model=BulkOperationResponse, summary="Bulk remove groups from users", response_description="Success confirmation", ) async def bulk_remove_users_from_groups( - bulk_group: BulkGroup, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(get_current) + bulk_group: BulkGroup, + db: AsyncSession = Depends(get_db), + _: AdminDetails = Depends(get_current), ): - """ - Bulk remove groups from multiple users, users under specific admins, or all users. + """Bulk remove groups from multiple users, users under specific admins, or all users. - **group_ids**: List of group IDs to remove (required) - **users**: Optional list of user IDs to remove the groups from @@ -201,5 +214,6 @@ async def bulk_remove_users_from_groups( - If neither 'users' nor 'admins' are provided, groups will be removed from *all users* - Only existing user-group associations will be removed - Returns list of affected users (those who had groups removed) + """ return await group_operator.bulk_remove_groups(db, bulk_group) diff --git a/app/routers/user.py b/app/routers/user.py index 3d0f0ba8d..f3897e7f1 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -7,6 +7,7 @@ from app.models.admin import AdminDetails from app.models.stats import Period, UserUsageStatsList from app.models.user import ( + BulkOperationResponse, BulkUser, BulkUsersProxy, CreateUserFromTemplate, @@ -37,10 +38,11 @@ status_code=status.HTTP_201_CREATED, ) async def create_user( - new_user: UserCreate, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current) + new_user: UserCreate, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(get_current), ): - """ - Create a new user + """Create a new user - **username**: 3 to 32 characters, can include a-z, 0-9, and underscores. - **status**: User's status, defaults to `active`. Special rules if `on_hold`. @@ -54,7 +56,6 @@ async def create_user( - **on_hold_expire_duration**: Duration (in seconds) for how long the user should stay in `on_hold` status. - **next_plan**: Next user plan (resets after use). """ - return await user_operator.create_user(db, new_user=new_user, admin=admin) @@ -69,8 +70,7 @@ async def modify_user( db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current), ): - """ - Modify an existing user + """Modify an existing user - **username**: Cannot be changed. Used to identify the user. - **status**: User's new status. Can be 'active', 'disabled', 'on_hold', 'limited', or 'expired'. @@ -90,7 +90,9 @@ async def modify_user( @router.delete( - "/{username}", responses={403: responses._403, 404: responses._404}, status_code=status.HTTP_204_NO_CONTENT + "/{username}", + responses={403: responses._403, 404: responses._404}, + status_code=status.HTTP_204_NO_CONTENT, ) async def remove_user(username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current)): """Remove a user""" @@ -99,17 +101,23 @@ async def remove_user(username: str, db: AsyncSession = Depends(get_db), admin: @router.post("/{username}/reset", response_model=UserResponse, responses={403: responses._403, 404: responses._404}) async def reset_user_data_usage( - username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current) + username: str, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(get_current), ): """Reset user data usage""" return await user_operator.reset_user_data_usage(db, username=username, admin=admin) @router.post( - "/{username}/revoke_sub", response_model=UserResponse, responses={403: responses._403, 404: responses._404} + "/{username}/revoke_sub", + response_model=UserResponse, + responses={403: responses._403, 404: responses._404}, ) async def revoke_user_subscription( - username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current) + username: str, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(get_current), ): """Revoke users subscription (Subscription link and proxies)""" return await user_operator.revoke_user_sub(db, username=username, admin=admin) @@ -135,10 +143,14 @@ async def set_owner( @router.post( - "/{username}/active_next", response_model=UserResponse, responses={403: responses._403, 404: responses._404} + "/{username}/active_next", + response_model=UserResponse, + responses={403: responses._403, 404: responses._404}, ) async def active_next_plan( - username: str, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(get_current) + username: str, + db: AsyncSession = Depends(get_db), + admin: AdminDetails = Depends(get_current), ): """Reset user by next plan""" return await user_operator.active_next_plan(db, username=username, admin=admin) @@ -167,7 +179,9 @@ async def get_user_sub_update_list( @router.get( - "s", response_model=UsersResponse, responses={400: responses._400, 403: responses._403, 404: responses._404} + "s", + response_model=UsersResponse, + responses={400: responses._400, 403: responses._403, 404: responses._404}, ) async def get_users( offset: int = None, @@ -201,7 +215,9 @@ async def get_users( @router.get( - "/{username}/usage", response_model=UserUsageStatsList, responses={403: responses._403, 404: responses._404} + "/{username}/usage", + response_model=UserUsageStatsList, + responses={403: responses._403, 404: responses._404}, ) async def get_user_usage( username: str, @@ -258,15 +274,13 @@ async def get_expired_users( expired_after: dt | None = Query(None, example="2024-01-01T00:00:00+03:30"), expired_before: dt | None = Query(None, example="2024-01-31T23:59:59+03:30"), ): - """ - Get users who have expired within the specified date range. + """Get users who have expired within the specified date range. - **expired_after** UTC datetime (optional) - **expired_before** UTC datetime (optional) - At least one of expired_after or expired_before must be provided for filtering - If both are omitted, returns all expired users """ - return await user_operator.get_expired_users(db, expired_after, expired_before, admin_username) @@ -278,15 +292,18 @@ async def delete_expired_users( expired_after: dt | None = Query(None, example="2024-01-01T00:00:00+03:30"), expired_before: dt | None = Query(None, example="2024-01-31T23:59:59+03:30"), ): - """ - Delete users who have expired within the specified date range. + """Delete users who have expired within the specified date range. - **expired_after** UTC datetime (optional) - **expired_before** UTC datetime (optional) - At least one of expired_after or expired_before must be provided """ return await user_operator.delete_expired_users( - db, admin, expired_after, expired_before, admin_username=admin_username + db=db, + admin=admin, + expired_after=expired_after, + expired_before=expired_before, + admin_username=admin_username, ) @@ -309,14 +326,18 @@ async def modify_user_with_template( return await user_operator.modify_user_with_template(db, username, modify_template_user, admin) -@router.post("s/bulk/expire", summary="Bulk sum/sub to expire of users", response_description="Success confirmation") +@router.post( + "s/bulk/expire", + response_model=BulkOperationResponse, + summary="Bulk sum/sub to expire of users", + response_description="Success confirmation", +) async def bulk_modify_users_expire( bulk_model: BulkUser, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(check_sudo_admin), ): - """ - Bulk expire users based on the provided criteria. + """Bulk expire users based on the provided criteria. - **amount**: amount to adjust the user's quota (in seconds, positive to increase, negative to decrease) required - **user_ids**: Optional list of user IDs to modify @@ -328,15 +349,17 @@ async def bulk_modify_users_expire( @router.post( - "s/bulk/data_limit", summary="Bulk sum/sub to data limit of users", response_description="Success confirmation" + "s/bulk/data_limit", + response_model=BulkOperationResponse, + summary="Bulk sum/sub to data limit of users", + response_description="Success confirmation", ) async def bulk_modify_users_datalimit( bulk_model: BulkUser, db: AsyncSession = Depends(get_db), _: AdminDetails = Depends(check_sudo_admin), ): - """ - Bulk modify users' data limit based on the provided criteria. + """Bulk modify users' data limit based on the provided criteria. - **amount**: amount to adjust the user's quota (positive to increase, negative to decrease) required - **user_ids**: Optional list of user IDs to modify @@ -348,7 +371,10 @@ async def bulk_modify_users_datalimit( @router.post( - "s/bulk/proxy_settings", summary="Bulk modify users proxy settings", response_description="Success confirmation" + "s/bulk/proxy_settings", + response_model=BulkOperationResponse, + summary="Bulk modify users proxy settings", + response_description="Success confirmation", ) async def bulk_modify_users_proxy_settings( bulk_model: BulkUsersProxy, diff --git a/app/utils/crypto.py b/app/utils/crypto.py index 76c645adf..26ea1a935 100644 --- a/app/utils/crypto.py +++ b/app/utils/crypto.py @@ -7,7 +7,7 @@ from cryptography.hazmat.primitives.asymmetric import x25519 -def generate_certificate(): #TODO: remove this fuction, migration needs +def generate_certificate(): # TODO: remove this fuction, migration needs return {"key": "dummy_key", "cert": "dummy_cert"} # Placeholder implementation From a8df5cfce5dad47407154692c5e76dfebc0820ff Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 13:30:35 +0330 Subject: [PATCH 4/7] chore: Add .ruff_cache to .dockerignore and .gitignore --- .dockerignore | 1 + .gitignore | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.dockerignore b/.dockerignore index 8c690bf3c..7bdf78b92 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,4 @@ venv* LICENSE install_service.sh Marzban.code-workspace +.ruff_cache \ No newline at end of file diff --git a/.gitignore b/.gitignore index c2945ec84..7e64d88fd 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,5 @@ xray-core #ignore node-module for dashboard node_modules/ deadlock-analysis.md + +.ruff_cache \ No newline at end of file From d3b36bc4e88d10988415e6a467f2eb80c778ae0f Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 13:30:44 +0330 Subject: [PATCH 5/7] refactor(ci): Simplify Ruff check workflow by removing Python setup and installation steps --- .github/workflows/ruff-check.yml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ruff-check.yml b/.github/workflows/ruff-check.yml index 9d7b57dd2..309f7cef7 100644 --- a/.github/workflows/ruff-check.yml +++ b/.github/workflows/ruff-check.yml @@ -18,16 +18,11 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 + - name: Run Ruff check + uses: astral-sh/ruff-action@v3 with: - python-version: "3.12" - - - name: Install Ruff - run: | - python -m pip install --upgrade pip - pip install ruff - - name: Run Ruff Format - run: | - ruff check --output-format=github . + args: check --respect-gitignore --config pyproject.toml --output-format=github . + - name: Run Ruff format + uses: astral-sh/ruff-action@v3 + with: + args: format --respect-gitignore --config pyproject.toml . From c3049753b4cb03d30563c50eb00ba9022ca35cfb Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 13:33:23 +0330 Subject: [PATCH 6/7] fix: Correctly specify app argument in uvicorn.run call --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 1f8e527af..7d03de1f0 100644 --- a/main.py +++ b/main.py @@ -135,7 +135,7 @@ def validate_cert_and_key(cert_file_path, key_file_path): try: uvicorn.run( - "main:app", + app="main:app", **bind_args, workers=1, reload=DEBUG, From 8e75d87bb5e6cdbed10e4c381ee46cfc49fe5bfb Mon Sep 17 00:00:00 2001 From: arian Date: Sat, 4 Oct 2025 13:36:02 +0330 Subject: [PATCH 7/7] refactor(ci): Remove redundant Ruff format step from workflow --- .github/workflows/ruff-check.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ruff-check.yml b/.github/workflows/ruff-check.yml index 309f7cef7..37b3fdfe3 100644 --- a/.github/workflows/ruff-check.yml +++ b/.github/workflows/ruff-check.yml @@ -22,7 +22,3 @@ jobs: uses: astral-sh/ruff-action@v3 with: args: check --respect-gitignore --config pyproject.toml --output-format=github . - - name: Run Ruff format - uses: astral-sh/ruff-action@v3 - with: - args: format --respect-gitignore --config pyproject.toml .