Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from app.scheduler import scheduler
from app.version import __version__


__all__ = [
"__version__",
"create_app",
Expand Down
5 changes: 2 additions & 3 deletions app/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from sqlalchemy.exc import DBAPIError

from app.lifecycle import on_shutdown, on_startup
from app.nats import is_nats_enabled
from app.middlewares import setup_middleware
from app.nats import is_nats_enabled
from app.nats.message import MessageTopic
from app.nats.router import router
from app.settings import handle_settings_message
Expand All @@ -16,7 +16,6 @@
from app.version import __version__
from config import runtime_settings, subscription_env_settings


logger = get_logger("app-factory")


Expand Down Expand Up @@ -164,7 +163,7 @@ def validation_exception_handler(request: Request, exc: RequestValidationError):

app.add_exception_handler(DBAPIError, database_operational_error_handler)

from app.operation.permissions import LimitExceeded, PermissionDenied # noqa: F401
from app.operation.permissions import LimitExceeded, PermissionDenied

@app.exception_handler(PermissionDenied)
async def permission_denied_handler(request: Request, exc: PermissionDenied):
Expand Down
2 changes: 1 addition & 1 deletion app/core/abstract_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def to_json(self) -> dict:

@classmethod
@abstractmethod
def from_json(cls, data: dict) -> "AbstractCore":
def from_json(cls, data: dict) -> AbstractCore:
"""Reconstruct the core config from a dictionary."""
raise NotImplementedError

Expand Down
3 changes: 2 additions & 1 deletion app/core/manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from asyncio import Lock
from copy import deepcopy
from typing import ClassVar

import nats
from aiocache import cached
Expand All @@ -26,7 +27,7 @@
class CoreManager:
STATE_CACHE_KEY = "state"
KV_BUCKET_NAME = "core_manager_state"
CORE_CLASSES = {
CORE_CLASSES: ClassVar[dict] = {
CoreType.xray: XRayConfig,
CoreType.wg: WireGuardConfig,
}
Expand Down
7 changes: 3 additions & 4 deletions app/core/wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from copy import deepcopy
from ipaddress import ip_interface
from pathlib import PosixPath
from typing import Union

import commentjson

Expand All @@ -20,7 +19,7 @@
class WireGuardConfig(dict):
def __init__(
self,
config: Union[dict, str, PosixPath] | None = None,
config: dict | str | PosixPath | None = None,
exclude_inbound_tags: set[str] | None = None,
fallbacks_inbound_tags: set[str] | None = None,
skip_validation: bool = False,
Expand Down Expand Up @@ -83,7 +82,7 @@ def _validate(self):

addresses = self.get("address")
if not isinstance(addresses, list):
raise ValueError("address must be a list")
raise TypeError("address must be a list")

normalized_addresses: list[str] = []
for cidr in addresses:
Expand Down Expand Up @@ -135,7 +134,7 @@ def to_json(self) -> dict:
}

@classmethod
def from_json(cls, data: dict) -> "WireGuardConfig":
def from_json(cls, data: dict) -> WireGuardConfig:
instance = cls(config=data.get("config", {}), skip_validation=True)
if "inbounds" in data:
instance._inbounds = data["inbounds"]
Expand Down
11 changes: 5 additions & 6 deletions app/core/xray.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import json
from copy import deepcopy
from pathlib import PosixPath
from typing import Union

import commentjson

Expand All @@ -24,7 +23,7 @@ def _protocols_from_inbounds_by_tag(inbounds_by_tag: dict[str, dict]) -> frozens
class XRayConfig(dict):
def __init__(
self,
config: Union[dict, str, PosixPath] | None = None,
config: dict | str | PosixPath | None = None,
exclude_inbound_tags: set[str] | None = None,
fallbacks_inbound_tags: set[str] | None = None,
skip_validation: bool = False,
Expand Down Expand Up @@ -150,7 +149,7 @@ def _create_base_settings(self, inbound: dict) -> dict:
def _is_unix_socket(inbound: dict) -> bool:
"""Return True if the inbound listens on a Unix domain socket instead of a TCP/UDP port."""
listen = inbound.get("listen", "")
return isinstance(listen, str) and (listen.startswith("/") or listen.startswith("@"))
return isinstance(listen, str) and (listen.startswith(("/", "@")))

def _handle_port_settings(self, inbound: dict, settings: dict):
"""Handle port settings for an inbound."""
Expand Down Expand Up @@ -254,7 +253,7 @@ def _handle_tcp_raw_settings(self, net_settings: dict, settings: dict, inbound_t
settings["header_type"] = header.get("type", "none")

if isinstance(path, str) or isinstance(host, str):
raise ValueError(
raise TypeError(
f"Settings of {inbound_tag} for path and host must be list, not str\n"
"https://xtls.github.io/config/transports/tcp.html#httpheaderobject"
)
Expand All @@ -273,7 +272,7 @@ def _handle_ws_settings(self, net_settings: dict, settings: dict, inbound_tag: s
settings["header_type"] = ""

if isinstance(path, list) or isinstance(host, list):
raise ValueError(
raise TypeError(
"Settings for path and host must be str, not list\n"
"https://xtls.github.io/config/transports/websocket.html#websocketobject"
)
Expand Down Expand Up @@ -547,7 +546,7 @@ def to_json(self) -> dict:
}

@classmethod
def from_json(cls, data: dict) -> "XRayConfig":
def from_json(cls, data: dict) -> XRayConfig:
"""Reconstruct the config from a dictionary."""
fallback_tags = data.get("fallbacks_inbound_tags")
if fallback_tags is None:
Expand Down
16 changes: 7 additions & 9 deletions app/db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from sqlalchemy.ext.asyncio import AsyncSession

from .base import Base, GetDB, get_db # noqa


from .models import JWT, System, User # noqa
from .base import Base, GetDB, get_db
from .models import JWT, System, User

__all__ = [
"GetDB",
"get_db",
"User",
"System",
"JWT",
"Base",
"AsyncSession",
"Base",
"GetDB",
"System",
"User",
"get_db",
]
2 changes: 1 addition & 1 deletion app/db/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sqlalchemy import MetaData
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
from sqlalchemy import MetaData

from config import database_settings

Expand Down
12 changes: 6 additions & 6 deletions app/db/compiles_types.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from sqlalchemy import BigInteger, String, Numeric, TypeDecorator
from sqlalchemy.sql.expression import FunctionElement
from sqlalchemy import BigInteger, Numeric, String, TypeDecorator
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import FunctionElement


class CaseSensitiveString(String):
def __init__(self, length=None):
super(CaseSensitiveString, self).__init__(length)
super().__init__(length)


class SqliteCompatibleBigInteger(BigInteger):
Expand Down Expand Up @@ -39,7 +39,7 @@ class EnumArray(TypeDecorator):
impl = String

def __init__(self, enum_cls, length=255):
super(EnumArray, self).__init__(length=length)
super().__init__(length=length)
self.enum_cls = enum_cls

def process_bind_param(self, value, dialect):
Expand All @@ -63,7 +63,7 @@ class StringArray(TypeDecorator):
impl = String

def __init__(self, length=255, **kwargs):
super(StringArray, self).__init__(length=length, **kwargs)
super().__init__(length=length, **kwargs)

def process_bind_param(self, value, dialect):
"""Convert list to a comma-separated string for storage."""
Expand All @@ -76,7 +76,7 @@ def process_result_value(self, value, dialect):
if value is None:
return set()
if isinstance(value, str):
return set(v for v in value.split(",") if v)
return {v for v in value.split(",") if v}
return set(value)


Expand Down
5 changes: 2 additions & 3 deletions app/db/crud/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
from .admin import get_admin
from .core import get_core_config_by_id
from .client_template import get_client_template_by_id
from .core import get_core_config_by_id
from .group import get_group_by_id
from .host import get_host_by_id
from .node import get_node_by_id
from .user import get_user
from .user_template import get_user_template


__all__ = [
"get_admin",
"get_core_config_by_id",
"get_client_template_by_id",
"get_core_config_by_id",
"get_group_by_id",
"get_host_by_id",
"get_node_by_id",
Expand Down
21 changes: 10 additions & 11 deletions app/db/crud/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime

from sqlalchemy import and_, case, delete, func, not_, select, update
from sqlalchemy.exc import InvalidRequestError
Expand All @@ -13,11 +13,11 @@
to_utc_for_filter,
)
from app.db.models import (
APIKey,
Admin,
AdminNotificationReminder,
AdminRole,
AdminUsageLogs,
APIKey,
NodeUserUsage,
ReminderType,
User,
Expand Down Expand Up @@ -187,10 +187,9 @@ async def update_admin(db: AsyncSession, db_admin: Admin, modified_admin: AdminM
Returns:
Admin: The updated admin object.
"""
if modified_admin.status is not None:
if modified_admin.status != db_admin.status:
db_admin.status = modified_admin.status
db_admin.last_status_change = datetime.now(timezone.utc)
if modified_admin.status is not None and modified_admin.status != db_admin.status:
db_admin.status = modified_admin.status
db_admin.last_status_change = datetime.now(UTC)
if modified_admin.data_limit is not None:
db_admin.data_limit = modified_admin.data_limit if modified_admin.data_limit > 0 else None
# Recompute limited/active based on new data_limit — never touch disabled
Expand All @@ -203,10 +202,10 @@ async def update_admin(db: AsyncSession, db_admin: Admin, modified_admin: AdminM
new_status = AdminStatus.limited if should_be_limited else AdminStatus.active
if db_admin.status != new_status:
db_admin.status = new_status
db_admin.last_status_change = datetime.now(timezone.utc)
db_admin.last_status_change = datetime.now(UTC)
if modified_admin.password is not None:
db_admin.hashed_password = await hash_password(modified_admin.password)
db_admin.password_reset_at = datetime.now(timezone.utc)
db_admin.password_reset_at = datetime.now(UTC)
if modified_admin.role_id is not None:
db_admin.role_id = modified_admin.role_id
if modified_admin.permission_overrides is not None:
Expand Down Expand Up @@ -615,7 +614,7 @@ async def update_admin_status(db: AsyncSession, db_admin: Admin, new_status: Adm
Admin: The updated admin object.
"""
db_admin.status = new_status
db_admin.last_status_change = datetime.now(timezone.utc)
db_admin.last_status_change = datetime.now(UTC)
await db.commit()
await db.refresh(db_admin)
await load_admin_attrs(db_admin)
Expand Down Expand Up @@ -644,7 +643,7 @@ async def reset_admin_usage(db: AsyncSession, db_admin: Admin) -> Admin:
# After reset, used_traffic = 0 so the admin is no longer limited
if db_admin.status == AdminStatus.limited:
db_admin.status = AdminStatus.active
db_admin.last_status_change = datetime.now(timezone.utc)
db_admin.last_status_change = datetime.now(UTC)

await db.commit()
await db.refresh(db_admin)
Expand Down Expand Up @@ -743,7 +742,7 @@ async def get_admin_usages(
async def update_owner_password(db: AsyncSession, owner: Admin, new_password: str) -> Admin:
"""Reset the owner's password. All DB work stays in the CRUD layer."""
owner.hashed_password = await hash_password(new_password)
owner.password_reset_at = datetime.now(timezone.utc)
owner.password_reset_at = datetime.now(UTC)
await db.commit()
await db.refresh(owner)
await load_admin_attrs(owner)
Expand Down
4 changes: 2 additions & 2 deletions app/db/crud/api_key.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import uuid
from datetime import datetime as dt, timezone as tz
from datetime import UTC, datetime as dt

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -110,7 +110,7 @@ async def revoke_api_key(db: AsyncSession, db_key: APIKey) -> tuple[str, APIKey]
raw_key = f"pg_key_{raw_uuid}"
db_key.key_hash = hash_api_key(raw_key)
db_key.api_key_trimmed = f"pg_key_{raw_uuid[:3]}***{raw_uuid[-3:]}"
db_key.revoked_at = dt.now(tz.utc)
db_key.revoked_at = dt.now(UTC)
db_key.status = APIKeyStatus.active
await db.flush()
await db.refresh(db_key)
Expand Down
13 changes: 6 additions & 7 deletions app/db/crud/bulk.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime as dt, timezone as tz
from typing import Optional
from datetime import UTC, datetime as dt

from sqlalchemy import and_, case, cast, delete, func, or_, select, text, update
from sqlalchemy.dialects.postgresql import JSONB
Expand All @@ -24,7 +23,7 @@

async def reset_all_users_data_usage(
db: AsyncSession,
admin: Optional[Admin] = None,
admin: Admin | None = None,
*,
clean_chart_data: bool = False,
):
Expand Down Expand Up @@ -81,7 +80,7 @@ async def disable_all_active_users(db: AsyncSession, admin: Admin | None = None)

await db.execute(
query.values(
{User.status: UserStatus.disabled, User.last_status_change: dt.now(tz.utc)},
{User.status: UserStatus.disabled, User.last_status_change: dt.now(UTC)},
)
)

Expand Down Expand Up @@ -111,12 +110,12 @@ async def activate_all_disabled_users(db: AsyncSession, admin: Admin | None = No

await db.execute(
query_for_on_hold_users.values(
{User.status: UserStatus.on_hold, User.last_status_change: dt.now(tz.utc)},
{User.status: UserStatus.on_hold, User.last_status_change: dt.now(UTC)},
)
)
await db.execute(
query_for_active_users.values(
{User.status: UserStatus.active, User.last_status_change: dt.now(tz.utc)},
{User.status: UserStatus.active, User.last_status_change: dt.now(UTC)},
)
)

Expand Down Expand Up @@ -308,7 +307,7 @@ async def update_users_expire(db: AsyncSession, bulk_model: BulkUser) -> tuple[l
).scalar_one_or_none() or 0
# Get database-specific datetime addition expression
new_expire = get_datetime_add_expression(db, User.expire, bulk_model.amount)
current_time = dt.now(tz.utc)
current_time = dt.now(UTC)

# First, get the users that will have status changes BEFORE updating
status_change_conditions = or_(
Expand Down
Loading