From 93987d7452c3b62cab6264f5759c6c5e929471b7 Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 17 Jan 2026 12:00:30 +0100 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D0=B5=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=B4=D0=B8=D1=84=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=D0=B0=D1=85=20=D0=B8=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- remnawave/controllers/bandwidthstats.py | 4 +-- remnawave/controllers/nodes.py | 12 +++++++- remnawave/controllers/system.py | 8 +++++ remnawave/models/__init__.py | 8 ++++- remnawave/models/nodes.py | 21 +++++++++++++ remnawave/models/subscription.py | 2 +- remnawave/models/subscriptions_settings.py | 3 +- remnawave/models/system.py | 36 ++++++++++++++++++++++ tests/test_bandwidthstats.py | 35 +++++++++++++-------- tests/utils.py | 6 ++++ 10 files changed, 117 insertions(+), 18 deletions(-) diff --git a/remnawave/controllers/bandwidthstats.py b/remnawave/controllers/bandwidthstats.py index 565af8e..392aa15 100644 --- a/remnawave/controllers/bandwidthstats.py +++ b/remnawave/controllers/bandwidthstats.py @@ -20,7 +20,7 @@ from remnawave.rapid import BaseController, get class BandWidthStatsController(BaseController): # ============ Legacy Endpoints (Deprecated) ============ - @get("/bandwidth-stats/users/{user_uuid}/legacy-old", response_class=GetUserUsageByRangeResponseDto) + @get("/bandwidth-stats/users/{userUuid}/legacy", response_class=GetUserUsageByRangeResponseDto) async def get_user_usage_legacy_old( self, user_uuid: Annotated[str, Path(description="UUID of the user", alias="userUuid")], @@ -30,7 +30,7 @@ class BandWidthStatsController(BaseController): """Get User Usage by Range (Legacy - Deprecated)""" ... - @get("/bandwidth-stats/nodes/{node_uuid}/users/legacy-old", response_class=GetNodeUserUsageByRangeResponseDto) + @get("/bandwidth-stats/nodes/{nodeUuid}/users/legacy", response_class=GetNodeUserUsageByRangeResponseDto) async def get_node_user_usage_legacy_old( self, node_uuid: Annotated[str, Path(description="UUID of the node", alias="nodeUuid")], diff --git a/remnawave/controllers/nodes.py b/remnawave/controllers/nodes.py index fb86663..12c1256 100644 --- a/remnawave/controllers/nodes.py +++ b/remnawave/controllers/nodes.py @@ -19,7 +19,9 @@ from remnawave.models import ( UpdateNodeResponseDto, RestartAllNodesRequestBodyDto, ResetNodeTrafficRequestDto, - ResetNodeTrafficResponseDto + ResetNodeTrafficResponseDto, + ProfileModificationRequestDto, + ProfileModificationResponseDto, ) from remnawave.rapid import BaseController, delete, get, patch, post @@ -110,4 +112,12 @@ class NodesController(BaseController): body: Annotated[ResetNodeTrafficRequestDto, PydanticBody()], ) -> ResetNodeTrafficResponseDto: """Reset Traffic All Nodes""" + ... + + @post("/nodes/bulk-actions/profile-modification", response_class=ProfileModificationResponseDto) + async def profile_modification( + self, + body: Annotated[ProfileModificationRequestDto, PydanticBody()], + ) -> ProfileModificationResponseDto: + """Modify Inbounds & Profile for many nodes""" ... \ No newline at end of file diff --git a/remnawave/controllers/system.py b/remnawave/controllers/system.py index f7a8aee..bf1e79f 100644 --- a/remnawave/controllers/system.py +++ b/remnawave/controllers/system.py @@ -11,11 +11,19 @@ from remnawave.models import ( EncryptHappCryptoLinkResponseDto, DebugSrrMatcherRequestDto, DebugSrrMatcherResponseDto, + GetMetadataResponseDto ) from remnawave.rapid import BaseController, get, post class SystemController(BaseController): + @get("/system/metadata", response_class=GetMetadataResponseDto) + async def get_metadata( + self, + ) -> GetMetadataResponseDto: + """Get Remnawave Information""" + ... + @get("/system/stats", response_class=GetStatsResponseDto) async def get_stats( self, diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 48a0bfb..94104d8 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -193,7 +193,9 @@ from .nodes import ( RestartAllNodesRequestDto, # Legacy alias, RestartAllNodesRequestBodyDto, ResetNodeTrafficRequestDto, - ResetNodeTrafficResponseDto + ResetNodeTrafficResponseDto, + ProfileModificationRequestDto, + ProfileModificationResponseDto, ) from .nodes_usage_history import ( GetNodeUserUsageByRangeResponseDto, @@ -263,6 +265,7 @@ from .system import ( DebugSrrMatcherResponseDto, EncryptHappCryptoLinkRequestDto, EncryptHappCryptoLinkResponseDto, + GetMetadataResponseDto ) from .users import ( # Request DTOs @@ -483,6 +486,8 @@ __all__ = [ "RestartAllNodesRequestBodyDto", "ResetNodeTrafficRequestDto", "ResetNodeTrafficResponseDto", + "ProfileModificationRequestDto", + "ProfileModificationResponseDto", # Hosts models "CreateHostRequestDto", "CreateHostResponseDto", @@ -570,6 +575,7 @@ __all__ = [ "DebugSrrMatcherResponseDto", "EncryptHappCryptoLinkRequestDto", "EncryptHappCryptoLinkResponseDto", + "GetMetadataResponseDto" # XRay config models "ConfigResponseDto", # Legacy alias "GetConfigResponseDto", diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 500530b..e8564f5 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -230,6 +230,27 @@ class ResetNodeTrafficRequestDto(BaseModel): class ResetNodeTrafficResponseDto(RestartEventResponse): pass +class ConfigProfileData(BaseModel): + """Config profile data for modification""" + active_config_profile_uuid: str = Field(alias="activeConfigProfileUuid") + active_inbounds: List[str] = Field(alias="activeInbounds", min_length=1) + + +class ProfileModificationRequestDto(BaseModel): + """Request to modify profiles for multiple nodes""" + uuids: List[str] = Field(min_length=1) + config_profile: ConfigProfileData = Field(alias="configProfile") + + +class ProfileModificationResponseData(BaseModel): + """Profile modification response data""" + event_sent: bool = Field(alias="eventSent") + + +class ProfileModificationResponseDto(ProfileModificationResponseData): + """Profile modification response""" + pass + # Для обратной совместимости RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto NodesResponseDto = NodeResponseDto \ No newline at end of file diff --git a/remnawave/models/subscription.py b/remnawave/models/subscription.py index 28a4686..d75878d 100644 --- a/remnawave/models/subscription.py +++ b/remnawave/models/subscription.py @@ -157,7 +157,7 @@ class RawHost(BaseModel): mldsa65_verify: Optional[str] = Field(None, alias="mldsa65Verify") encryption: Optional[str] = None protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions") - db_data: RawHostDbData = Field(alias="dbData") + db_data: Optional[RawHostDbData] = Field(None, alias="dbData") xray_json_template: Optional[Dict[str, Any]] = Field(None, alias="xrayJsonTemplate") diff --git a/remnawave/models/subscriptions_settings.py b/remnawave/models/subscriptions_settings.py index 94bc0c5..bc4f584 100644 --- a/remnawave/models/subscriptions_settings.py +++ b/remnawave/models/subscriptions_settings.py @@ -61,7 +61,8 @@ class CustomRemarksDto(BaseModel): limited_users: List[str] = Field(alias="limitedUsers", min_length=1) disabled_users: List[str] = Field(alias="disabledUsers", min_length=1) empty_hosts: List[str] = Field(alias="emptyHosts", min_length=1) - empty_internal_squads: List[str] = Field(alias="emptyInternalSquads", min_length=1) + hwid_max_devices_exceeded: List[str] = Field(alias="HWIDMaxDevicesExceeded", min_length=1) + hwid_not_supported: List[str] = Field(alias="HWIDNotSupported", min_length=1) class HwidSettingsDto(BaseModel): diff --git a/remnawave/models/system.py b/remnawave/models/system.py index 7e085ad..323fcd2 100644 --- a/remnawave/models/system.py +++ b/remnawave/models/system.py @@ -171,4 +171,40 @@ class DebugSrrMatcherData(BaseModel): class DebugSrrMatcherResponseDto(DebugSrrMatcherData): + pass + +class BuildInfo(BaseModel): + """Build information""" + time: str + number: str + + +class GitBackendInfo(BaseModel): + """Git backend information""" + commit_sha: str = Field(alias="commitSha") + branch: str + commit_url: str = Field(alias="commitUrl") + + +class GitFrontendInfo(BaseModel): + """Git frontend information""" + commit_sha: str = Field(alias="commitSha") + commit_url: str = Field(alias="commitUrl") + + +class GitInfo(BaseModel): + """Git information""" + backend: GitBackendInfo + frontend: GitFrontendInfo + + +class MetadataResponse(BaseModel): + """Metadata response data""" + version: str + build: BuildInfo + git: GitInfo + + +class GetMetadataResponseDto(MetadataResponse): + """Get metadata response""" pass \ No newline at end of file diff --git a/tests/test_bandwidthstats.py b/tests/test_bandwidthstats.py index a4df3b2..305be37 100644 --- a/tests/test_bandwidthstats.py +++ b/tests/test_bandwidthstats.py @@ -16,7 +16,7 @@ from remnawave.models import ( GetStatsNodeUsersUsageResponseDto, GetStatsUserUsageResponseDto, ) -from tests.utils import generate_isoformat_range +from tests.utils import generate_date_range, generate_isoformat_range @pytest.mark.asyncio async def test_legacy_user_usage(remnawave): @@ -27,15 +27,19 @@ async def test_legacy_user_usage(remnawave): pytest.skip("No users available for testing") user_uuid = str(users.users[0].uuid) - start, end = generate_isoformat_range() + start, end = generate_date_range() user_usage = await remnawave.bandwidthstats.get_user_usage_legacy_old( user_uuid=user_uuid, start=start, end=end ) - assert isinstance(user_usage, GetUserUsageByRangeResponseDto) - assert len(user_usage) >= 0 + assert hasattr(user_usage, 'root') + assert isinstance(user_usage.root, list) + if user_usage.root: + first_item = user_usage.root[0] + assert hasattr(first_item, 'user_uuid') + assert hasattr(first_item, 'node_uuid') @pytest.mark.asyncio @@ -47,14 +51,21 @@ async def test_legacy_node_user_usage(remnawave): pytest.skip("No nodes available for testing") node_uuid = str(nodes[0].uuid) - start, end = generate_isoformat_range() + start, end = generate_date_range() node_user_usage = await remnawave.bandwidthstats.get_node_user_usage_legacy_old( node_uuid=node_uuid, start=start, end=end ) - assert isinstance(node_user_usage, GetNodeUserUsageByRangeResponseDto) + assert hasattr(node_user_usage, 'root') + assert isinstance(node_user_usage.root, list) + if node_user_usage.root: + first_item = node_user_usage.root[0] + assert hasattr(first_item, 'user_uuid') + assert hasattr(first_item, 'username') + assert hasattr(first_item, 'node_uuid') + assert hasattr(first_item, 'total') assert len(node_user_usage) >= 0 @@ -79,7 +90,7 @@ async def test_stats_nodes_realtime_usage(remnawave): @pytest.mark.asyncio async def test_stats_nodes_usage(remnawave): """Test new stats nodes usage endpoint with charts""" - start, end = generate_isoformat_range() + start, end = generate_date_range() nodes_usage = await remnawave.bandwidthstats.get_stats_nodes_usage( start=start, @@ -109,7 +120,7 @@ async def test_stats_node_users_usage(remnawave): pytest.skip("No nodes available for testing") node_uuid = str(nodes[0].uuid) - start, end = generate_isoformat_range() + start, end = generate_date_range() node_users_usage = await remnawave.bandwidthstats.get_stats_node_users_usage( uuid=node_uuid, @@ -138,7 +149,7 @@ async def test_stats_user_usage(remnawave): pytest.skip("No users available for testing") user_uuid = str(users.users[0].uuid) - start, end = generate_isoformat_range() + start, end = generate_date_range() user_usage = await remnawave.bandwidthstats.get_stats_user_usage( uuid=user_uuid, @@ -169,7 +180,7 @@ async def test_legacy_stats_user_usage(remnawave): pytest.skip("No users available for testing") user_uuid = str(users.users[0].uuid) - start, end = generate_isoformat_range() + start, end = generate_date_range() legacy_user_usage = await remnawave.bandwidthstats.get_user_usage_legacy_stats( uuid=user_uuid, @@ -198,7 +209,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave): pytest.skip("No nodes available for testing") node_uuid = str(nodes[0].uuid) - start, end = generate_isoformat_range() + start, end = generate_date_range() legacy_node_users = await remnawave.bandwidthstats.get_node_users_usage_legacy_stats( uuid=node_uuid, @@ -220,7 +231,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave): @pytest.mark.asyncio async def test_bandwidth_data_structure(remnawave): """Test bandwidth stats data structure validity""" - start, end = generate_isoformat_range() + start, end = generate_date_range() # Get realtime data realtime = await remnawave.bandwidthstats.get_nodes_realtime_usage() diff --git a/tests/utils.py b/tests/utils.py index 11e7f13..a52b867 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -22,3 +22,9 @@ def generate_isoformat_range() -> Tuple[str, str]: start = (datetime.now() - timedelta(days=7)).isoformat(timespec="seconds") end = datetime.now().isoformat(timespec="seconds") return start, end + +def generate_date_range() -> tuple[str, str]: + """Generate date range in YYYY-MM-DD format for the past 7 days""" + end = datetime.now() + start = end - timedelta(days=7) + return start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d') \ No newline at end of file From 34595a5e5b94536cfac44b62c92ddb75f49edca9 Mon Sep 17 00:00:00 2001 From: masasibata Date: Tue, 10 Feb 2026 18:23:13 +0300 Subject: [PATCH 2/4] feat: add nodes bulk actions and revoke-only-passwords support --- remnawave/controllers/nodes.py | 12 +++++++++++- remnawave/enums/webhook.py | 1 + remnawave/models/__init__.py | 6 ++++++ remnawave/models/nodes.py | 18 ++++++++++++++++-- remnawave/models/users.py | 6 +++++- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/remnawave/controllers/nodes.py b/remnawave/controllers/nodes.py index 12c1256..1b99623 100644 --- a/remnawave/controllers/nodes.py +++ b/remnawave/controllers/nodes.py @@ -17,11 +17,13 @@ from remnawave.models import ( RestartNodeResponseDto, UpdateNodeRequestDto, UpdateNodeResponseDto, - RestartAllNodesRequestBodyDto, + RestartAllNodesRequestBodyDto, ResetNodeTrafficRequestDto, ResetNodeTrafficResponseDto, ProfileModificationRequestDto, ProfileModificationResponseDto, + NodesBulkActionsRequestDto, + NodesBulkActionsResponseDto, ) from remnawave.rapid import BaseController, delete, get, patch, post @@ -120,4 +122,12 @@ class NodesController(BaseController): body: Annotated[ProfileModificationRequestDto, PydanticBody()], ) -> ProfileModificationResponseDto: """Modify Inbounds & Profile for many nodes""" + ... + + @post("/nodes/bulk-actions", response_class=NodesBulkActionsResponseDto) + async def nodes_bulk_actions( + self, + body: Annotated[NodesBulkActionsRequestDto, PydanticBody()], + ) -> NodesBulkActionsResponseDto: + """Perform actions for many nodes (ENABLE, DISABLE, RESTART, RESET_TRAFFIC)""" ... \ No newline at end of file diff --git a/remnawave/enums/webhook.py b/remnawave/enums/webhook.py index 68c3a5c..1ab22f4 100644 --- a/remnawave/enums/webhook.py +++ b/remnawave/enums/webhook.py @@ -35,6 +35,7 @@ TServiceEvents = Literal[ "service.panel_started", "service.login_attempt_failed", "service.login_attempt_success", + "service.subpage_config_changed", ] TErrorsEvents = Literal[ diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 94104d8..c0a7ce2 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -196,6 +196,9 @@ from .nodes import ( ResetNodeTrafficResponseDto, ProfileModificationRequestDto, ProfileModificationResponseDto, + NodeBulkActionType, + NodesBulkActionsRequestDto, + NodesBulkActionsResponseDto, ) from .nodes_usage_history import ( GetNodeUserUsageByRangeResponseDto, @@ -488,6 +491,9 @@ __all__ = [ "ResetNodeTrafficResponseDto", "ProfileModificationRequestDto", "ProfileModificationResponseDto", + "NodeBulkActionType", + "NodesBulkActionsRequestDto", + "NodesBulkActionsResponseDto", # Hosts models "CreateHostRequestDto", "CreateHostResponseDto", diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index e8564f5..24e8bab 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Annotated, List, Optional, Union +from typing import Annotated, List, Optional, Union, Literal from uuid import UUID from pydantic import BaseModel, Field, StringConstraints, RootModel @@ -253,4 +253,18 @@ class ProfileModificationResponseDto(ProfileModificationResponseData): # Для обратной совместимости RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto -NodesResponseDto = NodeResponseDto \ No newline at end of file +NodesResponseDto = NodeResponseDto + + +NodeBulkActionType = Literal["ENABLE", "DISABLE", "RESTART", "RESET_TRAFFIC"] + + +class NodesBulkActionsRequestDto(BaseModel): + """Request for performing bulk actions on nodes""" + uuids: List[UUID] = Field(min_length=1) + action: NodeBulkActionType = Field(description="Action to perform on nodes") + + +class NodesBulkActionsResponseDto(BaseModel): + """Response after performing bulk actions on nodes""" + event_sent: bool = Field(alias="eventSent") \ No newline at end of file diff --git a/remnawave/models/users.py b/remnawave/models/users.py index 2e7ba56..80cc6b8 100644 --- a/remnawave/models/users.py +++ b/remnawave/models/users.py @@ -196,7 +196,11 @@ class RevokeUserRequestDto(BaseModel): max_length=48, pattern=r"^[a-zA-Z0-9_-]+$", ) - + revoke_only_passwords: Optional[bool] = Field( + None, + serialization_alias="revokeOnlyPasswords", + description="Optional. If true, only passwords will be revoked without changing the short UUID.", + ) class SubscriptionRequestRecord(BaseModel): """Subscription request history record""" From 6c9fa202ada58bb08b1600fa8338415bb7901177 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 18 Feb 2026 01:13:33 +0100 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E=20SDK?= =?UTF-8?q?=20=D0=B4=D0=BE=202.6.1=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D1=83=20=D1=82=D0=B5=D0=B3=D0=BE=D0=B2=20=D1=83?= =?UTF-8?q?=D0=B7=D0=BB=D0=BE=D0=B2=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B0=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + pyproject.toml | 4 +- remnawave/controllers/nodes.py | 8 ++++ remnawave/controllers/passkeys.py | 12 ++++- remnawave/enums/auth.py | 3 +- remnawave/models/__init__.py | 10 +++++ remnawave/models/config_profiles.py | 3 +- remnawave/models/external_squads.py | 1 + remnawave/models/hosts.py | 24 +++++----- remnawave/models/internal_squads.py | 9 ++-- remnawave/models/nodes.py | 16 +++++++ remnawave/models/passkeys.py | 13 +++++- remnawave/models/remnawave_settings.py | 51 ++++++++++++++++------ remnawave/models/subscription.py | 2 +- remnawave/models/subscription_page.py | 11 +++-- remnawave/models/subscriptions_template.py | 6 ++- 16 files changed, 133 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 5993d35..6e369b1 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ pip install git+https://github.com/remnawave/python-sdk.git@development | Contract Version | Remnawave Panel Version | | ---------------- | ----------------------- | +| 2.6.1 | >=2.6.0 | | 2.4.4 | >=2.4.0 | | 2.3.2 | >=2.3.0, <2.4.0 | | 2.3.0 | >=2.3.0, <2.3.2 | diff --git a/pyproject.toml b/pyproject.toml index e450fe4..b95c373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "remnawave" -version = "2.4.4" -description = "A Python SDK for interacting with the Remnawave API v2.4.4." +version = "2.6.1" +description = "A Python SDK for interacting with the Remnawave API v2.6.1." authors = [ {name = "Artem",email = "dev@forestsnet.com"} ] diff --git a/remnawave/controllers/nodes.py b/remnawave/controllers/nodes.py index 1b99623..746a42e 100644 --- a/remnawave/controllers/nodes.py +++ b/remnawave/controllers/nodes.py @@ -10,6 +10,7 @@ from remnawave.models import ( DisableNodeResponseDto, EnableNodeResponseDto, GetAllNodesResponseDto, + GetAllNodesTagsResponseDto, GetOneNodeResponseDto, ReorderNodeRequestDto, ReorderNodeResponseDto, @@ -29,6 +30,13 @@ from remnawave.rapid import BaseController, delete, get, patch, post class NodesController(BaseController): + @get("/nodes/tags", response_class=GetAllNodesTagsResponseDto) + async def get_all_nodes_tags( + self, + ) -> GetAllNodesTagsResponseDto: + """Get all nodes tags""" + ... + @post("/nodes", response_class=CreateNodeResponseDto) async def create_node( self, diff --git a/remnawave/controllers/passkeys.py b/remnawave/controllers/passkeys.py index 55e53db..82c88e9 100644 --- a/remnawave/controllers/passkeys.py +++ b/remnawave/controllers/passkeys.py @@ -7,10 +7,12 @@ from remnawave.models import ( DeletePasskeyResponseDto, GetAllPasskeysResponseDto, GetPasskeyRegistrationOptionsResponseDto, + UpdatePasskeyRequestDto, + UpdatePasskeyResponseDto, VerifyPasskeyRegistrationRequestDto, VerifyPasskeyRegistrationResponseDto, ) -from remnawave.rapid import BaseController, delete, get, post +from remnawave.rapid import BaseController, delete, get, patch, post class PasskeysController(BaseController): @@ -42,4 +44,12 @@ class PasskeysController(BaseController): body: Annotated[DeletePasskeyRequestDto, PydanticBody()], ) -> DeletePasskeyResponseDto: """Delete a passkey by ID""" + ... + + @patch("/passkeys", response_class=UpdatePasskeyResponseDto) + async def update_passkey( + self, + body: Annotated[UpdatePasskeyRequestDto, PydanticBody()], + ) -> UpdatePasskeyResponseDto: + """Update a passkey name""" ... \ No newline at end of file diff --git a/remnawave/enums/auth.py b/remnawave/enums/auth.py index cc18cd6..ac62047 100644 --- a/remnawave/enums/auth.py +++ b/remnawave/enums/auth.py @@ -4,4 +4,5 @@ class OAuth2Provider(StrEnum): """OAuth2 Provider enum""" GITHUB = "github" POCKETID = "pocketid" - YANDEX = "yandex" \ No newline at end of file + YANDEX = "yandex" + KEYCLOAK = "keycloak" \ No newline at end of file diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index c0a7ce2..126fcc4 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -179,6 +179,7 @@ from .nodes import ( EnableNodeResponseDto, ExcludedInbounds, GetAllNodesResponseDto, + GetAllNodesTagsResponseDto, GetOneNodeResponseDto, NodeConfigProfileDto, NodeConfigProfileRequestDto, @@ -388,6 +389,8 @@ from .passkeys import ( GetAllPasskeysResponseDto, GetPasskeyRegistrationOptionsResponseDto, PasskeyDto, + UpdatePasskeyRequestDto, + UpdatePasskeyResponseDto, VerifyPasskeyRegistrationRequestDto, VerifyPasskeyRegistrationResponseDto, ) @@ -422,6 +425,8 @@ from .remnawave_settings import ( BrandingSettings, GetRemnawaveSettingsResponseDto, GitHubOAuth2Settings, + GenericOAuth2Settings, + KeycloakOAuth2Settings, OAuth2Settings, PasskeySettings, PasswordSettings, @@ -474,6 +479,7 @@ __all__ = [ "EnableNodeResponseDto", "ExcludedInbounds", "GetAllNodesResponseDto", + "GetAllNodesTagsResponseDto", "GetOneNodeResponseDto", "NodeResponseDto", "NodesResponseDto", # Legacy alias @@ -815,6 +821,8 @@ __all__ = [ "GetAllPasskeysResponseDto", "GetPasskeyRegistrationOptionsResponseDto", "PasskeyDto", + "UpdatePasskeyRequestDto", + "UpdatePasskeyResponseDto", "VerifyPasskeyRegistrationRequestDto", "VerifyPasskeyRegistrationResponseDto", @@ -850,7 +858,9 @@ __all__ = [ "BrandingSettings", "GetRemnawaveSettingsResponseDto", + "GenericOAuth2Settings", "GitHubOAuth2Settings", + "KeycloakOAuth2Settings", "OAuth2Settings", "PasskeySettings", "PasswordSettings", diff --git a/remnawave/models/config_profiles.py b/remnawave/models/config_profiles.py index 46371fc..f3625ef 100644 --- a/remnawave/models/config_profiles.py +++ b/remnawave/models/config_profiles.py @@ -12,7 +12,7 @@ class InboundDto(BaseModel): type: str network: Optional[str] = None security: Optional[str] = None - port: Optional[int] = None + port: Optional[float] = None raw_inbound: Optional[Any] = Field(None, alias="rawInbound") class NodesProfileDto(BaseModel): @@ -23,6 +23,7 @@ class NodesProfileDto(BaseModel): class ConfigProfileDto(BaseModel): uuid: UUID name: str + view_position: int = Field(alias="viewPosition") config: Dict[str, Any] inbounds: List[InboundDto] nodes: List[NodesProfileDto] = [] diff --git a/remnawave/models/external_squads.py b/remnawave/models/external_squads.py index ee579d3..bae2d41 100644 --- a/remnawave/models/external_squads.py +++ b/remnawave/models/external_squads.py @@ -49,6 +49,7 @@ class ExternalSquadHostOverridesDto(BaseModel): class ExternalSquadDto(BaseModel): """External squad data model""" uuid: UUID + view_position: int = Field(alias="viewPosition") name: str info: ExternalSquadInfoDto templates: List[ExternalSquadTemplateDto] diff --git a/remnawave/models/hosts.py b/remnawave/models/hosts.py index f456f17..3795fef 100644 --- a/remnawave/models/hosts.py +++ b/remnawave/models/hosts.py @@ -65,18 +65,18 @@ class HostResponseDto(BaseModel): remark: str address: str port: int - path: Optional[str] = None - sni: Optional[str] = None - host: Optional[str] = None - alpn: Optional[str] = None - fingerprint: Optional[str] = None - x_http_extra_params: Optional[Dict[str, Any]] = Field(None, alias="xHttpExtraParams") - mux_params: Optional[Dict[str, Any]] = Field(None, alias="muxParams") - sockopt_params: Optional[Dict[str, Any]] = Field(None, alias="sockoptParams") + path: str | None = Field(alias="path") + sni: str | None = Field(alias="sni") + host: str | None = Field(alias="host") + alpn: str | None = Field(alias="alpn") + fingerprint: str | None = Field(alias="fingerprint") + x_http_extra_params: Dict[str, Any] | None = Field(alias="xHttpExtraParams") + mux_params: Dict[str, Any] | None = Field(alias="muxParams") + sockopt_params: Dict[str, Any] | None = Field(alias="sockoptParams") inbound: HostInboundData - server_description: Optional[str] = Field(None, alias="serverDescription") - tag: Optional[str] = None - vless_route_id: Optional[int] = Field(None, alias="vlessRouteId") + server_description: str | None = Field(alias="serverDescription") + tag: str | None = Field(alias="tag") + vless_route_id: int | None = Field(alias="vlessRouteId") shuffle_host: bool = Field(alias="shuffleHost") mihomo_x25519: bool = Field(alias="mihomoX25519") nodes: List[UUID] @@ -86,7 +86,7 @@ class HostResponseDto(BaseModel): override_sni_from_address: bool = Field(False, alias="overrideSniFromAddress") keep_blank_sni: bool = Field(False, alias="keepBlankSni") allow_insecure: bool = Field(False, alias="allowInsecure") - xray_json_template_uuid: Optional[UUID] = Field(None, alias="xrayJsonTemplateUuid") + xray_json_template_uuid: UUID | None = Field(alias="xrayJsonTemplateUuid") excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads") @property diff --git a/remnawave/models/internal_squads.py b/remnawave/models/internal_squads.py index 7b55a46..5034dcf 100644 --- a/remnawave/models/internal_squads.py +++ b/remnawave/models/internal_squads.py @@ -10,10 +10,10 @@ class InboundsDto(BaseModel): profile_uuid: UUID = Field(alias="profileUuid") tag: str type: str - network: Optional[str] = Field(default=None) - security: Optional[str] = Field(default=None) - port: Optional[float] = Field(default=None) - raw_inbound: Optional[dict] = Field(default=None, alias="rawInbound") + network: Optional[str] = None + security: Optional[str] = None + port: Optional[float] = None + raw_inbound: Optional[dict] = Field(None, alias="rawInbound") class InfoDto(BaseModel): @@ -23,6 +23,7 @@ class InfoDto(BaseModel): class InternalSquadDto(BaseModel): uuid: UUID + view_position: int = Field(alias="viewPosition") name: str info: Optional[InfoDto] = Field(default=None) inbounds: List[InboundsDto] = Field(default_factory=list) diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 24e8bab..9f2383d 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -28,6 +28,11 @@ class ReorderNodeItem(BaseModel): uuid: UUID +class GetAllNodesTagsResponseDto(BaseModel): + """Response with all nodes tags""" + tags: List[str] + + class NodeProviderDto(BaseModel): """Node provider information""" uuid: UUID @@ -79,6 +84,11 @@ class CreateNodeRequestDto(BaseModel): serialization_alias="configProfile" ) provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid") + tags: Optional[List[Annotated[str, StringConstraints(max_length=36, pattern=r'^[A-Z0-9_:]+$')]]] = Field( + None, + serialization_alias="tags", + max_length=10 + ) class UpdateNodeRequestDto(BaseModel): @@ -111,6 +121,11 @@ class UpdateNodeRequestDto(BaseModel): None, serialization_alias="configProfile" ) provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid") + tags: Optional[List[Annotated[str, StringConstraints(max_length=36, pattern=r'^[A-Z0-9_:]+$')]]] = Field( + None, + serialization_alias="tags", + max_length=10 + ) class ReorderNodeRequestDto(BaseModel): @@ -147,6 +162,7 @@ class NodeResponseDto(BaseModel): config_profile: NodeConfigProfileDto = Field(alias="configProfile") provider_uuid: Optional[UUID] = Field(None, alias="providerUuid") provider: Optional[NodeProviderDto] = None + tags: List[str] = Field(default_factory=list, alias="tags") class CreateNodeResponseDto(NodeResponseDto): diff --git a/remnawave/models/passkeys.py b/remnawave/models/passkeys.py index 45d6586..1f1725c 100644 --- a/remnawave/models/passkeys.py +++ b/remnawave/models/passkeys.py @@ -43,4 +43,15 @@ class DeletePasskeyRequestDto(BaseModel): class DeletePasskeyResponseDto(BaseModel): """Response with updated passkeys list after deletion""" - passkeys: List[PasskeyDto] \ No newline at end of file + passkeys: List[PasskeyDto] + + +class UpdatePasskeyRequestDto(BaseModel): + """Request to update a passkey""" + id: str + name: str + + +class UpdatePasskeyResponseDto(BaseModel): + """Response with updated passkey information""" + passkey: PasskeyDto \ No newline at end of file diff --git a/remnawave/models/remnawave_settings.py b/remnawave/models/remnawave_settings.py index 5f04010..76fcad4 100644 --- a/remnawave/models/remnawave_settings.py +++ b/remnawave/models/remnawave_settings.py @@ -6,32 +6,55 @@ from pydantic import BaseModel, Field, HttpUrl class PasskeySettings(BaseModel): """Passkey authentication settings""" enabled: bool - rp_id: Optional[str] = Field(None, alias="rpId") - origin: Optional[str] = None + rp_id: str | None = Field(alias="rpId") + origin: str | None class GitHubOAuth2Settings(BaseModel): """GitHub OAuth2 settings""" enabled: bool - client_id: Optional[str] = Field(None, alias="clientId") - client_secret: Optional[str] = Field(None, alias="clientSecret") + client_id: str | None = Field(alias="clientId") + client_secret: str | None = Field(alias="clientSecret") allowed_emails: List[str] = Field(alias="allowedEmails") class PocketIdOAuth2Settings(BaseModel): """PocketID OAuth2 settings""" enabled: bool - client_id: Optional[str] = Field(None, alias="clientId") - client_secret: Optional[str] = Field(None, alias="clientSecret") - plain_domain: Optional[str] = Field(None, alias="plainDomain") + client_id: str | None = Field(alias="clientId") + client_secret: str | None = Field(alias="clientSecret") + plain_domain: str | None = Field(alias="plainDomain") allowed_emails: List[str] = Field(alias="allowedEmails") class YandexOAuth2Settings(BaseModel): """Yandex OAuth2 settings""" enabled: bool - client_id: Optional[str] = Field(None, alias="clientId") - client_secret: Optional[str] = Field(None, alias="clientSecret") + client_id: str | None = Field(alias="clientId") + client_secret: str | None = Field(alias="clientSecret") + allowed_emails: List[str] = Field(alias="allowedEmails") + + +class KeycloakOAuth2Settings(BaseModel): + """Keycloak OAuth2 settings""" + enabled: bool + realm: str | None + client_id: str | None = Field(alias="clientId") + client_secret: str | None = Field(alias="clientSecret") + frontend_domain: str | None = Field(alias="frontendDomain") + keycloak_domain: str | None = Field(alias="keycloakDomain") + allowed_emails: List[str] = Field(alias="allowedEmails") + + +class GenericOAuth2Settings(BaseModel): + """Generic OAuth2 settings""" + enabled: bool + client_id: str | None = Field(alias="clientId") + client_secret: str | None = Field(alias="clientSecret") + with_pkce: bool = Field(alias="withPkce") + authorization_url: str | None = Field(alias="authorizationUrl") + token_url: str | None = Field(alias="tokenUrl") + frontend_domain: str | None = Field(alias="frontendDomain") allowed_emails: List[str] = Field(alias="allowedEmails") @@ -40,12 +63,14 @@ class OAuth2Settings(BaseModel): github: GitHubOAuth2Settings pocketid: PocketIdOAuth2Settings yandex: YandexOAuth2Settings + keycloak: KeycloakOAuth2Settings + generic: GenericOAuth2Settings class TelegramAuthSettings(BaseModel): """Telegram authentication settings""" enabled: bool - bot_token: Optional[str] = Field(None, alias="botToken") + bot_token: str | None = Field(alias="botToken") admin_ids: List[str] = Field(alias="adminIds") @@ -62,9 +87,9 @@ class BrandingSettings(BaseModel): class RemnawaveSettingsData(BaseModel): """Remnawave settings data""" - passkey_settings: Optional[PasskeySettings] = Field(None, alias="passkeySettings") - oauth2_settings: Optional[OAuth2Settings] = Field(None, alias="oauth2Settings") - tg_auth_settings: Optional[TelegramAuthSettings] = Field(None, alias="tgAuthSettings") + passkey_settings: PasskeySettings | None = Field(alias="passkeySettings") + oauth2_settings: OAuth2Settings | None = Field(alias="oauth2Settings") + tg_auth_settings: TelegramAuthSettings | None = Field(alias="tgAuthSettings") password_settings: Optional[PasswordSettings] = Field(None, alias="passwordSettings") branding_settings: Optional[BrandingSettings] = Field(None, alias="brandingSettings") diff --git a/remnawave/models/subscription.py b/remnawave/models/subscription.py index d75878d..37a892f 100644 --- a/remnawave/models/subscription.py +++ b/remnawave/models/subscription.py @@ -182,7 +182,7 @@ class UserSubscription(BaseModel): lifetime_traffic_used: str = Field(alias="lifetimeTrafficUsed") traffic_used_bytes: str = Field(alias="trafficUsedBytes") traffic_limit_bytes: str = Field(alias="trafficLimitBytes") - lifetime_traffic_used_bytes: int = Field(alias="lifetimeTrafficUsedBytes") + lifetime_traffic_used_bytes: str = Field(alias="lifetimeTrafficUsedBytes") traffic_limit_strategy: TrafficLimitStrategy = Field(alias="trafficLimitStrategy") expires_at: datetime = Field(alias="expiresAt") user_status: UserStatus = Field(alias="userStatus") diff --git a/remnawave/models/subscription_page.py b/remnawave/models/subscription_page.py index 7d8ff73..eccc716 100644 --- a/remnawave/models/subscription_page.py +++ b/remnawave/models/subscription_page.py @@ -11,7 +11,7 @@ class SubscriptionPageConfigDto(BaseModel): uuid: UUID view_position: int = Field(alias="viewPosition") name: str - config: Optional[Any] = None + config: Any | None class GetSubscriptionPageConfigsData(BaseModel): @@ -25,9 +25,14 @@ class GetSubscriptionPageConfigsResponseDto(GetSubscriptionPageConfigsData): pass -class GetSubscriptionPageConfigResponseDto(SubscriptionPageConfigDto): +class GetSubscriptionPageConfigResponseDto(BaseModel): """Response with single subscription page config""" - pass + model_config = ConfigDict(populate_by_name=True) + + uuid: UUID + view_position: int = Field(alias="viewPosition") + name: str + config: Any class CreateSubscriptionPageConfigRequestDto(BaseModel): diff --git a/remnawave/models/subscriptions_template.py b/remnawave/models/subscriptions_template.py index 1ff1102..254d7b9 100644 --- a/remnawave/models/subscriptions_template.py +++ b/remnawave/models/subscriptions_template.py @@ -9,15 +9,17 @@ from remnawave.enums import TemplateType class TemplateResponseDto(BaseModel): uuid: UUID name: str + view_position: int = Field(alias="viewPosition") template_type: TemplateType = Field(alias="templateType") - template_json: Optional[Any] = Field(None, alias="templateJson") - encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml") + template_json: Any | None = Field(alias="templateJson") + encoded_template_yaml: str | None = Field(alias="encodedTemplateYaml") class TemplateInfoDto(BaseModel): """Template info without content - used in list responses""" uuid: UUID name: str + view_position: int = Field(alias="viewPosition") template_type: TemplateType = Field(alias="templateType") template_json: Optional[Any] = Field(None, alias="templateJson") encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml") From 32d99f3f2d77f5e2edceb3da81508408670cfe95 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 18 Feb 2026 01:36:31 +0100 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D1=83=20=D0=BF=D0=B5=D1=80=D0=B5=D1=83=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=8F=D0=B4=D0=BE=D1=87=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D0=B5=D0=B9,=20=D0=B2?= =?UTF-8?q?=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D1=85=20=D0=B8=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B5=D0=BD=D0=BD=D0=B8=D1=85=20=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D1=8F=D0=B4=D0=BE=D0=B2,=20=D0=B0=20=D1=82=D0=B0=D0=BA?= =?UTF-8?q?=D0=B6=D0=B5=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- remnawave/controllers/config_profiles.py | 10 ++++++ remnawave/controllers/external_squads.py | 9 ++++++ remnawave/controllers/internal_squads.py | 10 ++++++ remnawave/controllers/nodes.py | 8 +++++ .../controllers/subscriptions_controller.py | 19 +++++++++++- .../controllers/subscriptions_template.py | 9 ++++++ remnawave/models/__init__.py | 31 +++++++++++++++++++ remnawave/models/config_profiles.py | 13 ++++++++ remnawave/models/external_squads.py | 20 ++++++++++++ remnawave/models/internal_squads.py | 13 ++++++++ remnawave/models/nodes.py | 4 +++ remnawave/models/subscription_page.py | 24 +++++++++++++- remnawave/models/subscriptions_template.py | 13 ++++++++ test_imports.py | 20 ++++++++++++ tests/test_config_profiles.py | 18 +++++++++++ tests/test_internal_squads.py | 18 +++++++++++ tests/test_nodes.py | 5 +++ tests/test_subscription.py | 19 ++++++++++-- tests/test_subscriptions_template.py | 25 ++++++++++++++- 19 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 test_imports.py diff --git a/remnawave/controllers/config_profiles.py b/remnawave/controllers/config_profiles.py index 0dfb441..d3affe0 100644 --- a/remnawave/controllers/config_profiles.py +++ b/remnawave/controllers/config_profiles.py @@ -11,6 +11,8 @@ from remnawave.models import ( GetAllInboundsResponseDto, GetConfigProfileByUuidResponseDto, GetInboundsByProfileUuidResponseDto, + ReorderConfigProfilesRequestDto, + ReorderConfigProfilesResponseDto, UpdateConfigProfileRequestDto, UpdateConfigProfileResponseDto, ) @@ -68,6 +70,14 @@ class ConfigProfilesController(BaseController): """Delete config profile""" ... + @post("/config-profiles/actions/reorder", response_class=ReorderConfigProfilesResponseDto) + async def reorder_config_profiles( + self, + body: Annotated[ReorderConfigProfilesRequestDto, PydanticBody()], + ) -> ReorderConfigProfilesResponseDto: + """Reorder config profiles""" + ... + # Get computed config profile by uuid​ @get("/config-profiles/{uuid}/computed-config", response_class=GetConfigProfileByUuidResponseDto) async def get_computed_config_profile_by_uuid( diff --git a/remnawave/controllers/external_squads.py b/remnawave/controllers/external_squads.py index 0b62504..84a2e58 100644 --- a/remnawave/controllers/external_squads.py +++ b/remnawave/controllers/external_squads.py @@ -10,6 +10,8 @@ from remnawave.models import ( GetExternalSquadByUuidResponseDto, GetExternalSquadsResponseDto, RemoveUsersFromExternalSquadResponseDto, + ReorderExternalSquadsRequestDto, + ReorderExternalSquadsResponseDto, UpdateExternalSquadRequestDto, UpdateExternalSquadResponseDto, ) @@ -70,4 +72,11 @@ class ExternalSquadsController(BaseController): uuid: str, ) -> RemoveUsersFromExternalSquadResponseDto: """Delete users from external squad""" + ... + @post("/external-squads/actions/reorder", response_class=ReorderExternalSquadsResponseDto) + async def reorder_external_squads( + self, + body: Annotated[ReorderExternalSquadsRequestDto, PydanticBody()], + ) -> ReorderExternalSquadsResponseDto: + """Reorder external squads""" ... \ No newline at end of file diff --git a/remnawave/controllers/internal_squads.py b/remnawave/controllers/internal_squads.py index efbdf39..9767072 100644 --- a/remnawave/controllers/internal_squads.py +++ b/remnawave/controllers/internal_squads.py @@ -13,6 +13,8 @@ from remnawave.models import ( DeleteUsersFromInternalSquadResponseDto, GetAllInternalSquadsResponseDto, GetInternalSquadByUuidResponseDto, + ReorderInternalSquadsRequestDto, + ReorderInternalSquadsResponseDto, UpdateInternalSquadRequestDto, UpdateInternalSquadResponseDto, GetInternalSquadAccessibleNodesResponseDto, @@ -90,3 +92,11 @@ class InternalSquadsController(BaseController): ) -> GetInternalSquadAccessibleNodesResponseDto: """Get accessible nodes for internal squad""" ... + + @post("/internal-squads/actions/reorder", response_class=ReorderInternalSquadsResponseDto) + async def reorder_internal_squads( + self, + body: Annotated[ReorderInternalSquadsRequestDto, PydanticBody()], + ) -> ReorderInternalSquadsResponseDto: + """Reorder internal squads""" + ... diff --git a/remnawave/controllers/nodes.py b/remnawave/controllers/nodes.py index 746a42e..52a9836 100644 --- a/remnawave/controllers/nodes.py +++ b/remnawave/controllers/nodes.py @@ -115,6 +115,14 @@ class NodesController(BaseController): ) -> ReorderNodeResponseDto: """Reorder Nodes""" ... + + @post("/nodes/{uuid}/actions/reset-traffic", response_class=ResetNodeTrafficResponseDto) + async def reset_node_traffic( + self, + uuid: Annotated[str, Path(description="UUID of the node")], + ) -> ResetNodeTrafficResponseDto: + """Reset traffic for individual node""" + ... @post("/nodes/actions/reset-traffic", response_class=ResetNodeTrafficResponseDto) async def reset_traffic_all_nodes( diff --git a/remnawave/controllers/subscriptions_controller.py b/remnawave/controllers/subscriptions_controller.py index ff7b80d..270c114 100644 --- a/remnawave/controllers/subscriptions_controller.py +++ b/remnawave/controllers/subscriptions_controller.py @@ -1,11 +1,19 @@ from typing import Annotated from rapid_api_client import Path, Query +from rapid_api_client.annotations import PydanticBody from remnawave.enums import ClientType from remnawave.models.subscription import GetRawSubscriptionByShortUuidResponseDto from remnawave.rapid import BaseController, get -from remnawave.models import GetAllSubscriptionsResponseDto, GetSubscriptionByUsernameResponseDto, GetSubscriptionByShortUUIDResponseDto, GetSubscriptionByUUIDResponseDto +from remnawave.models import ( + GetAllSubscriptionsResponseDto, + GetSubscriptionByUsernameResponseDto, + GetSubscriptionByShortUUIDResponseDto, + GetSubscriptionByUUIDResponseDto, + GetSubpageConfigByShortUuidRequestBodyDto, + GetSubpageConfigByShortUuidResponseDto, +) class SubscriptionsController(BaseController): @@ -47,6 +55,15 @@ class SubscriptionsController(BaseController): """None""" ... + @get("/subscriptions/subpage-config/{short_uuid}", response_class=GetSubpageConfigByShortUuidResponseDto) + async def get_subpage_config( + self, + short_uuid: Annotated[str, Path(description="Short UUID of the subscription")], + body: Annotated[GetSubpageConfigByShortUuidRequestBodyDto, PydanticBody()], + ) -> GetSubpageConfigByShortUuidResponseDto: + """Get subscription page config by short UUID""" + ... + @get("/subscriptions/by-short-uuid/{short_uuid}/raw", response_class=GetRawSubscriptionByShortUuidResponseDto) async def get_raw_subscription( self, diff --git a/remnawave/controllers/subscriptions_template.py b/remnawave/controllers/subscriptions_template.py index 72d497f..3c9baef 100644 --- a/remnawave/controllers/subscriptions_template.py +++ b/remnawave/controllers/subscriptions_template.py @@ -9,6 +9,8 @@ from remnawave.models import ( DeleteSubscriptionTemplateResponseDto, GetTemplateResponseDto, GetTemplatesResponseDto, + ReorderSubscriptionTemplatesRequestDto, + ReorderSubscriptionTemplatesResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto, ) @@ -51,4 +53,11 @@ class SubscriptionsTemplateController(BaseController): uuid: Annotated[str, Path(description="Template UUID")], ) -> DeleteSubscriptionTemplateResponseDto: """Delete subscription template""" + ... + @post("/subscription-templates/actions/reorder", response_class=ReorderSubscriptionTemplatesResponseDto) + async def reorder_templates( + self, + body: Annotated[ReorderSubscriptionTemplatesRequestDto, PydanticBody()], + ) -> ReorderSubscriptionTemplatesResponseDto: + """Reorder subscription templates""" ... \ No newline at end of file diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 126fcc4..d1f8328 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -61,6 +61,9 @@ from .config_profiles import ( GetInboundsByProfileUuidResponseDto, InboundDto, NodesProfileDto, + ReorderConfigProfileItem, + ReorderConfigProfilesRequestDto, + ReorderConfigProfilesResponseDto, UpdateConfigProfileRequestDto, UpdateConfigProfileResponseDto, ) @@ -166,6 +169,9 @@ from .internal_squads import ( GetAllInternalSquadsResponseDto, GetInternalSquadByUuidResponseDto, InternalSquadDto, + ReorderInternalSquadItem, + ReorderInternalSquadsRequestDto, + ReorderInternalSquadsResponseDto, UpdateInternalSquadRequestDto, UpdateInternalSquadResponseDto, GetInternalSquadAccessibleNodesResponseDto, @@ -243,6 +249,9 @@ from .subscriptions_template import ( GetTemplatesResponseDto, TemplateInfoDto, GetTemplateResponseDto, + ReorderTemplateItem, + ReorderSubscriptionTemplatesRequestDto, + ReorderSubscriptionTemplatesResponseDto, TemplateResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto, @@ -406,6 +415,9 @@ from .external_squads import ( GetExternalSquadByUuidResponseDto, GetExternalSquadsResponseDto, RemoveUsersFromExternalSquadResponseDto, + ReorderExternalSquadItem, + ReorderExternalSquadsRequestDto, + ReorderExternalSquadsResponseDto, TemplateType, UpdateExternalSquadRequestDto, UpdateExternalSquadResponseDto, @@ -445,6 +457,9 @@ from .subscription_page import ( DeleteSubscriptionPageConfigResponseDto, GetSubscriptionPageConfigResponseDto, GetSubscriptionPageConfigsResponseDto, + GetSubpageConfigByShortUuidRequestBodyDto, + GetSubpageConfigByShortUuidResponseDto, + SubpageConfigData, ReorderSubscriptionPageConfigItem, ReorderSubscriptionPageConfigsRequestDto, ReorderSubscriptionPageConfigsResponseDto, @@ -564,6 +579,9 @@ __all__ = [ "CreateSubscriptionTemplateResponseDto", "DeleteSubscriptionTemplateResponseDto", "GetTemplatesResponseDto", + "ReorderTemplateItem", + "ReorderSubscriptionTemplatesRequestDto", + "ReorderSubscriptionTemplatesResponseDto", "TemplateInfoDto", # System models "BandwidthStatistic", @@ -723,6 +741,9 @@ __all__ = [ "GetInboundsByProfileUuidResponseDto", "InboundDto", "NodesProfileDto", + "ReorderConfigProfileItem", + "ReorderConfigProfilesRequestDto", + "ReorderConfigProfilesResponseDto", "UpdateConfigProfileRequestDto", "UpdateConfigProfileResponseDto", "GetAllConfigProfilesResponsePaginated", @@ -766,8 +787,12 @@ __all__ = [ "GetAllInternalSquadsResponseDto", "GetInternalSquadByUuidResponseDto", "InternalSquadDto", + "ReorderInternalSquadItem", + "ReorderInternalSquadsRequestDto", + "ReorderInternalSquadsResponseDto", "UpdateInternalSquadRequestDto", "UpdateInternalSquadResponseDto", + "GetInternalSquadAccessibleNodesResponseDto", # Nodes usage history models "GetNodeUserUsageByRangeResponseDto", "GetNodesUsageByRangeResponseDto", @@ -838,6 +863,9 @@ __all__ = [ "GetExternalSquadByUuidResponseDto", "GetExternalSquadsResponseDto", "RemoveUsersFromExternalSquadResponseDto", + "ReorderExternalSquadItem", + "ReorderExternalSquadsRequestDto", + "ReorderExternalSquadsResponseDto", "TemplateType", "UpdateExternalSquadRequestDto", "UpdateExternalSquadResponseDto", @@ -879,6 +907,9 @@ __all__ = [ "DeleteSubscriptionPageConfigResponseDto", "GetSubscriptionPageConfigResponseDto", "GetSubscriptionPageConfigsResponseDto", + "GetSubpageConfigByShortUuidRequestBodyDto", + "GetSubpageConfigByShortUuidResponseDto", + "SubpageConfigData", "ReorderSubscriptionPageConfigItem", "ReorderSubscriptionPageConfigsRequestDto", "ReorderSubscriptionPageConfigsResponseDto", diff --git a/remnawave/models/config_profiles.py b/remnawave/models/config_profiles.py index f3625ef..e094c62 100644 --- a/remnawave/models/config_profiles.py +++ b/remnawave/models/config_profiles.py @@ -73,3 +73,16 @@ class GetAllInboundsResponseDto(List[InboundDto]): class GetInboundsByProfileUuidResponseDto(List[InboundDto]): pass + + +class ReorderConfigProfileItem(BaseModel): + view_position: int = Field(serialization_alias="viewPosition") + uuid: UUID + + +class ReorderConfigProfilesRequestDto(BaseModel): + items: List[ReorderConfigProfileItem] + + +class ReorderConfigProfilesResponseDto(GetAllConfigProfilesResponsePaginated): + pass diff --git a/remnawave/models/external_squads.py b/remnawave/models/external_squads.py index bae2d41..de9b020 100644 --- a/remnawave/models/external_squads.py +++ b/remnawave/models/external_squads.py @@ -106,6 +106,26 @@ class DeleteExternalSquadResponseDto(BaseModel): is_deleted: bool = Field(alias="isDeleted") +class ReorderExternalSquadItem(BaseModel): + view_position: int = Field(serialization_alias="viewPosition") + uuid: UUID + + +class ReorderExternalSquadsRequestDto(BaseModel): + items: List[ReorderExternalSquadItem] + + +class ReorderExternalSquadsResponseDto(BaseModel): + """Response after reordering external squads""" + total: int = Field(alias="total") + external_squads: List[ExternalSquadDto] = Field(alias="externalSquads") + + +class DeleteExternalSquadResponseDto(BaseModel): + """Response after deleting external squad""" + is_deleted: bool = Field(alias="isDeleted") + + class AddUsersToExternalSquadResponseDto(BaseModel): """Response after adding users to external squad""" event_sent: bool = Field(alias="eventSent") diff --git a/remnawave/models/internal_squads.py b/remnawave/models/internal_squads.py index 5034dcf..a00a440 100644 --- a/remnawave/models/internal_squads.py +++ b/remnawave/models/internal_squads.py @@ -101,3 +101,16 @@ class AccessibleNodeDto(BaseModel): class GetInternalSquadAccessibleNodesResponseDto(BaseModel): squad_uuid: UUID = Field(alias="squadUuid") accessible_nodes: List[AccessibleNodeDto] = Field(alias="accessibleNodes") + + +class ReorderInternalSquadItem(BaseModel): + view_position: int = Field(serialization_alias="viewPosition") + uuid: UUID + + +class ReorderInternalSquadsRequestDto(BaseModel): + items: List[ReorderInternalSquadItem] + + +class ReorderInternalSquadsResponseDto(GetAllInternalSquadsResponse): + pass diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 9f2383d..6d807bc 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -212,6 +212,10 @@ class RestartAllNodesResponseDto(BaseModel): event_sent: bool = Field(alias="eventSent") +class ResetNodeTrafficResponseDto(BaseModel): + event_sent: bool = Field(alias="eventSent") + + class ReorderNodeResponseDto(RootModel[List[NodeResponseDto]]): root: List[NodeResponseDto] diff --git a/remnawave/models/subscription_page.py b/remnawave/models/subscription_page.py index eccc716..5958abc 100644 --- a/remnawave/models/subscription_page.py +++ b/remnawave/models/subscription_page.py @@ -104,4 +104,26 @@ class CloneSubscriptionPageConfigRequestDto(BaseModel): class CloneSubscriptionPageConfigResponseDto(SubscriptionPageConfigDto): """Response after cloning subscription page config""" - pass \ No newline at end of file + pass + + +class GetSubpageConfigByShortUuidRequestBodyDto(BaseModel): + """Request body for getting subpage config by short UUID""" + model_config = ConfigDict(populate_by_name=True) + + request_headers: dict[str, str] = Field(default_factory=dict, serialization_alias="requestHeaders") + + +class SubpageConfigData(BaseModel): + """Data inside GetSubpageConfigByShortUuidResponseDto""" + model_config = ConfigDict(populate_by_name=True) + + subpage_config_uuid: UUID | None = Field(alias="subpageConfigUuid") + webpage_allowed: bool = Field(alias="webpageAllowed") + + +class GetSubpageConfigByShortUuidResponseDto(BaseModel): + """Response for getting subpage config by short UUID""" + model_config = ConfigDict(populate_by_name=True) + + response: SubpageConfigData \ No newline at end of file diff --git a/remnawave/models/subscriptions_template.py b/remnawave/models/subscriptions_template.py index 254d7b9..eaf43aa 100644 --- a/remnawave/models/subscriptions_template.py +++ b/remnawave/models/subscriptions_template.py @@ -65,6 +65,19 @@ class DeleteSubscriptionTemplateResponseDto(DeleteTemplateData): pass +class ReorderTemplateItem(BaseModel): + view_position: int = Field(serialization_alias="viewPosition") + uuid: UUID + + +class ReorderSubscriptionTemplatesRequestDto(BaseModel): + items: List[ReorderTemplateItem] + + +class ReorderSubscriptionTemplatesResponseDto(GetTemplatesData): + pass + + # Legacy aliases for backward compatibility class UpdateTemplateRequestDtoLegacy(BaseModel): template_type: TemplateType = Field(serialization_alias="templateType") diff --git a/test_imports.py b/test_imports.py new file mode 100644 index 0000000..fc5dcdd --- /dev/null +++ b/test_imports.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Quick test to verify all new imports work""" + +try: + from remnawave.models import ( + ReorderConfigProfilesRequestDto, + ReorderSubscriptionTemplatesRequestDto, + ReorderInternalSquadsRequestDto, + ReorderExternalSquadsRequestDto, + GetSubpageConfigByShortUuidResponseDto, + ) + print("✅ Все новые модели успешно импортируются!") + print(" - ReorderConfigProfilesRequestDto") + print(" - ReorderSubscriptionTemplatesRequestDto") + print(" - ReorderInternalSquadsRequestDto") + print(" - ReorderExternalSquadsRequestDto") + print(" - GetSubpageConfigByShortUuidResponseDto") +except ImportError as e: + print(f"❌ Ошибка импорта: {e}") + exit(1) diff --git a/tests/test_config_profiles.py b/tests/test_config_profiles.py index b48a8b9..0738f9f 100644 --- a/tests/test_config_profiles.py +++ b/tests/test_config_profiles.py @@ -8,6 +8,9 @@ from remnawave.models import ( GetAllInboundsResponseDto, GetConfigProfileByUuidResponseDto, GetInboundsByProfileUuidResponseDto, + ReorderConfigProfileItem, + ReorderConfigProfilesRequestDto, + ReorderConfigProfilesResponseDto, UpdateConfigProfileRequestDto, UpdateConfigProfileResponseDto, ) @@ -128,6 +131,21 @@ async def test_config_profiles(remnawave) -> None: inbounds_by_profile = await remnawave.config_profiles.get_inbounds_by_profile_uuid(profile_uuid) assert isinstance(inbounds_by_profile, GetInboundsByProfileUuidResponseDto) + # Test reorder config profiles + all_profiles_before_reorder = await remnawave.config_profiles.get_config_profiles() + if len(all_profiles_before_reorder.config_profiles) >= 2: + items = [ + ReorderConfigProfileItem( + uuid=profile.uuid, + view_position=idx + ) + for idx, profile in enumerate(all_profiles_before_reorder.config_profiles) + ] + reorder_result = await remnawave.config_profiles.reorder_config_profiles( + ReorderConfigProfilesRequestDto(items=items) + ) + assert isinstance(reorder_result, ReorderConfigProfilesResponseDto) + # Test delete config profile delete_profile = await remnawave.config_profiles.delete_config_profile_by_uuid(profile_uuid) assert isinstance(delete_profile, DeleteConfigProfileResponseDto) diff --git a/tests/test_internal_squads.py b/tests/test_internal_squads.py index c4f587c..a7970aa 100644 --- a/tests/test_internal_squads.py +++ b/tests/test_internal_squads.py @@ -11,6 +11,9 @@ from remnawave.models import ( DeleteUsersFromInternalSquadResponseDto, GetAllInternalSquadsResponseDto, GetInternalSquadByUuidResponseDto, + ReorderInternalSquadItem, + ReorderInternalSquadsRequestDto, + ReorderInternalSquadsResponseDto, UpdateInternalSquadRequestDto, UpdateInternalSquadResponseDto, ) @@ -69,6 +72,21 @@ async def test_internal_squads(remnawave) -> None: ) assert isinstance(remove_users, DeleteUsersFromInternalSquadResponseDto) + + # Test reorder internal squads + all_squads = await remnawave.internal_squads.get_internal_squads() + if len(all_squads.internal_squads) >= 2: + items = [ + ReorderInternalSquadItem( + uuid=squad.uuid, + view_position=idx + ) + for idx, squad in enumerate(all_squads.internal_squads) + ] + reorder_result = await remnawave.internal_squads.reorder_internal_squads( + ReorderInternalSquadsRequestDto(items=items) + ) + assert isinstance(reorder_result, ReorderInternalSquadsResponseDto) # Test delete internal squad delete_squad = await remnawave.internal_squads.delete_internal_squad(squad_uuid) diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 843ef95..64a8ea9 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -11,6 +11,7 @@ from remnawave.models import ( NodesResponseDto, ReorderNodeRequestDto, ReorderNodeResponseDto, + ResetNodeTrafficResponseDto, UpdateNodeRequestDto, ) from remnawave.models.nodes import ReorderNodeItem @@ -64,6 +65,10 @@ async def test_nodes(remnawave): assert update_node.uuid == create_node.uuid assert update_node.name == update_name + reset_traffic = await remnawave.nodes.reset_node_traffic(uuid=string_uuid) + assert isinstance(reset_traffic, ResetNodeTrafficResponseDto) + assert reset_traffic.event_sent is True + delete_node = await remnawave.nodes.delete_node(uuid=string_uuid) assert isinstance(delete_node, DeleteNodeResponseDto) assert delete_node.is_deleted is True diff --git a/tests/test_subscription.py b/tests/test_subscription.py index 9cb2f99..92ae5c5 100644 --- a/tests/test_subscription.py +++ b/tests/test_subscription.py @@ -6,7 +6,10 @@ from remnawave.models import ( GetSubscriptionInfoResponseDto, GetRawSubscriptionByShortUuidResponseDto, GetAllSubscriptionsResponseDto, - GetSubscriptionByUsernameResponseDto + GetSubscriptionByUsernameResponseDto, + GetSubpageConfigByShortUuidRequestBodyDto, + GetSubpageConfigByShortUuidResponseDto, + SubpageConfigData, ) from tests.conftest import REMNAWAVE_SHORT_UUID, REMNAWAVE_USER_USERNAME @@ -71,4 +74,16 @@ class TestSubscriptionsManagement: subscription_by_username = await remnawave.subscriptions.get_subscription_by_username( username=REMNAWAVE_USER_USERNAME ) - assert isinstance(subscription_by_username, GetSubscriptionByUsernameResponseDto) \ No newline at end of file + assert isinstance(subscription_by_username, GetSubscriptionByUsernameResponseDto) + + @pytest.mark.asyncio + async def test_get_subpage_config(self, remnawave): + """Тест получения конфига страницы подписки по short UUID""" + body = GetSubpageConfigByShortUuidRequestBodyDto(request_headers={}) + subpage_config = await remnawave.subscriptions.get_subpage_config( + short_uuid=REMNAWAVE_SHORT_UUID, + body=body, + ) + # Client auto-unwraps single "response" field → returns SubpageConfigData + assert isinstance(subpage_config, (GetSubpageConfigByShortUuidResponseDto, SubpageConfigData)) + assert hasattr(subpage_config, 'webpage_allowed') \ No newline at end of file diff --git a/tests/test_subscriptions_template.py b/tests/test_subscriptions_template.py index 3093bbc..250048c 100644 --- a/tests/test_subscriptions_template.py +++ b/tests/test_subscriptions_template.py @@ -6,6 +6,9 @@ from remnawave.models import ( DeleteSubscriptionTemplateResponseDto, GetTemplateResponseDto, GetTemplatesResponseDto, + ReorderTemplateItem, + ReorderSubscriptionTemplatesRequestDto, + ReorderSubscriptionTemplatesResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto, ) @@ -88,4 +91,24 @@ async def test_delete_template(remnawave): str(created.uuid) ) assert isinstance(delete_response, DeleteSubscriptionTemplateResponseDto) - assert delete_response.is_deleted is True \ No newline at end of file + assert delete_response.is_deleted is True + + +@pytest.mark.asyncio +async def test_reorder_templates(remnawave): + """Проверка изменения порядка шаблонов""" + templates = await remnawave.subscriptions_template.get_all_templates() + assert isinstance(templates, GetTemplatesResponseDto) + + if len(templates.templates) >= 2: + items = [ + ReorderTemplateItem( + uuid=tmpl.uuid, + view_position=idx + ) + for idx, tmpl in enumerate(templates.templates) + ] + reorder_result = await remnawave.subscriptions_template.reorder_templates( + ReorderSubscriptionTemplatesRequestDto(items=items) + ) + assert isinstance(reorder_result, ReorderSubscriptionTemplatesResponseDto) \ No newline at end of file