mirror of
https://github.com/remnawave/python-sdk.git
synced 2026-08-04 06:35:09 +00:00
Merge pull request #49 from remnawave:development
Introduce metadata management and enhance validation
This commit is contained in:
commit
bb337aa723
52 changed files with 2121 additions and 215 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -3,5 +3,7 @@
|
|||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python-envs.defaultEnvManager": "ms-python.python:poetry",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:poetry"
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
|
|||
|
||||
| Contract Version | Remnawave Panel Version |
|
||||
| ---------------- | ----------------------- |
|
||||
| 2.7.0 | >=2.7.0 |
|
||||
| 2.6.3 | >=2.6.3 |
|
||||
| 2.6.2 | >=2.6.2 |
|
||||
| 2.6.1 | >=2.6.0 |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[project]
|
||||
name = "remnawave"
|
||||
version = "2.6.3"
|
||||
description = "A Python SDK for interacting with the Remnawave API v2.6.3."
|
||||
version = "2.7.0"
|
||||
description = "A Python SDK for interacting with the Remnawave API v2.7.0."
|
||||
authors = [
|
||||
{name = "Artem",email = "dev@forestsnet.com"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ from remnawave.controllers import (
|
|||
RemnawaveSettingsController,
|
||||
SubscriptionPageConfigController,
|
||||
IpControlController,
|
||||
NodePluginsController,
|
||||
MetadataController,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -101,6 +103,8 @@ class RemnawaveSDK:
|
|||
self.remnawave_settings = RemnawaveSettingsController(self._client)
|
||||
self.subscription_page_config = SubscriptionPageConfigController(self._client)
|
||||
self.ip_control = IpControlController(self._client)
|
||||
self.node_plugins = NodePluginsController(self._client)
|
||||
self.metadata = MetadataController(self._client)
|
||||
|
||||
def _validate_params(self) -> None:
|
||||
if self._client is None:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ from .snippets import SnippetsController
|
|||
from .remnawave_settings import RemnawaveSettingsController
|
||||
from .subscription_page import SubscriptionPageConfigController
|
||||
from .ip_control import IpControlController
|
||||
from .node_plugins import NodePluginsController
|
||||
from .metadata import MetadataController
|
||||
|
||||
__all__ = [
|
||||
"APITokensManagementController",
|
||||
|
|
@ -58,4 +60,6 @@ __all__ = [
|
|||
"RemnawaveSettingsController",
|
||||
"SubscriptionPageConfigController",
|
||||
"IpControlController",
|
||||
"NodePluginsController",
|
||||
"MetadataController",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ class HWIDUserController(BaseController):
|
|||
"""Delete all user HWID devices"""
|
||||
...
|
||||
|
||||
@get("/hwid/devices/{uuid}", response_class=GetUserHwidDevicesResponseDto)
|
||||
@get("/hwid/devices/{userUuid}", response_class=GetUserHwidDevicesResponseDto)
|
||||
async def get_hwid_user(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the User")],
|
||||
uuid: Annotated[str, Path(description="UUID of the User", alias="userUuid")],
|
||||
) -> GetUserHwidDevicesResponseDto:
|
||||
"""Get a user HWID device"""
|
||||
...
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from remnawave.models import (
|
|||
DropConnectionsResponseDto,
|
||||
FetchIpsResponseDto,
|
||||
FetchIpsResultResponseDto,
|
||||
FetchUsersIpsResponseDto,
|
||||
FetchUsersIpsResultResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, get, post
|
||||
|
||||
|
|
@ -41,6 +43,33 @@ class IpControlController(BaseController):
|
|||
"""
|
||||
...
|
||||
|
||||
@post("/ip-control/fetch-users-ips/{nodeUuid}", response_class=FetchUsersIpsResponseDto)
|
||||
async def fetch_users_ips(
|
||||
self,
|
||||
nodeUuid: Annotated[str, Path(description="UUID of the node")],
|
||||
) -> FetchUsersIpsResponseDto:
|
||||
"""Request IP List for all users on a node.
|
||||
|
||||
Starts a background job that queries the specified node for the IPs
|
||||
of all connected users. The returned ``job_id`` must be passed to
|
||||
:meth:`get_fetch_users_ips_result` to retrieve the actual list once
|
||||
the job is complete.
|
||||
"""
|
||||
...
|
||||
|
||||
@get("/ip-control/fetch-users-ips/result/{jobId}", response_class=FetchUsersIpsResultResponseDto)
|
||||
async def get_fetch_users_ips_result(
|
||||
self,
|
||||
jobId: Annotated[str, Path(description="Job ID returned by fetch_users_ips")],
|
||||
) -> FetchUsersIpsResultResponseDto:
|
||||
"""Get Users IP List Result by Job ID.
|
||||
|
||||
Poll this endpoint after calling :meth:`fetch_users_ips`. When
|
||||
``is_completed`` is ``True`` the ``result`` field contains per-user
|
||||
IP lists.
|
||||
"""
|
||||
...
|
||||
|
||||
@post("/ip-control/drop-connections", response_class=DropConnectionsResponseDto)
|
||||
async def drop_connections(
|
||||
self,
|
||||
|
|
|
|||
50
remnawave/controllers/metadata.py
Normal file
50
remnawave/controllers/metadata.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from typing import Annotated
|
||||
|
||||
from rapid_api_client import Path
|
||||
from rapid_api_client.annotations import PydanticBody
|
||||
|
||||
from remnawave.models import (
|
||||
GetNodeMetadataResponseDto,
|
||||
GetUserMetadataResponseDto,
|
||||
UpsertNodeMetadataRequestBodyDto,
|
||||
UpsertNodeMetadataResponseDto,
|
||||
UpsertUserMetadataRequestBodyDto,
|
||||
UpsertUserMetadataResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, get, put
|
||||
|
||||
|
||||
class MetadataController(BaseController):
|
||||
@get("/metadata/user/{uuid}", response_class=GetUserMetadataResponseDto)
|
||||
async def get_user_metadata(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="User UUID")],
|
||||
) -> GetUserMetadataResponseDto:
|
||||
"""Get user metadata"""
|
||||
...
|
||||
|
||||
@put("/metadata/user/{uuid}", response_class=UpsertUserMetadataResponseDto)
|
||||
async def upsert_user_metadata(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="User UUID")],
|
||||
body: Annotated[UpsertUserMetadataRequestBodyDto, PydanticBody()],
|
||||
) -> UpsertUserMetadataResponseDto:
|
||||
"""Update or create User Metadata"""
|
||||
...
|
||||
|
||||
@get("/metadata/node/{uuid}", response_class=GetNodeMetadataResponseDto)
|
||||
async def get_node_metadata(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="Node UUID")],
|
||||
) -> GetNodeMetadataResponseDto:
|
||||
"""Get node metadata"""
|
||||
...
|
||||
|
||||
@put("/metadata/node/{uuid}", response_class=UpsertNodeMetadataResponseDto)
|
||||
async def upsert_node_metadata(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="Node UUID")],
|
||||
body: Annotated[UpsertNodeMetadataRequestBodyDto, PydanticBody()],
|
||||
) -> UpsertNodeMetadataResponseDto:
|
||||
"""Update or create Node Metadata"""
|
||||
...
|
||||
110
remnawave/controllers/node_plugins.py
Normal file
110
remnawave/controllers/node_plugins.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
from typing import Annotated, Optional
|
||||
|
||||
from rapid_api_client import Path, PydanticBody, Query
|
||||
|
||||
|
||||
from remnawave.models import (
|
||||
CloneNodePluginRequestDto,
|
||||
CloneNodePluginResponseDto,
|
||||
CreateNodePluginRequestDto,
|
||||
CreateNodePluginResponseDto,
|
||||
DeleteNodePluginResponseDto,
|
||||
GetNodePluginResponseDto,
|
||||
GetNodePluginsResponseDto,
|
||||
GetTorrentBlockerReportsResponseDto,
|
||||
GetTorrentBlockerReportsStatsResponseDto,
|
||||
PluginExecutorRequestDto,
|
||||
PluginExecutorResponseDto,
|
||||
ReorderNodePluginsRequestDto,
|
||||
ReorderNodePluginsResponseDto,
|
||||
TruncateTorrentBlockerReportsResponseDto,
|
||||
UpdateNodePluginRequestDto,
|
||||
UpdateNodePluginResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, delete, get, patch, post
|
||||
|
||||
|
||||
class NodePluginsController(BaseController):
|
||||
@get("/node-plugins/torrent-blocker", response_class=GetTorrentBlockerReportsResponseDto)
|
||||
async def get_torrent_blocker_reports(
|
||||
self,
|
||||
size: Annotated[Optional[int], Query(default=None, ge=1, description="Page size")] = None,
|
||||
start: Annotated[Optional[int], Query(default=None, ge=0, description="Offset")] = None,
|
||||
) -> GetTorrentBlockerReportsResponseDto:
|
||||
"""Get Torrent Blocker Reports"""
|
||||
...
|
||||
|
||||
@get("/node-plugins/torrent-blocker/stats", response_class=GetTorrentBlockerReportsStatsResponseDto)
|
||||
async def get_torrent_blocker_reports_stats(
|
||||
self,
|
||||
) -> GetTorrentBlockerReportsStatsResponseDto:
|
||||
"""Get Torrent Blocker Reports Stats"""
|
||||
...
|
||||
|
||||
@delete("/node-plugins/torrent-blocker/truncate", response_class=TruncateTorrentBlockerReportsResponseDto)
|
||||
async def truncate_torrent_blocker_reports(
|
||||
self,
|
||||
) -> TruncateTorrentBlockerReportsResponseDto:
|
||||
"""Truncate Torrent Blocker Reports"""
|
||||
...
|
||||
|
||||
@get("/node-plugins", response_class=GetNodePluginsResponseDto)
|
||||
async def get_all_node_plugins(self) -> GetNodePluginsResponseDto:
|
||||
"""Get all Node Plugins"""
|
||||
...
|
||||
|
||||
@patch("/node-plugins", response_class=UpdateNodePluginResponseDto)
|
||||
async def update_node_plugin(
|
||||
self,
|
||||
body: Annotated[UpdateNodePluginRequestDto, PydanticBody()],
|
||||
) -> UpdateNodePluginResponseDto:
|
||||
"""Update Node Plugin"""
|
||||
...
|
||||
|
||||
@post("/node-plugins", response_class=CreateNodePluginResponseDto)
|
||||
async def create_node_plugin(
|
||||
self,
|
||||
body: Annotated[CreateNodePluginRequestDto, PydanticBody()],
|
||||
) -> CreateNodePluginResponseDto:
|
||||
"""Create Node Plugin"""
|
||||
...
|
||||
|
||||
@get("/node-plugins/{uuid}", response_class=GetNodePluginResponseDto)
|
||||
async def get_node_plugin_by_uuid(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="Node plugin UUID")],
|
||||
) -> GetNodePluginResponseDto:
|
||||
"""Get Node Plugin by uuid"""
|
||||
...
|
||||
|
||||
@delete("/node-plugins/{uuid}", response_class=DeleteNodePluginResponseDto)
|
||||
async def delete_node_plugin(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="Node plugin UUID")],
|
||||
) -> DeleteNodePluginResponseDto:
|
||||
"""Delete Node Plugin"""
|
||||
...
|
||||
|
||||
@post("/node-plugins/actions/reorder", response_class=ReorderNodePluginsResponseDto)
|
||||
async def reorder_node_plugins(
|
||||
self,
|
||||
body: Annotated[ReorderNodePluginsRequestDto, PydanticBody()],
|
||||
) -> ReorderNodePluginsResponseDto:
|
||||
"""Reorder Node Plugins"""
|
||||
...
|
||||
|
||||
@post("/node-plugins/actions/clone", response_class=CloneNodePluginResponseDto)
|
||||
async def clone_node_plugin(
|
||||
self,
|
||||
body: Annotated[CloneNodePluginRequestDto, PydanticBody()],
|
||||
) -> CloneNodePluginResponseDto:
|
||||
"""Clone Node Plugin"""
|
||||
...
|
||||
|
||||
@post("/node-plugins/executor", response_class=PluginExecutorResponseDto)
|
||||
async def plugin_executor(
|
||||
self,
|
||||
body: Annotated[PluginExecutorRequestDto, PydanticBody()],
|
||||
) -> PluginExecutorResponseDto:
|
||||
"""Execute command on node plugins"""
|
||||
...
|
||||
|
|
@ -25,6 +25,8 @@ from remnawave.models import (
|
|||
ProfileModificationResponseDto,
|
||||
NodesBulkActionsRequestDto,
|
||||
NodesBulkActionsResponseDto,
|
||||
BulkNodesUpdateRequestDto,
|
||||
BulkNodesUpdateResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, delete, get, patch, post
|
||||
|
||||
|
|
@ -146,4 +148,12 @@ class NodesController(BaseController):
|
|||
body: Annotated[NodesBulkActionsRequestDto, PydanticBody()],
|
||||
) -> NodesBulkActionsResponseDto:
|
||||
"""Perform actions for many nodes (ENABLE, DISABLE, RESTART, RESET_TRAFFIC)"""
|
||||
...
|
||||
|
||||
@post("/nodes/bulk-actions/update", response_class=BulkNodesUpdateResponseDto)
|
||||
async def bulk_nodes_update(
|
||||
self,
|
||||
body: Annotated[BulkNodesUpdateRequestDto, PydanticBody()],
|
||||
) -> BulkNodesUpdateResponseDto:
|
||||
"""Update many nodes"""
|
||||
...
|
||||
|
|
@ -9,35 +9,35 @@ from remnawave.rapid import BaseController, get
|
|||
|
||||
class SubscriptionController(BaseController):
|
||||
# Public endpoints below
|
||||
@get("/sub/{short_uuid}/info", response_class=GetSubscriptionInfoResponseDto)
|
||||
@get("/sub/{shortUuid}/info", response_class=GetSubscriptionInfoResponseDto)
|
||||
async def get_subscription_info_by_short_uuid(
|
||||
self,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
|
||||
) -> GetSubscriptionInfoResponseDto:
|
||||
"""None"""
|
||||
...
|
||||
|
||||
@get("/sub/{short_uuid}", response_class=str)
|
||||
@get("/sub/{shortUuid}", response_class=str)
|
||||
async def get_subscription(
|
||||
self,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
|
||||
) -> str:
|
||||
"""None"""
|
||||
...
|
||||
|
||||
@get("/sub/{short_uuid}/{client_type}", response_class=str)
|
||||
@get("/sub/{shortUuid}/{clientType}", response_class=str)
|
||||
async def get_subscription_by_client_type(
|
||||
self,
|
||||
client_type: Annotated[ClientType, Path(description="Client type")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
|
||||
client_type: Annotated[ClientType, Path(description="Client type", alias="clientType")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
|
||||
) -> str:
|
||||
"""None"""
|
||||
...
|
||||
|
||||
@get("/sub/outline/{short_uuid}/{type}/{encoded_tag}", response_class=str)
|
||||
@get("/sub/outline/{shortUuid}/{type}/{encodedTag}", response_class=str)
|
||||
async def get_subscription_with_type(
|
||||
self,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
|
||||
type: Annotated[
|
||||
str,
|
||||
Path(
|
||||
|
|
@ -47,7 +47,8 @@ class SubscriptionController(BaseController):
|
|||
encoded_tag: Annotated[
|
||||
str,
|
||||
Path(
|
||||
description="Base64 encoded tag for Outline config. This paramter is optional. It is required only when type=ss."
|
||||
description="Base64 encoded tag for Outline config. This paramter is optional. It is required only when type=ss.",
|
||||
alias="encodedTag",
|
||||
),
|
||||
] = "VGVzdGVy",
|
||||
) -> str:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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 (
|
||||
|
|
@ -13,6 +12,7 @@ from remnawave.models import (
|
|||
GetSubscriptionByUUIDResponseDto,
|
||||
GetSubpageConfigByShortUuidRequestBodyDto,
|
||||
GetSubpageConfigByShortUuidResponseDto,
|
||||
GetConnectionKeysByUuidResponseDto,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -39,10 +39,10 @@ class SubscriptionsController(BaseController):
|
|||
"""None"""
|
||||
...
|
||||
|
||||
@get("/subscriptions/by-short-uuid/{short_uuid}", response_class=GetSubscriptionByShortUUIDResponseDto)
|
||||
@get("/subscriptions/by-short-uuid/{shortUuid}", response_class=GetSubscriptionByShortUUIDResponseDto)
|
||||
async def get_subscription_by_short_uuid(
|
||||
self,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the subscription")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the subscription", alias="shortUuid")],
|
||||
) -> GetSubscriptionByShortUUIDResponseDto:
|
||||
"""None"""
|
||||
...
|
||||
|
|
@ -55,20 +55,28 @@ class SubscriptionsController(BaseController):
|
|||
"""None"""
|
||||
...
|
||||
|
||||
@get("/subscriptions/subpage-config/{short_uuid}", response_class=GetSubpageConfigByShortUuidResponseDto)
|
||||
@get("/subscriptions/subpage-config/{shortUuid}", response_class=GetSubpageConfigByShortUuidResponseDto)
|
||||
async def get_subpage_config(
|
||||
self,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the subscription")],
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the subscription", alias="shortUuid")],
|
||||
body: Annotated[GetSubpageConfigByShortUuidRequestBodyDto, PydanticBody()],
|
||||
) -> GetSubpageConfigByShortUuidResponseDto:
|
||||
"""Get subscription page config by short UUID"""
|
||||
...
|
||||
|
||||
@get("/subscriptions/by-short-uuid/{short_uuid}/raw", response_class=GetRawSubscriptionByShortUuidResponseDto)
|
||||
@get("/subscriptions/by-short-uuid/{shortUuid}/raw", response_class=GetRawSubscriptionByShortUuidResponseDto)
|
||||
async def get_raw_subscription(
|
||||
self,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
|
||||
withDisabledHosts: Annotated[Annotated[bool, Path(description="Include disabled hosts")], bool] = False,
|
||||
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
|
||||
with_disabled_hosts: Annotated[bool, Query(default=False, alias="withDisabledHosts", description="Include disabled hosts")] = False,
|
||||
) -> GetRawSubscriptionByShortUuidResponseDto:
|
||||
"""None"""
|
||||
...
|
||||
|
||||
@get("/subscriptions/connection-keys/{uuid}", response_class=GetConnectionKeysByUuidResponseDto)
|
||||
async def get_connection_keys_by_uuid(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> GetConnectionKeysByUuidResponseDto:
|
||||
"""Get connection keys (base64 format) by uuid"""
|
||||
...
|
||||
|
|
@ -11,7 +11,8 @@ from remnawave.models import (
|
|||
EncryptHappCryptoLinkResponseDto,
|
||||
DebugSrrMatcherRequestDto,
|
||||
DebugSrrMatcherResponseDto,
|
||||
GetMetadataResponseDto
|
||||
GetMetadataResponseDto,
|
||||
GetRecapResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, get, post
|
||||
|
||||
|
|
@ -80,4 +81,11 @@ class SystemController(BaseController):
|
|||
body: Annotated[DebugSrrMatcherRequestDto, PydanticBody()],
|
||||
) -> DebugSrrMatcherResponseDto:
|
||||
"""Test SRR Matcher"""
|
||||
...
|
||||
|
||||
@get("/system/stats/recap", response_class=GetRecapResponseDto)
|
||||
async def get_recap(
|
||||
self,
|
||||
) -> GetRecapResponseDto:
|
||||
"""Get Recap"""
|
||||
...
|
||||
|
|
@ -21,6 +21,12 @@ from remnawave.models import (
|
|||
UpdateUserRequestDto,
|
||||
UpdateUserResponseDto,
|
||||
RevokeUserRequestDto,
|
||||
RevokeUserSubscriptionResponseDto,
|
||||
DisableUserResponseDto,
|
||||
EnableUserResponseDto,
|
||||
ResetUserTrafficResponseDto,
|
||||
ResolveUserRequestBodyDto,
|
||||
ResolveUserResponseDto,
|
||||
)
|
||||
from remnawave.rapid import BaseController, delete, get, patch, post
|
||||
|
||||
|
|
@ -65,36 +71,36 @@ class UsersController(BaseController):
|
|||
"""Delete user"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/revoke", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/revoke", response_class=RevokeUserSubscriptionResponseDto)
|
||||
async def revoke_user_subscription(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
body: Optional[Annotated[RevokeUserRequestDto, PydanticBody()]] = None,
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> RevokeUserSubscriptionResponseDto:
|
||||
"""Revoke User Subscription"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/disable", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/disable", response_class=DisableUserResponseDto)
|
||||
async def disable_user(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> DisableUserResponseDto:
|
||||
"""Disable User"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/enable", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/enable", response_class=EnableUserResponseDto)
|
||||
async def enable_user(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> EnableUserResponseDto:
|
||||
"""Enable User"""
|
||||
...
|
||||
|
||||
@post("/users/{uuid}/actions/reset-traffic", response_class=UpdateUserResponseDto)
|
||||
@post("/users/{uuid}/actions/reset-traffic", response_class=ResetUserTrafficResponseDto)
|
||||
async def reset_user_traffic(
|
||||
self,
|
||||
uuid: Annotated[str, Path(description="UUID of the user")],
|
||||
) -> UpdateUserResponseDto:
|
||||
) -> ResetUserTrafficResponseDto:
|
||||
"""Reset User Traffic"""
|
||||
...
|
||||
|
||||
|
|
@ -186,4 +192,12 @@ class UsersController(BaseController):
|
|||
tag: Annotated[str, Path(description="Tag of the user")],
|
||||
) -> TagUserResponseDto:
|
||||
"""Get Users By Tag"""
|
||||
...
|
||||
|
||||
@post("/users/resolve", response_class=ResolveUserResponseDto)
|
||||
async def resolve_user(
|
||||
self,
|
||||
body: Annotated[ResolveUserRequestBodyDto, PydanticBody()],
|
||||
) -> ResolveUserResponseDto:
|
||||
"""Resolve user by any identifier (uuid, id, shortUuid, username)"""
|
||||
...
|
||||
|
|
@ -6,7 +6,7 @@ from .security_layer import SecurityLayer
|
|||
from .template_type import TemplateType
|
||||
from .users import TrafficLimitStrategy, UserStatus
|
||||
from .webhook import (
|
||||
TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus
|
||||
TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus, TTorrentBlockerEvents
|
||||
)
|
||||
from .auth import OAuth2Provider
|
||||
from .subscriptions_settings import (
|
||||
|
|
@ -41,4 +41,5 @@ __all__ = [
|
|||
"TUserHwidDevicesEvents",
|
||||
"TResetPeriods",
|
||||
"TUsersStatus",
|
||||
"TTorrentBlockerEvents",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ from enum import StrEnum
|
|||
|
||||
class OAuth2Provider(StrEnum):
|
||||
"""OAuth2 Provider enum"""
|
||||
TELEGRAM = "telegram"
|
||||
GITHUB = "github"
|
||||
POCKETID = "pocketid"
|
||||
YANDEX = "yandex"
|
||||
KEYCLOAK = "keycloak"
|
||||
KEYCLOAK = "keycloak"
|
||||
GENERIC = "generic"
|
||||
|
|
@ -7,4 +7,5 @@ class TemplateType(StrEnum):
|
|||
SINGBOX_LEGACY = "SINGBOX_LEGACY"
|
||||
MIHOMO = "MIHOMO"
|
||||
XRAY_JSON = "XRAY_JSON"
|
||||
XRAY_BASE64 = "XRAY_BASE64"
|
||||
CLASH = "CLASH"
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@ class TrafficLimitStrategy(StrEnum):
|
|||
DAY = "DAY"
|
||||
WEEK = "WEEK"
|
||||
MONTH = "MONTH"
|
||||
MONTH_ROLLING = "MONTH_ROLLING"
|
||||
|
|
|
|||
|
|
@ -57,5 +57,9 @@ TUserHwidDevicesEvents = Literal[
|
|||
"user_hwid_devices.deleted",
|
||||
]
|
||||
|
||||
TResetPeriods = Literal["NO_RESET", "DAY", "WEEK", "MONTH"]
|
||||
TTorrentBlockerEvents = Literal[
|
||||
"torrent_blocker.report",
|
||||
]
|
||||
|
||||
TResetPeriods = Literal["NO_RESET", "DAY", "WEEK", "MONTH", "MONTH_ROLLING"]
|
||||
TUsersStatus = Literal["DISABLED", "LIMITED", "EXPIRED", "ACTIVE"]
|
||||
|
|
|
|||
|
|
@ -206,6 +206,8 @@ from .nodes import (
|
|||
NodeBulkActionType,
|
||||
NodesBulkActionsRequestDto,
|
||||
NodesBulkActionsResponseDto,
|
||||
BulkNodesUpdateRequestDto,
|
||||
BulkNodesUpdateResponseDto,
|
||||
)
|
||||
from .nodes_usage_history import (
|
||||
GetNodeUserUsageByRangeResponseDto,
|
||||
|
|
@ -225,6 +227,7 @@ from .subscription import (
|
|||
RawSettings,
|
||||
GetSubscriptionByShortUUIDResponseDto,
|
||||
GetSubscriptionByUUIDResponseDto,
|
||||
GetConnectionKeysByUuidResponseDto,
|
||||
)
|
||||
from .subscriptions_settings import (
|
||||
GetSubscriptionSettingsResponseDto,
|
||||
|
|
@ -233,6 +236,7 @@ from .subscriptions_settings import (
|
|||
ResponseRule,
|
||||
ResponseRuleCondition,
|
||||
ResponseRules,
|
||||
ResponseRulesSettings,
|
||||
SubscriptionSettingsResponseDto,
|
||||
SubscriptionType,
|
||||
UpdateSubscriptionSettingsRequestDto,
|
||||
|
|
@ -273,19 +277,24 @@ from .system import (
|
|||
StatusCounts,
|
||||
UsersStatistic,
|
||||
GetNodesMetricsResponseDto,
|
||||
GetX25519KeyPairResponseDto,
|
||||
GetX25519KeyPairResponseDto,
|
||||
X25519KeyPair,
|
||||
DebugSrrMatcherRequestDto,
|
||||
DebugSrrMatcherResponseDto,
|
||||
EncryptHappCryptoLinkRequestDto,
|
||||
EncryptHappCryptoLinkResponseDto,
|
||||
GetMetadataResponseDto
|
||||
GetMetadataResponseDto,
|
||||
GetRecapResponseDto,
|
||||
RecapThisMonth,
|
||||
RecapTotal,
|
||||
)
|
||||
from .users import (
|
||||
# Request DTOs
|
||||
CreateUserRequestDto,
|
||||
UpdateUserRequestDto,
|
||||
RevokeUserRequestDto,
|
||||
ResolveUserRequestBodyDto,
|
||||
ResolveUserResponseDto,
|
||||
|
||||
# Response DTOs - Single User
|
||||
CreateUserResponseDto,
|
||||
|
|
@ -375,7 +384,7 @@ from .subscription_request_history import (
|
|||
SubscriptionRequestHistoryStatsData
|
||||
)
|
||||
from .webhook import (
|
||||
UserEventDto,
|
||||
UserEventDto,
|
||||
UserHwidDeviceEventDto,
|
||||
HwidUserDeviceDto,
|
||||
LastConnectedNodeDto,
|
||||
|
|
@ -391,8 +400,15 @@ from .webhook import (
|
|||
NodeEventDto,
|
||||
CustomErrorEventDto,
|
||||
CrmEventDto,
|
||||
TorrentBlockerEventDto,
|
||||
TorrentBlockerReportDto,
|
||||
WebhookPayloadDto,
|
||||
UserTrafficDto
|
||||
UserTrafficDto,
|
||||
NodeSystemDto,
|
||||
NodeSystemInfoDto,
|
||||
NodeSystemStatsDto,
|
||||
NodeSystemInterfaceDto,
|
||||
NodeVersionsDto,
|
||||
)
|
||||
from .passkeys import (
|
||||
DeletePasskeyRequestDto,
|
||||
|
|
@ -405,6 +421,42 @@ from .passkeys import (
|
|||
VerifyPasskeyRegistrationRequestDto,
|
||||
VerifyPasskeyRegistrationResponseDto,
|
||||
)
|
||||
from .metadata import (
|
||||
GetMetadataResponseDto,
|
||||
GetUserMetadataResponseDto,
|
||||
UpsertUserMetadataRequestBodyDto,
|
||||
UpsertUserMetadataResponseDto,
|
||||
GetNodeMetadataResponseDto,
|
||||
UpsertNodeMetadataRequestBodyDto,
|
||||
UpsertNodeMetadataResponseDto,
|
||||
)
|
||||
from .node_plugins import (
|
||||
GetTorrentBlockerReportsResponseDto,
|
||||
GetTorrentBlockerReportsStatsResponseDto,
|
||||
TruncateTorrentBlockerReportsResponseDto,
|
||||
GetNodePluginsResponseDto,
|
||||
GetNodePluginResponseDto,
|
||||
UpdateNodePluginRequestDto,
|
||||
UpdateNodePluginResponseDto,
|
||||
DeleteNodePluginResponseDto,
|
||||
CreateNodePluginRequestDto,
|
||||
CreateNodePluginResponseDto,
|
||||
ReorderNodePluginItem,
|
||||
ReorderNodePluginsRequestDto,
|
||||
ReorderNodePluginsResponseDto,
|
||||
CloneNodePluginRequestDto,
|
||||
CloneNodePluginResponseDto,
|
||||
PluginExecutorRequestDto,
|
||||
PluginExecutorResponseDto,
|
||||
BlockIpsCommandDto,
|
||||
UnblockIpsCommandDto,
|
||||
RecreateTablesCommandDto,
|
||||
BlockIpItemDto,
|
||||
TorrentBlockerReportRecordDto,
|
||||
NodePluginDto,
|
||||
TargetAllNodesDto,
|
||||
TargetSpecificNodesDto,
|
||||
)
|
||||
from .external_squads import (
|
||||
AddUsersToExternalSquadResponseDto,
|
||||
CreateExternalSquadRequestDto,
|
||||
|
|
@ -479,6 +531,8 @@ from .ip_control import (
|
|||
# Response DTOs
|
||||
FetchIpsResponseDto,
|
||||
FetchIpsResultResponseDto,
|
||||
FetchUsersIpsResponseDto,
|
||||
FetchUsersIpsResultResponseDto,
|
||||
DropConnectionsResponseDto,
|
||||
# Data models
|
||||
FetchIpsJobData,
|
||||
|
|
@ -486,6 +540,11 @@ from .ip_control import (
|
|||
FetchIpsNodeResult,
|
||||
FetchIpsResult,
|
||||
FetchIpsResultData,
|
||||
FetchUsersIpsJobData,
|
||||
FetchUsersIpsUserIp,
|
||||
FetchUsersIpsUser,
|
||||
FetchUsersIpsResult,
|
||||
FetchUsersIpsResultData,
|
||||
DropConnectionsResponseData,
|
||||
)
|
||||
|
||||
|
|
@ -507,6 +566,7 @@ __all__ = [
|
|||
"VerifyPasskeyAuthenticationRequestDto",
|
||||
"VerifyPasskeyAuthenticationResponseDto",
|
||||
"GetPasskeyAuthenticationOptionsResponseDto",
|
||||
"BrandingSettings",
|
||||
# Nodes models
|
||||
"CreateNodeRequestDto",
|
||||
"CreateNodeResponseDto",
|
||||
|
|
@ -536,6 +596,8 @@ __all__ = [
|
|||
"NodeBulkActionType",
|
||||
"NodesBulkActionsRequestDto",
|
||||
"NodesBulkActionsResponseDto",
|
||||
"BulkNodesUpdateRequestDto",
|
||||
"BulkNodesUpdateResponseDto",
|
||||
# Hosts models
|
||||
"CreateHostRequestDto",
|
||||
"CreateHostResponseDto",
|
||||
|
|
@ -576,6 +638,7 @@ __all__ = [
|
|||
"UserSubscription",
|
||||
"GetRawSubscriptionByShortUuidResponseDto",
|
||||
"RawSettings",
|
||||
"GetConnectionKeysByUuidResponseDto",
|
||||
# Subscription settings models
|
||||
"GetSubscriptionSettingsResponseDto",
|
||||
"SubscriptionSettingsResponseDto",
|
||||
|
|
@ -592,6 +655,7 @@ __all__ = [
|
|||
"ResponseRule",
|
||||
"ResponseRuleCondition",
|
||||
"ResponseRules",
|
||||
"ResponseRulesSettings",
|
||||
# Subscription template models
|
||||
"GetTemplateResponseDto",
|
||||
"TemplateResponseDto",
|
||||
|
|
@ -627,7 +691,10 @@ __all__ = [
|
|||
"DebugSrrMatcherResponseDto",
|
||||
"EncryptHappCryptoLinkRequestDto",
|
||||
"EncryptHappCryptoLinkResponseDto",
|
||||
"GetMetadataResponseDto"
|
||||
"GetMetadataResponseDto",
|
||||
"GetRecapResponseDto",
|
||||
"RecapThisMonth",
|
||||
"RecapTotal",
|
||||
# XRay config models
|
||||
"ConfigResponseDto", # Legacy alias
|
||||
"GetConfigResponseDto",
|
||||
|
|
@ -694,6 +761,8 @@ __all__ = [
|
|||
"CreateUserRequestDto",
|
||||
"UpdateUserRequestDto",
|
||||
"RevokeUserRequestDto",
|
||||
"ResolveUserRequestBodyDto",
|
||||
"ResolveUserResponseDto",
|
||||
"CreateUserResponseDto",
|
||||
"UpdateUserResponseDto",
|
||||
"GetUserByUuidResponseDto",
|
||||
|
|
@ -860,6 +929,17 @@ __all__ = [
|
|||
# CRM EVENTS
|
||||
"CrmEventDto",
|
||||
|
||||
# TORRENT BLOCKER EVENTS
|
||||
"TorrentBlockerEventDto",
|
||||
"TorrentBlockerReportDto",
|
||||
|
||||
# NODE SYSTEM/VERSIONS
|
||||
"NodeSystemDto",
|
||||
"NodeSystemInfoDto",
|
||||
"NodeSystemStatsDto",
|
||||
"NodeSystemInterfaceDto",
|
||||
"NodeVersionsDto",
|
||||
|
||||
# WEBHOOK PAYLOAD
|
||||
"WebhookPayloadDto",
|
||||
|
||||
|
|
@ -954,5 +1034,48 @@ __all__ = [
|
|||
"FetchIpsNodeResult",
|
||||
"FetchIpsResult",
|
||||
"FetchIpsResultData",
|
||||
"FetchUsersIpsResponseDto",
|
||||
"FetchUsersIpsResultResponseDto",
|
||||
"FetchUsersIpsJobData",
|
||||
"FetchUsersIpsUserIp",
|
||||
"FetchUsersIpsUser",
|
||||
"FetchUsersIpsResult",
|
||||
"FetchUsersIpsResultData",
|
||||
"DropConnectionsResponseData",
|
||||
|
||||
# Metadata models
|
||||
"GetMetadataResponseDto",
|
||||
"GetUserMetadataResponseDto",
|
||||
"UpsertUserMetadataRequestBodyDto",
|
||||
"UpsertUserMetadataResponseDto",
|
||||
"GetNodeMetadataResponseDto",
|
||||
"UpsertNodeMetadataRequestBodyDto",
|
||||
"UpsertNodeMetadataResponseDto",
|
||||
|
||||
# Node plugins models
|
||||
"GetTorrentBlockerReportsResponseDto",
|
||||
"GetTorrentBlockerReportsStatsResponseDto",
|
||||
"TruncateTorrentBlockerReportsResponseDto",
|
||||
"GetNodePluginsResponseDto",
|
||||
"GetNodePluginResponseDto",
|
||||
"UpdateNodePluginRequestDto",
|
||||
"UpdateNodePluginResponseDto",
|
||||
"DeleteNodePluginResponseDto",
|
||||
"CreateNodePluginRequestDto",
|
||||
"CreateNodePluginResponseDto",
|
||||
"ReorderNodePluginItem",
|
||||
"ReorderNodePluginsRequestDto",
|
||||
"ReorderNodePluginsResponseDto",
|
||||
"CloneNodePluginRequestDto",
|
||||
"CloneNodePluginResponseDto",
|
||||
"PluginExecutorRequestDto",
|
||||
"PluginExecutorResponseDto",
|
||||
"BlockIpsCommandDto",
|
||||
"UnblockIpsCommandDto",
|
||||
"RecreateTablesCommandDto",
|
||||
"BlockIpItemDto",
|
||||
"TorrentBlockerReportRecordDto",
|
||||
"NodePluginDto",
|
||||
"TargetAllNodesDto",
|
||||
"TargetSpecificNodesDto",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Annotated, Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, StringConstraints
|
||||
from pydantic import BaseModel, Field, StringConstraints, field_validator
|
||||
|
||||
from remnawave.enums.auth import OAuth2Provider
|
||||
|
||||
|
|
@ -17,18 +17,35 @@ class LoginResponseDto(BaseModel):
|
|||
access_token: str = Field(alias="accessToken")
|
||||
|
||||
|
||||
class TelegramBotInfo(BaseModel):
|
||||
bot_id: int = Field(alias="botId")
|
||||
class PasskeyAuthenticationSettings(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class StatusResponseData(BaseModel):
|
||||
class OAuth2ProvidersSettings(BaseModel):
|
||||
providers: Dict[str, bool]
|
||||
|
||||
|
||||
class PasswordAuthenticationSettings(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class AuthenticationSettings(BaseModel):
|
||||
passkey: PasskeyAuthenticationSettings
|
||||
oauth2: OAuth2ProvidersSettings
|
||||
password: PasswordAuthenticationSettings
|
||||
|
||||
|
||||
class BrandingSettings(BaseModel):
|
||||
title: Optional[str] = None
|
||||
logo_url: Optional[str] = Field(None, alias="logoUrl")
|
||||
|
||||
|
||||
class GetStatusResponseDto(BaseModel):
|
||||
"""Status response with authentication and branding settings"""
|
||||
is_login_allowed: bool = Field(alias="isLoginAllowed")
|
||||
is_register_allowed: bool = Field(alias="isRegisterAllowed")
|
||||
tg_auth: Optional[TelegramBotInfo] = Field(None, alias="tgAuth")
|
||||
|
||||
|
||||
class GetStatusResponseDto(StatusResponseData):
|
||||
pass
|
||||
authentication: Optional[AuthenticationSettings] = None
|
||||
branding: BrandingSettings
|
||||
|
||||
|
||||
class LoginRequestDto(BaseModel):
|
||||
|
|
@ -40,6 +57,17 @@ class RegisterRequestDto(BaseModel):
|
|||
username: str
|
||||
password: Annotated[str, StringConstraints(min_length=24)]
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password_complexity(cls, v: str) -> str:
|
||||
if not any(c.isupper() for c in v):
|
||||
raise ValueError("Password must contain at least one uppercase letter")
|
||||
if not any(c.islower() for c in v):
|
||||
raise ValueError("Password must contain at least one lowercase letter")
|
||||
if not any(c.isdigit() for c in v):
|
||||
raise ValueError("Password must contain at least one digit")
|
||||
return v
|
||||
|
||||
|
||||
class TelegramCallbackRequestDto(BaseModel):
|
||||
id: int
|
||||
|
|
@ -82,8 +110,8 @@ class OAuth2CallbackResponseDto(BaseModel):
|
|||
# Passkey Authentication models
|
||||
class GetPasskeyAuthenticationOptionsResponseDto(BaseModel):
|
||||
"""Response with passkey authentication options"""
|
||||
# Passkey options are complex WebAuthn objects, using Any for flexibility
|
||||
response: Dict[str, Any]
|
||||
# Passkey options are complex WebAuthn objects
|
||||
pass
|
||||
|
||||
|
||||
class VerifyPasskeyAuthenticationRequestDto(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Annotated, Any, Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, StringConstraints
|
||||
|
||||
|
||||
class InboundDto(BaseModel):
|
||||
|
|
@ -32,7 +32,7 @@ class ConfigProfileDto(BaseModel):
|
|||
|
||||
|
||||
class CreateConfigProfileRequestDto(BaseModel):
|
||||
name: str
|
||||
name: Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
|
||||
config: Dict[str, Any]
|
||||
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ class CreateConfigProfileResponseDto(ConfigProfileDto):
|
|||
|
||||
class UpdateConfigProfileRequestDto(BaseModel):
|
||||
uuid: UUID
|
||||
name: Optional[str] = Field(None, pattern=r"^[A-Za-z0-9_-]+$")
|
||||
name: Optional[Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ class UpdateConfigProfileResponseDto(ConfigProfileDto):
|
|||
|
||||
|
||||
class GetAllConfigProfilesResponsePaginated(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
config_profiles: List[ConfigProfileDto] = Field(alias="configProfiles")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class ExternalSquadDto(BaseModel):
|
|||
response_headers: Optional[Dict[str, str]] = Field(None, alias="responseHeaders")
|
||||
hwid_settings: Optional[HwidSettingsDto] = Field(None, alias="hwidSettings")
|
||||
custom_remarks: Optional[CustomRemarksDto] = Field(None, alias="customRemarks")
|
||||
subpage_config_uuid: Optional[UUID] = Field(None, alias="subpageConfigUuid")
|
||||
created_at: datetime = Field(alias="createdAt")
|
||||
updated_at: datetime = Field(alias="updatedAt")
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ class ExternalSquadDto(BaseModel):
|
|||
# Request/Response models
|
||||
class GetExternalSquadsResponseDto(BaseModel):
|
||||
"""Response with all external squads"""
|
||||
total: int = Field(alias="total")
|
||||
total: float = Field(alias="total")
|
||||
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
|
||||
|
||||
|
||||
|
|
@ -94,6 +95,7 @@ class UpdateExternalSquadRequestDto(BaseModel):
|
|||
hwid_settings: Optional[HwidSettingsDto] = Field(None, alias="hwidSettings")
|
||||
custom_remarks: Optional[CustomRemarksDto] = Field(None, alias="customRemarks")
|
||||
response_headers: Optional[Dict[str, str]] = Field(None, serialization_alias="responseHeaders")
|
||||
subpage_config_uuid: Optional[UUID] = Field(None, serialization_alias="subpageConfigUuid")
|
||||
|
||||
|
||||
class UpdateExternalSquadResponseDto(ExternalSquadDto):
|
||||
|
|
@ -117,15 +119,10 @@ class ReorderExternalSquadsRequestDto(BaseModel):
|
|||
|
||||
class ReorderExternalSquadsResponseDto(BaseModel):
|
||||
"""Response after reordering external squads"""
|
||||
total: int = Field(alias="total")
|
||||
total: float = 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")
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class UpdateHostRequestDto(BaseModel):
|
|||
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
|
||||
is_hidden: Optional[bool] = Field(None, serialization_alias="isHidden")
|
||||
override_sni_from_address: Optional[bool] = Field(None, serialization_alias="overrideSniFromAddress")
|
||||
keep_blank_sni: Optional[bool] = Field(None, serialization_alias="keepBlankSni")
|
||||
keep_blank_sni: Optional[bool] = Field(None, serialization_alias="keepSniBlank")
|
||||
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
|
||||
shuffle_host: Optional[bool] = Field(None, serialization_alias="shuffleHost")
|
||||
mihomo_x25519: Optional[bool] = Field(None, serialization_alias="mihomoX25519")
|
||||
|
|
@ -89,7 +89,7 @@ class HostResponseDto(BaseModel):
|
|||
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, alias="securityLayer")
|
||||
is_hidden: bool = Field(False, alias="isHidden")
|
||||
override_sni_from_address: bool = Field(False, alias="overrideSniFromAddress")
|
||||
keep_blank_sni: bool = Field(False, alias="keepBlankSni")
|
||||
keep_blank_sni: bool = Field(False, alias="keepSniBlank")
|
||||
allow_insecure: bool = Field(False, alias="allowInsecure")
|
||||
xray_json_template_uuid: UUID | None = Field(alias="xrayJsonTemplateUuid")
|
||||
excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads")
|
||||
|
|
@ -128,7 +128,7 @@ class CreateHostRequestDto(BaseModel):
|
|||
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, serialization_alias="securityLayer")
|
||||
is_hidden: bool = Field(False, serialization_alias="isHidden")
|
||||
override_sni_from_address: bool = Field(False, serialization_alias="overrideSniFromAddress")
|
||||
keep_blank_sni: bool = Field(False, serialization_alias="keepBlankSni")
|
||||
keep_blank_sni: bool = Field(False, serialization_alias="keepSniBlank")
|
||||
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
|
||||
excluded_internal_squads: List[UUID] = Field(default_factory=list, serialization_alias="excludedInternalSquads")
|
||||
exclude_from_subscription_types: List[SubscriptionType] = Field(
|
||||
|
|
@ -147,6 +147,10 @@ class CreateHostRequestDto(BaseModel):
|
|||
config_profile_uuid: Optional[UUID] = None,
|
||||
**data,
|
||||
):
|
||||
# Backward-compatible support for misspelled helper argument used in old tests/examples
|
||||
if config_profile_uuid is None and "config_profile_inbound_uuid" in data:
|
||||
config_profile_uuid = data.pop("config_profile_inbound_uuid")
|
||||
|
||||
if inbound_uuid is not None and "inbound" not in data:
|
||||
data["inbound"] = CreateHostInboundData(
|
||||
config_profile_uuid=config_profile_uuid
|
||||
|
|
|
|||
|
|
@ -31,22 +31,22 @@ class HwidDeviceDto(BaseModel):
|
|||
|
||||
|
||||
class HwidDevicesData(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
devices: List[HwidDeviceDto]
|
||||
|
||||
|
||||
class CreateUserHwidDeviceResponseDto(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
devices: List[HwidDeviceDto]
|
||||
|
||||
|
||||
class DeleteUserHwidDeviceResponseDto(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
devices: List[HwidDeviceDto]
|
||||
|
||||
|
||||
class GetUserHwidDevicesResponseDto(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
devices: List[HwidDeviceDto]
|
||||
|
||||
class PlatformStatItem(BaseModel):
|
||||
|
|
@ -82,13 +82,13 @@ class TopUserByHwidDevicesDto(BaseModel):
|
|||
user_uuid: UUID = Field(alias="userUuid")
|
||||
id: int
|
||||
username: str
|
||||
devices_count: int = Field(alias="devicesCount")
|
||||
devices_count: float = Field(alias="devicesCount")
|
||||
|
||||
|
||||
class TopUsersByHwidDevicesData(BaseModel):
|
||||
"""Top users by HWID devices data"""
|
||||
users: list[TopUserByHwidDevicesDto]
|
||||
total: int
|
||||
total: float
|
||||
|
||||
|
||||
class GetTopUsersByHwidDevicesResponseDto(TopUsersByHwidDevicesData):
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ class InboundResponseDto(BaseModel):
|
|||
security: Optional[str] = None
|
||||
port: Optional[float] = None
|
||||
raw_inbound: Optional[Any] = Field(None, alias="rawInbound")
|
||||
active_squads: Optional[list[UUID]] = Field(None, alias="activeSquads")
|
||||
active_squads: List[UUID] = Field(default_factory=list, alias="activeSquads")
|
||||
|
||||
|
||||
class AllInboundsData(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
inbounds: List[InboundResponseDto]
|
||||
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ class GetAllInboundsResponseDto(AllInboundsData):
|
|||
|
||||
|
||||
class InboundsByProfileData(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
inbounds: List[InboundResponseDto]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class UpdateInfraProviderResponseDto(InfraProviderDto):
|
|||
|
||||
|
||||
class AllInfraProvidersData(BaseModel):
|
||||
total: int = Field(alias="total")
|
||||
total: float = Field(alias="total")
|
||||
providers: List[InfraProviderDto]
|
||||
|
||||
|
||||
|
|
@ -122,11 +122,9 @@ class DeleteInfraProviderByUuidResponseDto(BaseModel):
|
|||
# Billing History models
|
||||
class CreateInfraBillingHistoryRecordRequestDto(BaseModel):
|
||||
"""Модель для создания записи истории биллинга"""
|
||||
node_uuid: UUID = Field(serialization_alias="nodeUuid")
|
||||
provider_uuid: UUID = Field(serialization_alias="providerUuid")
|
||||
amount: float
|
||||
description: Optional[str] = None
|
||||
payment_date: datetime = Field(serialization_alias="paymentDate")
|
||||
amount: float = Field(ge=0)
|
||||
billed_at: datetime = Field(serialization_alias="billedAt")
|
||||
|
||||
|
||||
class CreateInfraBillingHistoryRecordResponseDto(InfraBillingHistoryDto):
|
||||
|
|
@ -135,7 +133,7 @@ class CreateInfraBillingHistoryRecordResponseDto(InfraBillingHistoryDto):
|
|||
|
||||
class InfraBillingHistoryData(BaseModel):
|
||||
records: List[InfraBillingHistoryDto]
|
||||
total: int
|
||||
total: float
|
||||
|
||||
|
||||
class GetInfraBillingHistoryRecordsResponseDto(InfraBillingHistoryData):
|
||||
|
|
@ -150,7 +148,7 @@ class DeleteInfraBillingHistoryRecordByUuidResponseDto(BaseModel):
|
|||
class CreateInfraBillingNodeRequestDto(BaseModel):
|
||||
node_uuid: UUID = Field(serialization_alias="nodeUuid")
|
||||
provider_uuid: UUID = Field(serialization_alias="providerUuid")
|
||||
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
|
||||
next_billing_at: Optional[datetime] = Field(None, serialization_alias="nextBillingAt")
|
||||
|
||||
|
||||
# ИСПРАВЛЕНО: API возвращает список всех billing nodes после создания, а не один созданный
|
||||
|
|
@ -167,8 +165,12 @@ class UpdateInfraBillingNodeRequestDto(BaseModel):
|
|||
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
|
||||
|
||||
|
||||
class UpdateInfraBillingNodeResponseDto(InfraBillingNodeDto):
|
||||
pass
|
||||
class UpdateInfraBillingNodeResponseDto(BaseModel):
|
||||
total_billing_nodes: float = Field(alias="totalBillingNodes")
|
||||
billing_nodes: List[InfraBillingNodeDto] = Field(alias="billingNodes")
|
||||
available_billing_nodes: List[AvailableBillingNodeDto] = Field(alias="availableBillingNodes")
|
||||
total_available_billing_nodes: float = Field(alias="totalAvailableBillingNodes")
|
||||
stats: BillingStatsDto
|
||||
|
||||
|
||||
class InfraBillingNodesData(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import Annotated, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, StringConstraints
|
||||
|
||||
|
||||
class InboundsDto(BaseModel):
|
||||
|
|
@ -17,8 +17,8 @@ class InboundsDto(BaseModel):
|
|||
|
||||
|
||||
class InfoDto(BaseModel):
|
||||
members_count: int = Field(alias="membersCount")
|
||||
inbounds_count: int = Field(alias="inboundsCount")
|
||||
members_count: float = Field(alias="membersCount")
|
||||
inbounds_count: float = Field(alias="inboundsCount")
|
||||
|
||||
|
||||
class InternalSquadDto(BaseModel):
|
||||
|
|
@ -32,7 +32,7 @@ class InternalSquadDto(BaseModel):
|
|||
|
||||
|
||||
class CreateInternalSquadRequestDto(BaseModel):
|
||||
name: str
|
||||
name: Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
|
||||
inbounds: List[UUID] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ class CreateInternalSquadResponseDto(InternalSquadDto):
|
|||
class UpdateInternalSquadRequestDto(BaseModel):
|
||||
uuid: UUID
|
||||
inbounds: List[UUID] = Field(default_factory=list)
|
||||
name: Optional[str] = Field(None, pattern=r"^[A-Za-z0-9_-]+$")
|
||||
name: Optional[Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]] = None
|
||||
|
||||
|
||||
class UpdateInternalSquadResponseDto(InternalSquadDto):
|
||||
|
|
@ -51,7 +51,7 @@ class UpdateInternalSquadResponseDto(InternalSquadDto):
|
|||
|
||||
|
||||
class GetAllInternalSquadsResponse(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
internal_squads: List[InternalSquadDto] = Field(alias="internalSquads")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from datetime import datetime
|
||||
from typing import Annotated, List, Literal, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
|
|
@ -114,6 +115,55 @@ TargetNodes = Annotated[
|
|||
]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Fetch Users IPs – step 1: start the job
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class FetchUsersIpsJobData(BaseModel):
|
||||
"""Returned job ID after requesting users IP fetch"""
|
||||
job_id: str = Field(alias="jobId")
|
||||
|
||||
|
||||
class FetchUsersIpsResponseDto(FetchUsersIpsJobData):
|
||||
"""Response for POST /api/ip-control/fetch-users-ips/{nodeUuid}"""
|
||||
pass
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Fetch Users IPs – step 2: poll the job result
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class FetchUsersIpsUserIp(BaseModel):
|
||||
"""IP entry with last seen timestamp"""
|
||||
ip: str
|
||||
last_seen: datetime = Field(alias="lastSeen")
|
||||
|
||||
|
||||
class FetchUsersIpsUser(BaseModel):
|
||||
"""Per-user IP list"""
|
||||
user_id: str = Field(alias="userId")
|
||||
ips: List[FetchUsersIpsUserIp]
|
||||
|
||||
|
||||
class FetchUsersIpsResult(BaseModel):
|
||||
"""Full result payload when the job is completed"""
|
||||
success: bool
|
||||
node_uuid: UUID = Field(alias="nodeUuid")
|
||||
users: List[FetchUsersIpsUser]
|
||||
|
||||
|
||||
class FetchUsersIpsResultData(BaseModel):
|
||||
"""Job state + optional result"""
|
||||
is_completed: bool = Field(alias="isCompleted")
|
||||
is_failed: bool = Field(alias="isFailed")
|
||||
result: Optional[FetchUsersIpsResult] = None
|
||||
|
||||
|
||||
class FetchUsersIpsResultResponseDto(FetchUsersIpsResultData):
|
||||
"""Response for GET /api/ip-control/fetch-users-ips/result/{jobId}"""
|
||||
pass
|
||||
|
||||
|
||||
class DropConnectionsRequestDto(BaseModel):
|
||||
"""Request body for POST /api/ip-control/drop-connections"""
|
||||
drop_by: DropBy = Field(
|
||||
|
|
|
|||
41
remnawave/models/metadata.py
Normal file
41
remnawave/models/metadata.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"""Metadata management models for Users and Nodes"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GetMetadataResponseDto(BaseModel):
|
||||
"""Get metadata response"""
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class GetUserMetadataResponseDto(BaseModel):
|
||||
"""Get user metadata response"""
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class UpsertUserMetadataRequestBodyDto(BaseModel):
|
||||
"""Request body for upserting user metadata"""
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
class UpsertUserMetadataResponseDto(BaseModel):
|
||||
"""Response for upserting user metadata"""
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
class GetNodeMetadataResponseDto(BaseModel):
|
||||
"""Get node metadata response"""
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class UpsertNodeMetadataRequestBodyDto(BaseModel):
|
||||
"""Request body for upserting node metadata"""
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
class UpsertNodeMetadataResponseDto(BaseModel):
|
||||
"""Response for upserting node metadata"""
|
||||
metadata: Dict[str, Any]
|
||||
235
remnawave/models/node_plugins.py
Normal file
235
remnawave/models/node_plugins.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
from datetime import datetime
|
||||
from typing import Any, Annotated, List, Literal, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
||||
|
||||
|
||||
class TorrentBlockerUserDto(BaseModel):
|
||||
uuid: UUID
|
||||
username: str
|
||||
|
||||
|
||||
class TorrentBlockerNodeDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
uuid: UUID
|
||||
name: str
|
||||
country_code: str = Field(alias="countryCode")
|
||||
|
||||
|
||||
class TorrentBlockerActionReportDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
blocked: bool
|
||||
ip: str
|
||||
block_duration: float = Field(alias="blockDuration")
|
||||
will_unblock_at: datetime = Field(alias="willUnblockAt")
|
||||
user_id: str = Field(alias="userId")
|
||||
processed_at: datetime = Field(alias="processedAt")
|
||||
|
||||
|
||||
class TorrentBlockerXrayReportDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
email: Optional[str] = None
|
||||
level: Optional[float] = None
|
||||
protocol: Optional[str] = None
|
||||
network: str
|
||||
source: Optional[str] = None
|
||||
destination: str
|
||||
route_target: Optional[str] = Field(default=None, alias="routeTarget")
|
||||
original_target: Optional[str] = Field(default=None, alias="originalTarget")
|
||||
inbound_tag: Optional[str] = Field(default=None, alias="inboundTag")
|
||||
inbound_name: Optional[str] = Field(default=None, alias="inboundName")
|
||||
inbound_local: Optional[str] = Field(default=None, alias="inboundLocal")
|
||||
outbound_tag: Optional[str] = Field(default=None, alias="outboundTag")
|
||||
ts: float
|
||||
|
||||
|
||||
class TorrentBlockerReportPayloadDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
action_report: TorrentBlockerActionReportDto = Field(alias="actionReport")
|
||||
xray_report: TorrentBlockerXrayReportDto = Field(alias="xrayReport")
|
||||
|
||||
|
||||
class TorrentBlockerReportRecordDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
id: float
|
||||
user_id: float = Field(alias="userId")
|
||||
node_id: float = Field(alias="nodeId")
|
||||
user: TorrentBlockerUserDto
|
||||
node: TorrentBlockerNodeDto
|
||||
report: TorrentBlockerReportPayloadDto
|
||||
created_at: datetime = Field(alias="createdAt")
|
||||
|
||||
|
||||
class TorrentBlockerReportsData(BaseModel):
|
||||
records: List[TorrentBlockerReportRecordDto]
|
||||
total: float
|
||||
|
||||
|
||||
class GetTorrentBlockerReportsResponseDto(TorrentBlockerReportsData):
|
||||
pass
|
||||
|
||||
|
||||
class TruncateTorrentBlockerReportsResponseDto(TorrentBlockerReportsData):
|
||||
pass
|
||||
|
||||
|
||||
class TorrentBlockerStatsDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
distinct_nodes: float = Field(alias="distinctNodes")
|
||||
distinct_users: float = Field(alias="distinctUsers")
|
||||
total_reports: float = Field(alias="totalReports")
|
||||
reports_last_24_hours: float = Field(alias="reportsLast24Hours")
|
||||
|
||||
|
||||
class TorrentBlockerTopUserDto(BaseModel):
|
||||
uuid: UUID
|
||||
color: str
|
||||
username: str
|
||||
total: float
|
||||
|
||||
|
||||
class TorrentBlockerTopNodeDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
uuid: UUID
|
||||
country_code: str = Field(alias="countryCode")
|
||||
color: str
|
||||
name: str
|
||||
total: float
|
||||
|
||||
|
||||
class GetTorrentBlockerReportsStatsResponseDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
stats: TorrentBlockerStatsDto
|
||||
top_users: List[TorrentBlockerTopUserDto] = Field(alias="topUsers")
|
||||
top_nodes: List[TorrentBlockerTopNodeDto] = Field(alias="topNodes")
|
||||
|
||||
|
||||
class NodePluginDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
uuid: UUID
|
||||
view_position: int = Field(alias="viewPosition")
|
||||
name: str
|
||||
plugin_config: Any | None = Field(alias="pluginConfig")
|
||||
|
||||
|
||||
class GetNodePluginsResponseDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
total: float
|
||||
node_plugins: List[NodePluginDto] = Field(alias="nodePlugins")
|
||||
|
||||
|
||||
class GetNodePluginResponseDto(NodePluginDto):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateNodePluginRequestDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
uuid: UUID
|
||||
name: Optional[
|
||||
Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
|
||||
] = None
|
||||
plugin_config: Optional[Any] = Field(default=None, alias="pluginConfig")
|
||||
|
||||
|
||||
class UpdateNodePluginResponseDto(NodePluginDto):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteNodePluginResponseDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
is_deleted: bool = Field(alias="isDeleted")
|
||||
|
||||
|
||||
class CreateNodePluginRequestDto(BaseModel):
|
||||
name: Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
|
||||
|
||||
|
||||
class CreateNodePluginResponseDto(NodePluginDto):
|
||||
pass
|
||||
|
||||
|
||||
class ReorderNodePluginItem(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
view_position: int = Field(alias="viewPosition")
|
||||
uuid: UUID
|
||||
|
||||
|
||||
class ReorderNodePluginsRequestDto(BaseModel):
|
||||
items: List[ReorderNodePluginItem]
|
||||
|
||||
|
||||
class ReorderNodePluginsResponseDto(GetNodePluginsResponseDto):
|
||||
pass
|
||||
|
||||
|
||||
class CloneNodePluginRequestDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
clone_from_uuid: UUID = Field(alias="cloneFromUuid")
|
||||
|
||||
|
||||
class CloneNodePluginResponseDto(NodePluginDto):
|
||||
pass
|
||||
|
||||
|
||||
class BlockIpItemDto(BaseModel):
|
||||
ip: str
|
||||
timeout: float
|
||||
|
||||
|
||||
class BlockIpsCommandDto(BaseModel):
|
||||
command: Literal["blockIps"]
|
||||
ips: List[BlockIpItemDto]
|
||||
|
||||
|
||||
class UnblockIpsCommandDto(BaseModel):
|
||||
command: Literal["unblockIps"]
|
||||
ips: List[str]
|
||||
|
||||
|
||||
class RecreateTablesCommandDto(BaseModel):
|
||||
command: Literal["recreateTables"]
|
||||
|
||||
|
||||
PluginCommandDto = Union[BlockIpsCommandDto, UnblockIpsCommandDto, RecreateTablesCommandDto]
|
||||
|
||||
|
||||
class TargetAllNodesDto(BaseModel):
|
||||
target: Literal["allNodes"]
|
||||
|
||||
|
||||
class TargetSpecificNodesDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
target: Literal["specificNodes"]
|
||||
node_uuids: List[UUID] = Field(alias="nodeUuids")
|
||||
|
||||
|
||||
PluginTargetNodesDto = Union[TargetAllNodesDto, TargetSpecificNodesDto]
|
||||
|
||||
|
||||
class PluginExecutorRequestDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
command: PluginCommandDto
|
||||
target_nodes: PluginTargetNodesDto = Field(alias="targetNodes")
|
||||
|
||||
|
||||
class PluginExecutorResponseDto(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
event_sent: bool = Field(alias="eventSent")
|
||||
|
|
@ -89,6 +89,9 @@ class CreateNodeRequestDto(BaseModel):
|
|||
serialization_alias="tags",
|
||||
max_length=10
|
||||
)
|
||||
active_plugin_uuid: Optional[UUID] = Field(
|
||||
None, serialization_alias="activePluginUuid"
|
||||
)
|
||||
|
||||
|
||||
class UpdateNodeRequestDto(BaseModel):
|
||||
|
|
@ -126,6 +129,9 @@ class UpdateNodeRequestDto(BaseModel):
|
|||
serialization_alias="tags",
|
||||
max_length=10
|
||||
)
|
||||
active_plugin_uuid: Optional[UUID] = Field(
|
||||
None, serialization_alias="activePluginUuid"
|
||||
)
|
||||
|
||||
|
||||
class ReorderNodeRequestDto(BaseModel):
|
||||
|
|
@ -144,7 +150,7 @@ class NodeResponseDto(BaseModel):
|
|||
last_status_message: Optional[str] = Field(None, alias="lastStatusMessage")
|
||||
xray_version: Optional[str] = Field(None, alias="xrayVersion")
|
||||
node_version: Optional[str] = Field(None, alias="nodeVersion")
|
||||
xray_uptime: str = Field(alias="xrayUptime")
|
||||
xray_uptime: float = Field(0, alias="xrayUptime")
|
||||
is_traffic_tracking_active: bool = Field(alias="isTrafficTrackingActive")
|
||||
traffic_reset_day: Optional[int] = Field(None, alias="trafficResetDay")
|
||||
traffic_limit_bytes: Optional[float] = Field(None, alias="trafficLimitBytes")
|
||||
|
|
@ -163,6 +169,7 @@ class NodeResponseDto(BaseModel):
|
|||
provider_uuid: Optional[UUID] = Field(None, alias="providerUuid")
|
||||
provider: Optional[NodeProviderDto] = None
|
||||
tags: List[str] = Field(default_factory=list, alias="tags")
|
||||
active_plugin_uuid: Optional[UUID] = Field(None, alias="activePluginUuid")
|
||||
|
||||
|
||||
class CreateNodeResponseDto(NodeResponseDto):
|
||||
|
|
@ -287,4 +294,14 @@ class NodesBulkActionsRequestDto(BaseModel):
|
|||
|
||||
class NodesBulkActionsResponseDto(BaseModel):
|
||||
"""Response after performing bulk actions on nodes"""
|
||||
event_sent: bool = Field(alias="eventSent")
|
||||
event_sent: bool = Field(alias="eventSent")
|
||||
|
||||
|
||||
class BulkNodesUpdateRequestDto(NodesBulkActionsRequestDto):
|
||||
"""OpenAPI alias for bulk nodes update request"""
|
||||
pass
|
||||
|
||||
|
||||
class BulkNodesUpdateResponseDto(NodesBulkActionsResponseDto):
|
||||
"""OpenAPI alias for bulk nodes update response"""
|
||||
pass
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, StringConstraints
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
class PasskeyDto(BaseModel):
|
||||
|
|
@ -15,8 +16,8 @@ class PasskeyDto(BaseModel):
|
|||
# Registration models
|
||||
class GetPasskeyRegistrationOptionsResponseDto(BaseModel):
|
||||
"""Response with passkey registration options"""
|
||||
# WebAuthn registration options are complex objects, using Any for flexibility
|
||||
response: Dict[str, Any]
|
||||
# WebAuthn registration options are complex objects
|
||||
pass
|
||||
|
||||
|
||||
class VerifyPasskeyRegistrationRequestDto(BaseModel):
|
||||
|
|
@ -25,12 +26,21 @@ class VerifyPasskeyRegistrationRequestDto(BaseModel):
|
|||
response: Dict[str, Any]
|
||||
|
||||
|
||||
class VerifyPasskeyRegistrationResponseData(BaseModel):
|
||||
"""Passkey registration verification result data"""
|
||||
verified: bool
|
||||
|
||||
|
||||
class VerifyPasskeyRegistrationResponseDto(BaseModel):
|
||||
"""Response with passkey registration verification result"""
|
||||
verified: bool
|
||||
|
||||
|
||||
# Passkeys management models
|
||||
class GetAllPasskeysResponseData(BaseModel):
|
||||
"""Response data with all user's passkeys"""
|
||||
passkeys: List[PasskeyDto]
|
||||
|
||||
|
||||
class GetAllPasskeysResponseDto(BaseModel):
|
||||
"""Response with all user's passkeys"""
|
||||
passkeys: List[PasskeyDto]
|
||||
|
|
@ -41,6 +51,11 @@ class DeletePasskeyRequestDto(BaseModel):
|
|||
id: str
|
||||
|
||||
|
||||
class DeletePasskeyResponseData(BaseModel):
|
||||
"""Response data with updated passkeys list after deletion"""
|
||||
passkeys: List[PasskeyDto]
|
||||
|
||||
|
||||
class DeletePasskeyResponseDto(BaseModel):
|
||||
"""Response with updated passkeys list after deletion"""
|
||||
passkeys: List[PasskeyDto]
|
||||
|
|
@ -49,9 +64,14 @@ class DeletePasskeyResponseDto(BaseModel):
|
|||
class UpdatePasskeyRequestDto(BaseModel):
|
||||
"""Request to update a passkey"""
|
||||
id: str
|
||||
name: str
|
||||
name: Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
|
||||
|
||||
|
||||
class UpdatePasskeyResponseData(BaseModel):
|
||||
"""Response data with updated passkeys list"""
|
||||
passkeys: List[PasskeyDto]
|
||||
|
||||
|
||||
class UpdatePasskeyResponseDto(BaseModel):
|
||||
"""Response with updated passkey information"""
|
||||
passkey: PasskeyDto
|
||||
passkeys: List[PasskeyDto]
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ class GenericOAuth2Settings(BaseModel):
|
|||
allowed_emails: List[str] = Field(alias="allowedEmails")
|
||||
|
||||
|
||||
class TelegramOAuth2Settings(BaseModel):
|
||||
"""Telegram OAuth2 settings"""
|
||||
enabled: bool
|
||||
client_id: str | None = Field(alias="clientId")
|
||||
client_secret: str | None = Field(alias="clientSecret")
|
||||
allowed_ids: List[str] = Field(alias="allowedIds")
|
||||
frontend_domain: str | None = Field(alias="frontendDomain")
|
||||
|
||||
|
||||
class OAuth2Settings(BaseModel):
|
||||
"""OAuth2 authentication settings"""
|
||||
github: GitHubOAuth2Settings
|
||||
|
|
@ -65,13 +74,16 @@ class OAuth2Settings(BaseModel):
|
|||
yandex: YandexOAuth2Settings
|
||||
keycloak: KeycloakOAuth2Settings
|
||||
generic: GenericOAuth2Settings
|
||||
telegram: TelegramOAuth2Settings
|
||||
|
||||
|
||||
class TelegramAuthSettings(BaseModel):
|
||||
"""Telegram authentication settings"""
|
||||
enabled: bool
|
||||
bot_token: str | None = Field(alias="botToken")
|
||||
admin_ids: List[str] = Field(alias="adminIds")
|
||||
client_id: str | None = Field(alias="clientId")
|
||||
client_secret: str | None = Field(alias="clientSecret")
|
||||
allowed_ids: List[str] = Field(alias="allowedIds")
|
||||
frontend_domain: str | None = Field(alias="frontendDomain")
|
||||
|
||||
|
||||
class PasswordSettings(BaseModel):
|
||||
|
|
@ -89,7 +101,6 @@ class RemnawaveSettingsData(BaseModel):
|
|||
"""Remnawave settings data"""
|
||||
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")
|
||||
|
||||
|
|
@ -103,7 +114,6 @@ class UpdateRemnawaveSettingsRequestDto(BaseModel):
|
|||
"""Update Remnawave settings request"""
|
||||
passkey_settings: Optional[PasskeySettings] = Field(None, serialization_alias="passkeySettings")
|
||||
oauth2_settings: Optional[OAuth2Settings] = Field(None, serialization_alias="oauth2Settings")
|
||||
tg_auth_settings: Optional[TelegramAuthSettings] = Field(None, serialization_alias="tgAuthSettings")
|
||||
password_settings: Optional[PasswordSettings] = Field(None, serialization_alias="passwordSettings")
|
||||
branding_settings: Optional[BrandingSettings] = Field(None, serialization_alias="brandingSettings")
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class RawSubscriptionResponse(BaseModel):
|
|||
user: UserResponseDto
|
||||
converted_user_info: ConvertedUserInfo = Field(alias="convertedUserInfo")
|
||||
headers: Dict[str, str]
|
||||
raw_hosts: List[RawHost] = Field(alias="rawHosts")
|
||||
raw_hosts: Optional[List[RawHost]] = Field(None, alias="rawHosts")
|
||||
|
||||
|
||||
class GetRawSubscriptionByShortUuidResponseDto(RawSubscriptionResponse):
|
||||
|
|
@ -176,7 +176,7 @@ class GetRawSubscriptionByShortUuidResponseDto(RawSubscriptionResponse):
|
|||
class UserSubscription(BaseModel):
|
||||
short_uuid: str = Field(alias="shortUuid")
|
||||
username: str
|
||||
days_left: int = Field(alias="daysLeft")
|
||||
days_left: float = Field(alias="daysLeft")
|
||||
traffic_used: str = Field(alias="trafficUsed")
|
||||
traffic_limit: str = Field(alias="trafficLimit")
|
||||
lifetime_traffic_used: str = Field(alias="lifetimeTrafficUsed")
|
||||
|
|
@ -222,7 +222,7 @@ class SubscriptionWithoutHapp(BaseModel):
|
|||
|
||||
class GetAllSubscriptionsResponseDto(BaseModel):
|
||||
subscriptions: List[SubscriptionWithoutHapp]
|
||||
total: int
|
||||
total: float
|
||||
|
||||
|
||||
class GetSubscriptionByUsernameResponseDto(BaseModel):
|
||||
|
|
@ -241,5 +241,16 @@ class GetSubscriptionByUUIDResponseDto(GetSubscriptionByUsernameResponseDto):
|
|||
pass
|
||||
|
||||
|
||||
class GetConnectionKeysByUuidResponseDto(BaseModel):
|
||||
enabled_keys: List[str] = Field(alias="enabledKeys")
|
||||
hidden_keys: List[str] = Field(alias="hiddenKeys")
|
||||
disabled_keys: List[str] = Field(alias="disabledKeys")
|
||||
|
||||
@property
|
||||
def connection_keys(self) -> List[str]:
|
||||
"""Backward compatibility: historically SDK exposed a flat list of keys."""
|
||||
return self.enabled_keys
|
||||
|
||||
|
||||
# Legacy alias for backward compatibility
|
||||
SubscriptionInfoResponseDto = GetSubscriptionInfoResponseDto
|
||||
|
|
@ -16,7 +16,7 @@ class SubscriptionPageConfigDto(BaseModel):
|
|||
|
||||
class GetSubscriptionPageConfigsData(BaseModel):
|
||||
"""Data for getting all subscription page configs"""
|
||||
total: int
|
||||
total: float
|
||||
configs: List[SubscriptionPageConfigDto]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -74,10 +74,20 @@ class ResponseRule(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class ResponseRulesSettings(BaseModel):
|
||||
"""Settings for response rules"""
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
disable_subscription_access_by_path: Optional[bool] = Field(
|
||||
None, alias="disableSubscriptionAccessByPath"
|
||||
)
|
||||
|
||||
|
||||
class ResponseRules(BaseModel):
|
||||
"""Response rules configuration"""
|
||||
version: ResponseRuleVersion
|
||||
rules: List[ResponseRule]
|
||||
settings: Optional[ResponseRulesSettings] = None
|
||||
|
||||
|
||||
class CustomRemarksDto(BaseModel):
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class GetTemplateResponseDto(TemplateResponseDto):
|
|||
pass
|
||||
|
||||
class GetTemplatesData(BaseModel):
|
||||
total: int
|
||||
total: float
|
||||
templates: List[TemplateInfoDto]
|
||||
|
||||
class GetTemplatesResponseDto(GetTemplatesData):
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from remnawave.models.subscriptions_settings import ResponseRule, ResponseRules
|
|||
class NodeStatistic(BaseModel):
|
||||
node_name: str = Field(alias="nodeName")
|
||||
date: datetime.date
|
||||
total_bytes: int = Field(alias="totalBytes")
|
||||
total_bytes: str = Field(alias="totalBytes")
|
||||
|
||||
|
||||
class NodesStatisticResponseDto(BaseModel):
|
||||
|
|
@ -32,16 +32,16 @@ class BandwidthStatisticResponseDto(BaseModel):
|
|||
|
||||
|
||||
class CPUStatistic(BaseModel):
|
||||
cores: int
|
||||
physical_cores: int = Field(alias="physicalCores")
|
||||
cores: float
|
||||
physical_cores: Optional[float] = Field(None, alias="physicalCores")
|
||||
|
||||
|
||||
class MemoryStatistic(BaseModel):
|
||||
total: int
|
||||
free: int
|
||||
used: int
|
||||
active: int
|
||||
available: int
|
||||
total: float
|
||||
free: float
|
||||
used: float
|
||||
active: Optional[float] = None
|
||||
available: Optional[float] = None
|
||||
|
||||
|
||||
class StatusCounts(BaseModel):
|
||||
|
|
@ -59,18 +59,18 @@ class StatusCounts(BaseModel):
|
|||
|
||||
class UsersStatistic(BaseModel):
|
||||
status_counts: StatusCounts = Field(alias="statusCounts")
|
||||
total_users: int = Field(alias="totalUsers")
|
||||
total_users: float = Field(alias="totalUsers")
|
||||
|
||||
|
||||
class OnlineStatistic(BaseModel):
|
||||
last_day: int = Field(alias="lastDay")
|
||||
last_week: int = Field(alias="lastWeek")
|
||||
never_online: int = Field(alias="neverOnline")
|
||||
online_now: int = Field(alias="onlineNow")
|
||||
last_day: float = Field(alias="lastDay")
|
||||
last_week: float = Field(alias="lastWeek")
|
||||
never_online: float = Field(alias="neverOnline")
|
||||
online_now: float = Field(alias="onlineNow")
|
||||
|
||||
|
||||
class NodesStatistic(BaseModel):
|
||||
total_online: int = Field(alias="totalOnline")
|
||||
total_online: float = Field(alias="totalOnline")
|
||||
total_bytes_lifetime: str = Field(alias="totalBytesLifetime")
|
||||
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ class StatisticResponseDto(BaseModel):
|
|||
cpu: CPUStatistic
|
||||
memory: MemoryStatistic
|
||||
uptime: float
|
||||
timestamp: int
|
||||
timestamp: float
|
||||
users: UsersStatistic
|
||||
online_stats: OnlineStatistic = Field(alias="onlineStats")
|
||||
nodes: NodesStatistic
|
||||
|
|
@ -112,25 +112,73 @@ class GetNodesStatisticsResponseDto(BaseModel):
|
|||
last_seven_days: List[NodeStatistic] = Field(alias="lastSevenDays")
|
||||
|
||||
|
||||
class RuntimeMetric(BaseModel):
|
||||
"""Runtime metric from health endpoint"""
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
rss: Optional[float] = None
|
||||
heap_total: Optional[float] = Field(None, alias="heapTotal")
|
||||
heap_used: Optional[float] = Field(None, alias="heapUsed")
|
||||
external: Optional[float] = None
|
||||
instance_type: Optional[str] = Field(None, alias="instanceType")
|
||||
|
||||
|
||||
class GetRemnawaveHealthResponseDto(BaseModel):
|
||||
pm2_stats: List[PM2Stat] = Field(alias="pm2Stats")
|
||||
pm2_stats: Optional[List[PM2Stat]] = Field(None, alias="pm2Stats")
|
||||
runtime_metrics: Optional[List[RuntimeMetric]] = Field(None, alias="runtimeMetrics")
|
||||
|
||||
|
||||
class TrafficStatDto(BaseModel):
|
||||
tag: str
|
||||
upload: str
|
||||
download: str
|
||||
|
||||
|
||||
class NodeMetric(BaseModel):
|
||||
"""Node metric data"""
|
||||
uuid: str = Field(alias="nodeUuid")
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
is_online: Optional[bool] = Field(None, alias="isOnline")
|
||||
cpu_usage: Optional[float] = Field(None, alias="cpuUsage")
|
||||
memory_usage: Optional[float] = Field(None, alias="memoryUsage")
|
||||
network_upload: Optional[int] = Field(None, alias="networkUpload")
|
||||
network_download: Optional[int] = Field(None, alias="networkDownload")
|
||||
uptime: Optional[int] = None
|
||||
last_seen: Optional[datetime.datetime] = Field(None, alias="lastSeen")
|
||||
connected_users: Optional[int] = Field(None, alias="connectedUsers")
|
||||
upload: Optional[str] = None
|
||||
download: Optional[str] = None
|
||||
"""Node metric data (API v1.10)"""
|
||||
node_uuid: str = Field(alias="nodeUuid")
|
||||
node_name: str = Field(alias="nodeName")
|
||||
country_emoji: str = Field(alias="countryEmoji")
|
||||
provider_name: str = Field(alias="providerName")
|
||||
users_online: float = Field(alias="usersOnline")
|
||||
inbounds_stats: List[TrafficStatDto] = Field(alias="inboundsStats")
|
||||
outbounds_stats: List[TrafficStatDto] = Field(alias="outboundsStats")
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
return self.node_uuid
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.node_name
|
||||
|
||||
@property
|
||||
def connected_users(self) -> float:
|
||||
return self.users_online
|
||||
|
||||
@property
|
||||
def cpu_usage(self) -> None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def memory_usage(self) -> None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def network_upload(self) -> None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def network_download(self) -> None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def uptime(self) -> None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def last_seen(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class GetNodesMetricsResponseDto(BaseModel):
|
||||
|
|
@ -143,7 +191,11 @@ class X25519KeyPair(BaseModel):
|
|||
|
||||
|
||||
class GetX25519KeyPairResponseDto(BaseModel):
|
||||
key_pairs: List[X25519KeyPair] = Field(alias="keyPairs")
|
||||
key_pairs: List[X25519KeyPair] = Field(alias="keypairs")
|
||||
|
||||
|
||||
# OpenAPI v1.10 schema name
|
||||
GenerateX25519ResponseDto = GetX25519KeyPairResponseDto
|
||||
|
||||
|
||||
class EncryptHappCryptoLinkRequestDto(BaseModel):
|
||||
|
|
@ -173,6 +225,27 @@ class DebugSrrMatcherData(BaseModel):
|
|||
class DebugSrrMatcherResponseDto(DebugSrrMatcherData):
|
||||
pass
|
||||
|
||||
class RecapThisMonth(BaseModel):
|
||||
users: float
|
||||
traffic: str
|
||||
|
||||
|
||||
class RecapTotal(BaseModel):
|
||||
users: float
|
||||
nodes: float
|
||||
traffic: str
|
||||
nodes_ram: str = Field(alias="nodesRam")
|
||||
nodes_cpu_cores: float = Field(alias="nodesCpuCores")
|
||||
distinct_countries: float = Field(alias="distinctCountries")
|
||||
|
||||
|
||||
class GetRecapResponseDto(BaseModel):
|
||||
this_month: RecapThisMonth = Field(alias="thisMonth")
|
||||
total: RecapTotal
|
||||
version: str
|
||||
init_date: datetime.datetime = Field(alias="initDate")
|
||||
|
||||
|
||||
class BuildInfo(BaseModel):
|
||||
"""Build information"""
|
||||
time: str
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ class UpdateUserRequestDto(BaseModel):
|
|||
|
||||
class UserTrafficDto(BaseModel):
|
||||
"""User traffic information"""
|
||||
used_traffic_bytes: int = Field(alias="usedTrafficBytes")
|
||||
lifetime_used_traffic_bytes: int = Field(alias="lifetimeUsedTrafficBytes")
|
||||
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
|
||||
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
|
||||
online_at: Optional[datetime] = Field(None, alias="onlineAt")
|
||||
first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt")
|
||||
last_connected_node_uuid: Optional[UUID] = Field(None, alias="lastConnectedNodeUuid")
|
||||
|
|
@ -149,12 +149,12 @@ class UserResponseDto(BaseModel):
|
|||
user_traffic: UserTrafficDto = Field(alias="userTraffic")
|
||||
|
||||
@property
|
||||
def used_traffic_bytes(self) -> int:
|
||||
def used_traffic_bytes(self) -> float:
|
||||
"""Backward compatibility property"""
|
||||
return self.user_traffic.used_traffic_bytes
|
||||
|
||||
@property
|
||||
def lifetime_used_traffic_bytes(self) -> int:
|
||||
def lifetime_used_traffic_bytes(self) -> float:
|
||||
"""Backward compatibility property"""
|
||||
return self.user_traffic.lifetime_used_traffic_bytes
|
||||
|
||||
|
|
@ -203,6 +203,22 @@ class RevokeUserRequestDto(BaseModel):
|
|||
description="Optional. If true, only passwords will be revoked without changing the short UUID.",
|
||||
)
|
||||
|
||||
class ResolveUserRequestBodyDto(BaseModel):
|
||||
"""Request DTO for resolving a user by any identifier"""
|
||||
uuid: Optional[UUID] = None
|
||||
id: Optional[int] = None
|
||||
short_uuid: Optional[str] = Field(None, serialization_alias="shortUuid")
|
||||
username: Optional[str] = None
|
||||
|
||||
|
||||
class ResolveUserResponseDto(BaseModel):
|
||||
"""Response DTO for resolved user"""
|
||||
uuid: UUID
|
||||
username: str
|
||||
id: int
|
||||
short_uuid: str = Field(alias="shortUuid")
|
||||
|
||||
|
||||
class SubscriptionRequestRecord(BaseModel):
|
||||
"""Subscription request history record"""
|
||||
id: int
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ class BulkAllExtendExpirationDateRequestDto(BaseModel):
|
|||
# Base Response DTOs (без обертки response)
|
||||
class BulkResponseData(BaseModel):
|
||||
"""Common bulk response with affected rows"""
|
||||
affected_rows: int = Field(alias="affectedRows")
|
||||
affected_rows: float = Field(alias="affectedRows")
|
||||
|
||||
|
||||
class BulkEventResponseData(BaseModel):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from uuid import UUID
|
|||
from pydantic import BaseModel, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
from remnawave.enums import (
|
||||
TUsersStatus, TUserEvents, TUserHwidDevicesEvents, TServiceEvents, TNodeEvents, TErrorsEvents, TCRMEvents, TResetPeriods
|
||||
TUsersStatus, TUserEvents, TUserHwidDevicesEvents, TServiceEvents, TNodeEvents, TErrorsEvents, TCRMEvents, TTorrentBlockerEvents, TResetPeriods
|
||||
)
|
||||
# ---------------- USER ---------------- #
|
||||
|
||||
|
|
@ -223,6 +223,55 @@ class WebhookNodeConfigProfileDto(BaseModel):
|
|||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemInfoDto(BaseModel):
|
||||
arch: str
|
||||
cpus: int
|
||||
cpu_model: str
|
||||
memory_total: float
|
||||
hostname: str
|
||||
platform: str
|
||||
release: str
|
||||
type: str
|
||||
version: str
|
||||
network_interfaces: List[str]
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemInterfaceDto(BaseModel):
|
||||
interface: str
|
||||
rx_bytes_per_sec: float
|
||||
tx_bytes_per_sec: float
|
||||
rx_total: float
|
||||
tx_total: float
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemStatsDto(BaseModel):
|
||||
memory_free: float
|
||||
memory_used: float
|
||||
uptime: float
|
||||
load_avg: List[float]
|
||||
interface: Optional[NodeSystemInterfaceDto] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeSystemDto(BaseModel):
|
||||
info: NodeSystemInfoDto
|
||||
stats: NodeSystemStatsDto
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeVersionsDto(BaseModel):
|
||||
xray: str
|
||||
node: str
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodeDto(BaseModel):
|
||||
uuid: UUID
|
||||
name: str
|
||||
|
|
@ -234,11 +283,8 @@ class NodeDto(BaseModel):
|
|||
last_status_change: Optional[datetime] = None
|
||||
last_status_message: Optional[str] = None
|
||||
|
||||
xray_version: Optional[str] = None
|
||||
node_version: Optional[str] = None
|
||||
xray_uptime: str
|
||||
|
||||
users_online: Optional[int] = None
|
||||
xray_uptime: float = 0
|
||||
users_online: Optional[float] = None
|
||||
|
||||
is_traffic_tracking_active: bool
|
||||
traffic_reset_day: Optional[int] = None
|
||||
|
|
@ -252,10 +298,6 @@ class NodeDto(BaseModel):
|
|||
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
|
||||
cpu_count: Optional[int] = None
|
||||
cpu_model: Optional[str] = None
|
||||
total_ram: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
|
@ -264,6 +306,10 @@ class NodeDto(BaseModel):
|
|||
provider_uuid: Optional[UUID] = None
|
||||
provider: Optional[InfraProviderDto] = None
|
||||
|
||||
active_plugin_uuid: Optional[UUID] = None
|
||||
system: Optional[NodeSystemDto] = None
|
||||
versions: Optional[NodeVersionsDto] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
# Backward-compat shims for code that used the flat fields directly
|
||||
|
|
@ -318,6 +364,21 @@ class CrmEventDto(BaseModel):
|
|||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
# ---------------- TORRENT BLOCKER EVENTS ---------------- #
|
||||
|
||||
class TorrentBlockerReportDto(BaseModel):
|
||||
node: NodeDto
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class TorrentBlockerEventDto(BaseModel):
|
||||
event_name: TTorrentBlockerEvents
|
||||
data: TorrentBlockerReportDto
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
# ---------------- WEBHOOK PAYLOAD ---------------- #
|
||||
|
||||
class WebhookPayloadDto(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
#!/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)
|
||||
|
|
@ -6,4 +6,5 @@ REMNAWAVE_INBOUND_UUID=
|
|||
REMNAWAVE_USER_UUID=
|
||||
REMNAWAVE_SHORT_UUID=
|
||||
REMNAWAVE_CONFIG_PROFILE_UUID=
|
||||
REMNAWAVE_USER_USERNAME=
|
||||
REMNAWAVE_USER_USERNAME=
|
||||
REMNAWAVE_NODE_UUID=
|
||||
|
|
@ -15,6 +15,7 @@ REMNAWAVE_CONFIG_PROFILE_UUID = os.getenv("REMNAWAVE_CONFIG_PROFILE_UUID")
|
|||
REMNAWAVE_USER_UUID = os.getenv("REMNAWAVE_USER_UUID")
|
||||
REMNAWAVE_SHORT_UUID = os.getenv("REMNAWAVE_SHORT_UUID")
|
||||
REMNAWAVE_USER_USERNAME = os.getenv("REMNAWAVE_USER_USERNAME")
|
||||
REMNAWAVE_NODE_UUID = os.getenv("REMNAWAVE_NODE_UUID")
|
||||
|
||||
@pytest.fixture
|
||||
async def remnawave() -> RemnawaveSDK:
|
||||
|
|
@ -44,4 +45,6 @@ async def remnawave() -> RemnawaveSDK:
|
|||
assert sdk.subscription_page_config is not None
|
||||
assert sdk.xray_config is not None
|
||||
assert sdk.hwid is not None
|
||||
assert sdk.node_plugins is not None
|
||||
assert sdk.metadata is not None
|
||||
return sdk
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import pytest
|
||||
from uuid import UUID
|
||||
|
||||
from remnawave.models import (
|
||||
# Legacy models (deprecated)
|
||||
|
|
@ -7,11 +6,10 @@ from remnawave.models import (
|
|||
GetNodesRealtimeUsageResponseDto,
|
||||
GetNodeUserUsageByRangeResponseDto,
|
||||
GetUserUsageByRangeResponseDto,
|
||||
|
||||
|
||||
# New stats models
|
||||
GetLegacyStatsUserUsageResponseDto,
|
||||
GetLegacyStatsNodesUsersUsageResponseDto,
|
||||
GetStatsNodesRealtimeUsageResponseDto,
|
||||
GetStatsNodesUsageResponseDto,
|
||||
GetStatsNodeUsersUsageResponseDto,
|
||||
GetStatsUserUsageResponseDto,
|
||||
|
|
@ -69,24 +67,6 @@ async def test_legacy_node_user_usage(remnawave):
|
|||
assert len(node_user_usage) >= 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats_nodes_realtime_usage(remnawave):
|
||||
"""Test new stats nodes realtime usage endpoint"""
|
||||
realtime_usage = await remnawave.bandwidthstats.get_nodes_realtime_usage()
|
||||
assert isinstance(realtime_usage, GetStatsNodesRealtimeUsageResponseDto)
|
||||
assert hasattr(realtime_usage, 'response')
|
||||
assert isinstance(realtime_usage.response, list)
|
||||
|
||||
# Check structure if data exists
|
||||
if realtime_usage.response:
|
||||
first_item = realtime_usage.response[0]
|
||||
assert hasattr(first_item, 'node_uuid')
|
||||
assert hasattr(first_item, 'node_name')
|
||||
assert hasattr(first_item, 'download_bytes')
|
||||
assert hasattr(first_item, 'upload_bytes')
|
||||
assert hasattr(first_item, 'total_bytes')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats_nodes_usage(remnawave):
|
||||
"""Test new stats nodes usage endpoint with charts"""
|
||||
|
|
@ -232,20 +212,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave):
|
|||
async def test_bandwidth_data_structure(remnawave):
|
||||
"""Test bandwidth stats data structure validity"""
|
||||
start, end = generate_date_range()
|
||||
|
||||
# Get realtime data
|
||||
realtime = await remnawave.bandwidthstats.get_nodes_realtime_usage()
|
||||
|
||||
if realtime.response:
|
||||
# Verify each node has required fields
|
||||
for node in realtime.response:
|
||||
assert isinstance(node.node_uuid, UUID)
|
||||
assert isinstance(node.node_name, str)
|
||||
assert isinstance(node.download_bytes, (int, float))
|
||||
assert isinstance(node.upload_bytes, (int, float))
|
||||
assert isinstance(node.total_bytes, (int, float))
|
||||
assert node.total_bytes >= 0
|
||||
|
||||
|
||||
# Get stats data
|
||||
stats = await remnawave.bandwidthstats.get_stats_nodes_usage(
|
||||
start=start,
|
||||
|
|
|
|||
119
tests/test_controllers_completeness.py
Normal file
119
tests/test_controllers_completeness.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Tests that all required endpoints exist in controllers."""
|
||||
import pytest
|
||||
import inspect
|
||||
|
||||
from remnawave.controllers.users import UsersController
|
||||
from remnawave.controllers.system import SystemController
|
||||
from remnawave.controllers.ip_control import IpControlController
|
||||
|
||||
|
||||
class TestUsersControllerEndpoints:
|
||||
def test_has_resolve_user(self):
|
||||
assert hasattr(UsersController, "resolve_user")
|
||||
assert callable(getattr(UsersController, "resolve_user"))
|
||||
|
||||
def test_has_revoke_user_subscription(self):
|
||||
assert hasattr(UsersController, "revoke_user_subscription")
|
||||
|
||||
def test_has_disable_user(self):
|
||||
assert hasattr(UsersController, "disable_user")
|
||||
|
||||
def test_has_enable_user(self):
|
||||
assert hasattr(UsersController, "enable_user")
|
||||
|
||||
def test_has_reset_user_traffic(self):
|
||||
assert hasattr(UsersController, "reset_user_traffic")
|
||||
|
||||
def test_has_create_user(self):
|
||||
assert hasattr(UsersController, "create_user")
|
||||
|
||||
def test_has_update_user(self):
|
||||
assert hasattr(UsersController, "update_user")
|
||||
|
||||
def test_has_delete_user(self):
|
||||
assert hasattr(UsersController, "delete_user")
|
||||
|
||||
def test_has_get_all_users(self):
|
||||
assert hasattr(UsersController, "get_all_users")
|
||||
|
||||
def test_has_get_user_by_uuid(self):
|
||||
assert hasattr(UsersController, "get_user_by_uuid")
|
||||
|
||||
def test_has_get_user_by_short_uuid(self):
|
||||
assert hasattr(UsersController, "get_user_by_short_uuid")
|
||||
|
||||
def test_has_get_user_by_username(self):
|
||||
assert hasattr(UsersController, "get_user_by_username")
|
||||
|
||||
def test_has_get_user_by_id(self):
|
||||
assert hasattr(UsersController, "get_user_by_id")
|
||||
|
||||
def test_has_get_users_by_telegram_id(self):
|
||||
assert hasattr(UsersController, "get_users_by_telegram_id")
|
||||
|
||||
def test_has_get_users_by_email(self):
|
||||
assert hasattr(UsersController, "get_users_by_email")
|
||||
|
||||
def test_has_get_users_by_tag(self):
|
||||
assert hasattr(UsersController, "get_users_by_tag")
|
||||
|
||||
def test_has_get_all_tags(self):
|
||||
assert hasattr(UsersController, "get_all_tags")
|
||||
|
||||
def test_has_get_user_accessible_nodes(self):
|
||||
assert hasattr(UsersController, "get_user_accessible_nodes")
|
||||
|
||||
def test_has_get_user_subscription_request_history(self):
|
||||
assert hasattr(UsersController, "get_user_subscription_request_history")
|
||||
|
||||
|
||||
class TestSystemControllerEndpoints:
|
||||
def test_has_get_recap(self):
|
||||
assert hasattr(SystemController, "get_recap")
|
||||
assert callable(getattr(SystemController, "get_recap"))
|
||||
|
||||
def test_has_get_metadata(self):
|
||||
assert hasattr(SystemController, "get_metadata")
|
||||
|
||||
def test_has_get_stats(self):
|
||||
assert hasattr(SystemController, "get_stats")
|
||||
|
||||
def test_has_get_bandwidth_stats(self):
|
||||
assert hasattr(SystemController, "get_bandwidth_stats")
|
||||
|
||||
def test_has_get_nodes_statistics(self):
|
||||
assert hasattr(SystemController, "get_nodes_statistics")
|
||||
|
||||
def test_has_get_health(self):
|
||||
assert hasattr(SystemController, "get_health")
|
||||
|
||||
def test_has_get_nodes_metrics(self):
|
||||
assert hasattr(SystemController, "get_nodes_metrics")
|
||||
|
||||
def test_has_get_x25519_key_pair(self):
|
||||
assert hasattr(SystemController, "get_x25519_key_pair")
|
||||
|
||||
def test_has_encrypt_happ_crypto_link(self):
|
||||
assert hasattr(SystemController, "encrypt_happ_crypto_link")
|
||||
|
||||
def test_has_debug_srr_matcher(self):
|
||||
assert hasattr(SystemController, "debug_srr_matcher")
|
||||
|
||||
|
||||
class TestIpControlControllerEndpoints:
|
||||
def test_has_fetch_user_ips(self):
|
||||
assert hasattr(IpControlController, "fetch_user_ips")
|
||||
|
||||
def test_has_get_fetch_ips_result(self):
|
||||
assert hasattr(IpControlController, "get_fetch_ips_result")
|
||||
|
||||
def test_has_fetch_users_ips(self):
|
||||
assert hasattr(IpControlController, "fetch_users_ips")
|
||||
assert callable(getattr(IpControlController, "fetch_users_ips"))
|
||||
|
||||
def test_has_get_fetch_users_ips_result(self):
|
||||
assert hasattr(IpControlController, "get_fetch_users_ips_result")
|
||||
assert callable(getattr(IpControlController, "get_fetch_users_ips_result"))
|
||||
|
||||
def test_has_drop_connections(self):
|
||||
assert hasattr(IpControlController, "drop_connections")
|
||||
131
tests/test_enums.py
Normal file
131
tests/test_enums.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Tests for enum completeness against the OpenAPI spec."""
|
||||
import pytest
|
||||
|
||||
from remnawave.enums import (
|
||||
ALPN,
|
||||
ClientType,
|
||||
Fingerprint,
|
||||
OAuth2Provider,
|
||||
SecurityLayer,
|
||||
TemplateType,
|
||||
TrafficLimitStrategy,
|
||||
UserStatus,
|
||||
ResponseRuleConditionOperator,
|
||||
ResponseRuleOperator,
|
||||
ResponseRuleVersion,
|
||||
ResponseType,
|
||||
SubscriptionType,
|
||||
TTorrentBlockerEvents,
|
||||
)
|
||||
|
||||
|
||||
class TestOAuth2Provider:
|
||||
def test_has_telegram(self):
|
||||
assert OAuth2Provider.TELEGRAM == "telegram"
|
||||
|
||||
def test_has_generic(self):
|
||||
assert OAuth2Provider.GENERIC == "generic"
|
||||
|
||||
def test_has_github(self):
|
||||
assert OAuth2Provider.GITHUB == "github"
|
||||
|
||||
def test_has_pocketid(self):
|
||||
assert OAuth2Provider.POCKETID == "pocketid"
|
||||
|
||||
def test_has_yandex(self):
|
||||
assert OAuth2Provider.YANDEX == "yandex"
|
||||
|
||||
def test_has_keycloak(self):
|
||||
assert OAuth2Provider.KEYCLOAK == "keycloak"
|
||||
|
||||
def test_all_values(self):
|
||||
expected = {"telegram", "github", "pocketid", "yandex", "keycloak", "generic"}
|
||||
actual = {v.value for v in OAuth2Provider}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestTemplateType:
|
||||
def test_has_xray_base64(self):
|
||||
assert TemplateType.XRAY_BASE64 == "XRAY_BASE64"
|
||||
|
||||
def test_has_xray_json(self):
|
||||
assert TemplateType.XRAY_JSON == "XRAY_JSON"
|
||||
|
||||
def test_all_api_values(self):
|
||||
api_values = {"XRAY_JSON", "XRAY_BASE64", "MIHOMO", "STASH", "CLASH", "SINGBOX"}
|
||||
actual = {v.value for v in TemplateType}
|
||||
assert api_values.issubset(actual)
|
||||
|
||||
|
||||
class TestTrafficLimitStrategy:
|
||||
def test_has_month_rolling(self):
|
||||
assert TrafficLimitStrategy.MONTH_ROLLING == "MONTH_ROLLING"
|
||||
|
||||
def test_all_api_values(self):
|
||||
api_values = {"NO_RESET", "DAY", "WEEK", "MONTH", "MONTH_ROLLING"}
|
||||
actual = {v.value for v in TrafficLimitStrategy}
|
||||
assert api_values == actual
|
||||
|
||||
|
||||
class TestUserStatus:
|
||||
def test_all_values(self):
|
||||
expected = {"ACTIVE", "DISABLED", "LIMITED", "EXPIRED"}
|
||||
actual = {v.value for v in UserStatus}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestClientType:
|
||||
def test_api_values_present(self):
|
||||
api_values = {"stash", "singbox", "mihomo", "json", "v2ray-json", "clash"}
|
||||
actual = {v.value for v in ClientType}
|
||||
assert api_values.issubset(actual)
|
||||
|
||||
|
||||
class TestSecurityLayer:
|
||||
def test_all_values(self):
|
||||
expected = {"DEFAULT", "TLS", "NONE"}
|
||||
actual = {v.value for v in SecurityLayer}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestFingerprint:
|
||||
def test_all_api_values(self):
|
||||
api_values = {"chrome", "firefox", "safari", "ios", "android", "edge", "qq", "random", "randomized"}
|
||||
actual = {v.value for v in Fingerprint}
|
||||
assert api_values == actual
|
||||
|
||||
|
||||
class TestALPN:
|
||||
def test_has_h3(self):
|
||||
assert "h3" in {v.value for v in ALPN}
|
||||
|
||||
def test_has_h2(self):
|
||||
assert "h2" in {v.value for v in ALPN}
|
||||
|
||||
|
||||
class TestResponseRuleOperator:
|
||||
def test_all_values(self):
|
||||
expected = {"AND", "OR"}
|
||||
actual = {v.value for v in ResponseRuleOperator}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestResponseRuleConditionOperator:
|
||||
def test_all_values(self):
|
||||
expected = {
|
||||
"EQUALS", "NOT_EQUALS", "CONTAINS", "NOT_CONTAINS",
|
||||
"STARTS_WITH", "NOT_STARTS_WITH", "ENDS_WITH", "NOT_ENDS_WITH",
|
||||
"REGEX", "NOT_REGEX",
|
||||
}
|
||||
actual = {v.value for v in ResponseRuleConditionOperator}
|
||||
assert actual == expected
|
||||
|
||||
|
||||
class TestResponseType:
|
||||
def test_all_values(self):
|
||||
expected = {
|
||||
"XRAY_JSON", "XRAY_BASE64", "MIHOMO", "STASH", "CLASH", "SINGBOX",
|
||||
"BROWSER", "BLOCK", "STATUS_CODE_404", "STATUS_CODE_451", "SOCKET_DROP",
|
||||
}
|
||||
actual = {v.value for v in ResponseType}
|
||||
assert actual == expected
|
||||
218
tests/test_metadata.py
Normal file
218
tests/test_metadata.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from remnawave.exceptions import NotFoundError
|
||||
from remnawave.models import (
|
||||
CreateUserRequestDto,
|
||||
GetNodeMetadataResponseDto,
|
||||
GetUserMetadataResponseDto,
|
||||
UpsertNodeMetadataRequestBodyDto,
|
||||
UpsertNodeMetadataResponseDto,
|
||||
UpsertUserMetadataRequestBodyDto,
|
||||
UpsertUserMetadataResponseDto,
|
||||
)
|
||||
from tests.utils import generate_random_string
|
||||
|
||||
|
||||
REMNAWAVE_NODE_UUID = os.getenv("REMNAWAVE_NODE_UUID")
|
||||
|
||||
|
||||
class TestUserMetadata:
|
||||
"""Тесты для User Metadata контроллера"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_and_get_user_metadata(self, remnawave):
|
||||
"""Тест создания/обновления и получения метаданных пользователя"""
|
||||
# Create test user first
|
||||
username = generate_random_string(length=8)
|
||||
expire_at = datetime.now(timezone.utc) + timedelta(days=7)
|
||||
create_user = await remnawave.users.create_user(
|
||||
CreateUserRequestDto(username=username, expire_at=expire_at)
|
||||
)
|
||||
user_uuid = str(create_user.uuid)
|
||||
|
||||
try:
|
||||
# Upsert metadata
|
||||
test_metadata = {
|
||||
"custom_field_1": "test_value_1",
|
||||
"custom_field_2": 123,
|
||||
"nested": {
|
||||
"field": "value"
|
||||
}
|
||||
}
|
||||
|
||||
upsert_response = await remnawave.metadata.upsert_user_metadata(
|
||||
uuid=user_uuid,
|
||||
body=UpsertUserMetadataRequestBodyDto(metadata=test_metadata)
|
||||
)
|
||||
|
||||
assert isinstance(upsert_response, UpsertUserMetadataResponseDto)
|
||||
|
||||
# Get metadata
|
||||
get_response = await remnawave.metadata.get_user_metadata(uuid=user_uuid)
|
||||
|
||||
assert isinstance(get_response, GetUserMetadataResponseDto)
|
||||
assert hasattr(get_response, "metadata")
|
||||
assert get_response.metadata is not None
|
||||
assert get_response.metadata.get("custom_field_1") == "test_value_1"
|
||||
assert get_response.metadata.get("custom_field_2") == 123
|
||||
assert get_response.metadata.get("nested") == {"field": "value"}
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
await remnawave.users.delete_user(uuid=user_uuid)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_existing_user_metadata(self, remnawave):
|
||||
"""Тест обновления существующих метаданных пользователя"""
|
||||
# Create test user
|
||||
username = generate_random_string(length=8)
|
||||
expire_at = datetime.now(timezone.utc) + timedelta(days=7)
|
||||
create_user = await remnawave.users.create_user(
|
||||
CreateUserRequestDto(username=username, expire_at=expire_at)
|
||||
)
|
||||
user_uuid = str(create_user.uuid)
|
||||
|
||||
try:
|
||||
# Initial metadata
|
||||
initial_metadata = {"field1": "value1"}
|
||||
await remnawave.metadata.upsert_user_metadata(
|
||||
uuid=user_uuid,
|
||||
body=UpsertUserMetadataRequestBodyDto(metadata=initial_metadata)
|
||||
)
|
||||
|
||||
# Update metadata
|
||||
updated_metadata = {
|
||||
"field1": "updated_value1",
|
||||
"field2": "value2"
|
||||
}
|
||||
upsert_response = await remnawave.metadata.upsert_user_metadata(
|
||||
uuid=user_uuid,
|
||||
body=UpsertUserMetadataRequestBodyDto(metadata=updated_metadata)
|
||||
)
|
||||
|
||||
assert isinstance(upsert_response, UpsertUserMetadataResponseDto)
|
||||
|
||||
# Verify update
|
||||
get_response = await remnawave.metadata.get_user_metadata(uuid=user_uuid)
|
||||
assert get_response.metadata.get("field1") == "updated_value1"
|
||||
assert get_response.metadata.get("field2") == "value2"
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
await remnawave.users.delete_user(uuid=user_uuid)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_metadata_empty(self, remnawave):
|
||||
"""Тест получения пустых метаданных пользователя"""
|
||||
# Create test user without metadata
|
||||
username = generate_random_string(length=8)
|
||||
expire_at = datetime.now(timezone.utc) + timedelta(days=7)
|
||||
create_user = await remnawave.users.create_user(
|
||||
CreateUserRequestDto(username=username, expire_at=expire_at)
|
||||
)
|
||||
user_uuid = str(create_user.uuid)
|
||||
|
||||
try:
|
||||
# Get metadata (API может вернуть пустой объект или 404, если метаданные не созданы)
|
||||
try:
|
||||
get_response = await remnawave.metadata.get_user_metadata(uuid=user_uuid)
|
||||
assert isinstance(get_response, GetUserMetadataResponseDto)
|
||||
assert get_response.metadata is None or get_response.metadata == {}
|
||||
except NotFoundError:
|
||||
# Ожидаемое поведение для пользователя без метаданных
|
||||
assert True
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
await remnawave.users.delete_user(uuid=user_uuid)
|
||||
|
||||
|
||||
class TestNodeMetadata:
|
||||
"""Тесты для Node Metadata контроллера"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_and_get_node_metadata(self, remnawave):
|
||||
"""Тест создания/обновления и получения метаданных ноды"""
|
||||
# Skip if no node UUID configured
|
||||
if not REMNAWAVE_NODE_UUID:
|
||||
pytest.skip("REMNAWAVE_NODE_UUID not set in environment")
|
||||
|
||||
node_uuid = REMNAWAVE_NODE_UUID
|
||||
|
||||
# Upsert metadata
|
||||
test_metadata = {
|
||||
"location": "datacenter-1",
|
||||
"region": "us-east",
|
||||
"capacity": 1000,
|
||||
"config": {
|
||||
"max_users": 100
|
||||
}
|
||||
}
|
||||
|
||||
upsert_response = await remnawave.metadata.upsert_node_metadata(
|
||||
uuid=node_uuid,
|
||||
body=UpsertNodeMetadataRequestBodyDto(metadata=test_metadata)
|
||||
)
|
||||
|
||||
assert isinstance(upsert_response, UpsertNodeMetadataResponseDto)
|
||||
|
||||
# Get metadata
|
||||
get_response = await remnawave.metadata.get_node_metadata(uuid=node_uuid)
|
||||
|
||||
assert isinstance(get_response, GetNodeMetadataResponseDto)
|
||||
assert hasattr(get_response, "metadata")
|
||||
assert get_response.metadata is not None
|
||||
assert get_response.metadata.get("location") == "datacenter-1"
|
||||
assert get_response.metadata.get("region") == "us-east"
|
||||
assert get_response.metadata.get("capacity") == 1000
|
||||
assert get_response.metadata.get("config") == {"max_users": 100}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_existing_node_metadata(self, remnawave):
|
||||
"""Тест обновления существующих метаданных ноды"""
|
||||
if not REMNAWAVE_NODE_UUID:
|
||||
pytest.skip("REMNAWAVE_NODE_UUID not set in environment")
|
||||
|
||||
node_uuid = REMNAWAVE_NODE_UUID
|
||||
|
||||
# Initial metadata
|
||||
initial_metadata = {"status": "active"}
|
||||
await remnawave.metadata.upsert_node_metadata(
|
||||
uuid=node_uuid,
|
||||
body=UpsertNodeMetadataRequestBodyDto(metadata=initial_metadata)
|
||||
)
|
||||
|
||||
# Update metadata
|
||||
updated_metadata = {
|
||||
"status": "maintenance",
|
||||
"last_check": "2026-03-09"
|
||||
}
|
||||
upsert_response = await remnawave.metadata.upsert_node_metadata(
|
||||
uuid=node_uuid,
|
||||
body=UpsertNodeMetadataRequestBodyDto(metadata=updated_metadata)
|
||||
)
|
||||
|
||||
assert isinstance(upsert_response, UpsertNodeMetadataResponseDto)
|
||||
|
||||
# Verify update
|
||||
get_response = await remnawave.metadata.get_node_metadata(uuid=node_uuid)
|
||||
assert get_response.metadata.get("status") == "maintenance"
|
||||
assert get_response.metadata.get("last_check") == "2026-03-09"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_node_metadata(self, remnawave):
|
||||
"""Тест получения метаданных ноды"""
|
||||
if not REMNAWAVE_NODE_UUID:
|
||||
pytest.skip("REMNAWAVE_NODE_UUID not set in environment")
|
||||
|
||||
node_uuid = REMNAWAVE_NODE_UUID
|
||||
|
||||
# Get metadata
|
||||
get_response = await remnawave.metadata.get_node_metadata(uuid=node_uuid)
|
||||
|
||||
assert isinstance(get_response, GetNodeMetadataResponseDto)
|
||||
assert hasattr(get_response, "metadata")
|
||||
# Metadata can be empty dict or None or contain data depending on previous tests
|
||||
277
tests/test_models_validation.py
Normal file
277
tests/test_models_validation.py
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
"""Tests for model field validation and serialization."""
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from remnawave.models import (
|
||||
# Users
|
||||
ResolveUserRequestBodyDto,
|
||||
ResolveUserResponseDto,
|
||||
RevokeUserRequestDto,
|
||||
# System
|
||||
GetRecapResponseDto,
|
||||
RecapThisMonth,
|
||||
RecapTotal,
|
||||
# IP Control
|
||||
FetchUsersIpsResponseDto,
|
||||
FetchUsersIpsResultResponseDto,
|
||||
FetchUsersIpsUserIp,
|
||||
FetchUsersIpsUser,
|
||||
FetchUsersIpsResult,
|
||||
DropConnectionsRequestDto,
|
||||
DropByUserUuids,
|
||||
DropByIpAddresses,
|
||||
TargetAllNodes,
|
||||
TargetSpecificNodes,
|
||||
# Infra Billing
|
||||
CreateInfraBillingHistoryRecordRequestDto,
|
||||
CreateInfraBillingNodeRequestDto,
|
||||
# Subscription Settings
|
||||
ResponseRules,
|
||||
ResponseRulesSettings,
|
||||
# Webhook
|
||||
NodeSystemDto,
|
||||
NodeSystemInfoDto,
|
||||
NodeSystemStatsDto,
|
||||
NodeVersionsDto,
|
||||
)
|
||||
from remnawave.enums import ResponseRuleVersion
|
||||
|
||||
|
||||
class TestResolveUserRequestBodyDto:
|
||||
def test_create_with_uuid(self):
|
||||
uid = uuid4()
|
||||
dto = ResolveUserRequestBodyDto(uuid=uid)
|
||||
assert dto.uuid == uid
|
||||
assert dto.id is None
|
||||
assert dto.username is None
|
||||
|
||||
def test_create_with_username(self):
|
||||
dto = ResolveUserRequestBodyDto(username="testuser")
|
||||
assert dto.username == "testuser"
|
||||
assert dto.uuid is None
|
||||
|
||||
def test_create_with_short_uuid(self):
|
||||
dto = ResolveUserRequestBodyDto(short_uuid="abc123")
|
||||
assert dto.short_uuid == "abc123"
|
||||
|
||||
def test_serialization_alias(self):
|
||||
dto = ResolveUserRequestBodyDto(short_uuid="abc123")
|
||||
data = dto.model_dump(by_alias=True)
|
||||
assert "shortUuid" in data
|
||||
|
||||
def test_create_with_id(self):
|
||||
dto = ResolveUserRequestBodyDto(id=42)
|
||||
assert dto.id == 42
|
||||
|
||||
|
||||
class TestResolveUserResponseDto:
|
||||
def test_from_api_response(self):
|
||||
uid = uuid4()
|
||||
dto = ResolveUserResponseDto(
|
||||
uuid=uid,
|
||||
username="testuser",
|
||||
id=1,
|
||||
shortUuid="abc123",
|
||||
)
|
||||
assert dto.uuid == uid
|
||||
assert dto.username == "testuser"
|
||||
assert dto.id == 1
|
||||
assert dto.short_uuid == "abc123"
|
||||
|
||||
|
||||
class TestGetRecapResponseDto:
|
||||
def test_from_api_response(self):
|
||||
dto = GetRecapResponseDto(
|
||||
thisMonth={"users": 10, "traffic": "1.5 GB"},
|
||||
total={
|
||||
"users": 100,
|
||||
"nodes": 5,
|
||||
"traffic": "500 GB",
|
||||
"nodesRam": "32 GB",
|
||||
"nodesCpuCores": 16,
|
||||
"distinctCountries": 3,
|
||||
},
|
||||
version="1.11.0",
|
||||
initDate="2025-01-01T00:00:00Z",
|
||||
)
|
||||
assert dto.this_month.users == 10
|
||||
assert dto.this_month.traffic == "1.5 GB"
|
||||
assert dto.total.nodes == 5
|
||||
assert dto.total.nodes_ram == "32 GB"
|
||||
assert dto.total.nodes_cpu_cores == 16
|
||||
assert dto.total.distinct_countries == 3
|
||||
assert dto.version == "1.11.0"
|
||||
assert isinstance(dto.init_date, datetime)
|
||||
|
||||
|
||||
class TestFetchUsersIpsModels:
|
||||
def test_response_dto(self):
|
||||
dto = FetchUsersIpsResponseDto(jobId="job-123")
|
||||
assert dto.job_id == "job-123"
|
||||
|
||||
def test_result_not_completed(self):
|
||||
dto = FetchUsersIpsResultResponseDto(
|
||||
isCompleted=False,
|
||||
isFailed=False,
|
||||
result=None,
|
||||
)
|
||||
assert dto.is_completed is False
|
||||
assert dto.is_failed is False
|
||||
assert dto.result is None
|
||||
|
||||
def test_result_completed(self):
|
||||
uid = uuid4()
|
||||
dto = FetchUsersIpsResultResponseDto(
|
||||
isCompleted=True,
|
||||
isFailed=False,
|
||||
result={
|
||||
"success": True,
|
||||
"nodeUuid": str(uid),
|
||||
"users": [
|
||||
{
|
||||
"userId": "user-1",
|
||||
"ips": [
|
||||
{"ip": "1.2.3.4", "lastSeen": "2025-01-01T00:00:00Z"},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
assert dto.is_completed is True
|
||||
assert dto.result.success is True
|
||||
assert dto.result.node_uuid == uid
|
||||
assert len(dto.result.users) == 1
|
||||
assert dto.result.users[0].user_id == "user-1"
|
||||
assert dto.result.users[0].ips[0].ip == "1.2.3.4"
|
||||
|
||||
|
||||
class TestCreateInfraBillingHistoryRecordRequestDto:
|
||||
def test_fields_match_spec(self):
|
||||
uid = uuid4()
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
dto = CreateInfraBillingHistoryRecordRequestDto(
|
||||
provider_uuid=uid,
|
||||
amount=29.99,
|
||||
billed_at=now,
|
||||
)
|
||||
assert dto.provider_uuid == uid
|
||||
assert dto.amount == 29.99
|
||||
assert dto.billed_at == now
|
||||
|
||||
def test_serialization(self):
|
||||
uid = uuid4()
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
dto = CreateInfraBillingHistoryRecordRequestDto(
|
||||
provider_uuid=uid,
|
||||
amount=10.0,
|
||||
billed_at=now,
|
||||
)
|
||||
data = dto.model_dump(by_alias=True)
|
||||
assert "providerUuid" in data
|
||||
assert "billedAt" in data
|
||||
assert "amount" in data
|
||||
|
||||
def test_no_old_fields(self):
|
||||
"""Ensure removed fields don't exist."""
|
||||
assert not hasattr(CreateInfraBillingHistoryRecordRequestDto, "node_uuid")
|
||||
assert not hasattr(CreateInfraBillingHistoryRecordRequestDto, "payment_date")
|
||||
assert not hasattr(CreateInfraBillingHistoryRecordRequestDto, "description")
|
||||
|
||||
|
||||
class TestCreateInfraBillingNodeRequestDto:
|
||||
def test_next_billing_at_optional(self):
|
||||
dto = CreateInfraBillingNodeRequestDto(
|
||||
node_uuid=uuid4(),
|
||||
provider_uuid=uuid4(),
|
||||
)
|
||||
assert dto.next_billing_at is None
|
||||
|
||||
def test_next_billing_at_provided(self):
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
dto = CreateInfraBillingNodeRequestDto(
|
||||
node_uuid=uuid4(),
|
||||
provider_uuid=uuid4(),
|
||||
next_billing_at=now,
|
||||
)
|
||||
assert dto.next_billing_at == now
|
||||
|
||||
|
||||
class TestResponseRulesSettings:
|
||||
def test_settings_field_exists(self):
|
||||
rules = ResponseRules(
|
||||
version=ResponseRuleVersion.V1,
|
||||
rules=[],
|
||||
settings=ResponseRulesSettings(
|
||||
disable_subscription_access_by_path=True,
|
||||
),
|
||||
)
|
||||
assert rules.settings is not None
|
||||
assert rules.settings.disable_subscription_access_by_path is True
|
||||
|
||||
def test_settings_optional(self):
|
||||
rules = ResponseRules(
|
||||
version=ResponseRuleVersion.V1,
|
||||
rules=[],
|
||||
)
|
||||
assert rules.settings is None
|
||||
|
||||
def test_settings_deserialization(self):
|
||||
rules = ResponseRules.model_validate({
|
||||
"version": "1",
|
||||
"rules": [],
|
||||
"settings": {"disableSubscriptionAccessByPath": False},
|
||||
})
|
||||
assert rules.settings.disable_subscription_access_by_path is False
|
||||
|
||||
|
||||
class TestWebhookNodeDto:
|
||||
def test_system_field(self):
|
||||
system = NodeSystemDto.model_validate({
|
||||
"info": {
|
||||
"arch": "x64",
|
||||
"cpus": 4,
|
||||
"cpuModel": "Intel Core i7",
|
||||
"memoryTotal": 16384,
|
||||
"hostname": "node-1",
|
||||
"platform": "linux",
|
||||
"release": "5.15.0",
|
||||
"type": "Linux",
|
||||
"version": "#1 SMP",
|
||||
"networkInterfaces": ["eth0", "lo"],
|
||||
},
|
||||
"stats": {
|
||||
"memoryFree": 8192,
|
||||
"memoryUsed": 8192,
|
||||
"uptime": 3600,
|
||||
"loadAvg": [0.5, 0.3, 0.1],
|
||||
"interface": None,
|
||||
},
|
||||
})
|
||||
assert system.info.arch == "x64"
|
||||
assert system.info.cpus == 4
|
||||
assert system.info.cpu_model == "Intel Core i7"
|
||||
assert system.stats.memory_free == 8192
|
||||
assert system.stats.uptime == 3600
|
||||
|
||||
def test_versions_field(self):
|
||||
versions = NodeVersionsDto.model_validate({
|
||||
"xray": "1.8.6",
|
||||
"node": "0.5.0",
|
||||
})
|
||||
assert versions.xray == "1.8.6"
|
||||
assert versions.node == "0.5.0"
|
||||
|
||||
def test_node_dto_has_new_fields(self):
|
||||
from remnawave.models.webhook import NodeDto
|
||||
|
||||
fields = NodeDto.model_fields
|
||||
assert "active_plugin_uuid" in fields
|
||||
assert "system" in fields
|
||||
assert "versions" in fields
|
||||
|
||||
def test_node_dto_xray_uptime_is_float(self):
|
||||
from remnawave.models.webhook import NodeDto
|
||||
|
||||
field = NodeDto.model_fields["xray_uptime"]
|
||||
assert field.annotation == float or field.annotation is float
|
||||
257
tests/test_node_plugins.py
Normal file
257
tests/test_node_plugins.py
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import pytest
|
||||
|
||||
from remnawave.exceptions import NotFoundError
|
||||
from remnawave.models import (
|
||||
CloneNodePluginRequestDto,
|
||||
CloneNodePluginResponseDto,
|
||||
CreateNodePluginRequestDto,
|
||||
CreateNodePluginResponseDto,
|
||||
DeleteNodePluginResponseDto,
|
||||
GetNodePluginResponseDto,
|
||||
GetNodePluginsResponseDto,
|
||||
GetTorrentBlockerReportsResponseDto,
|
||||
GetTorrentBlockerReportsStatsResponseDto,
|
||||
PluginExecutorRequestDto,
|
||||
PluginExecutorResponseDto,
|
||||
ReorderNodePluginsRequestDto,
|
||||
ReorderNodePluginsResponseDto,
|
||||
TruncateTorrentBlockerReportsResponseDto,
|
||||
UpdateNodePluginRequestDto,
|
||||
UpdateNodePluginResponseDto,
|
||||
BlockIpsCommandDto,
|
||||
BlockIpItemDto,
|
||||
ReorderNodePluginItem,
|
||||
TargetAllNodesDto,
|
||||
)
|
||||
from tests.utils import generate_random_string
|
||||
|
||||
|
||||
class TestNodePlugins:
|
||||
"""Тесты для Node Plugins контроллера"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_node_plugins(self, remnawave):
|
||||
"""Тест получения списка всех Node Plugins"""
|
||||
response = await remnawave.node_plugins.get_all_node_plugins()
|
||||
|
||||
assert isinstance(response, GetNodePluginsResponseDto)
|
||||
assert hasattr(response, "node_plugins")
|
||||
assert isinstance(response.node_plugins, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_and_delete_node_plugin(self, remnawave):
|
||||
"""Тест создания и удаления Node Plugin"""
|
||||
plugin_name = f"test_plugin_{generate_random_string(length=8)}"
|
||||
|
||||
# Create plugin
|
||||
create_response = await remnawave.node_plugins.create_node_plugin(
|
||||
CreateNodePluginRequestDto(
|
||||
name=plugin_name
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(create_response, CreateNodePluginResponseDto)
|
||||
assert create_response.uuid is not None
|
||||
plugin_uuid = str(create_response.uuid)
|
||||
|
||||
# Get plugin by UUID to verify creation
|
||||
get_response = await remnawave.node_plugins.get_node_plugin_by_uuid(uuid=plugin_uuid)
|
||||
assert isinstance(get_response, GetNodePluginResponseDto)
|
||||
assert get_response.name == plugin_name
|
||||
|
||||
# Delete plugin
|
||||
delete_response = await remnawave.node_plugins.delete_node_plugin(uuid=plugin_uuid)
|
||||
assert isinstance(delete_response, DeleteNodePluginResponseDto)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_node_plugin(self, remnawave):
|
||||
"""Тест обновления Node Plugin"""
|
||||
plugin_name = f"test_plugin_{generate_random_string(length=8)}"
|
||||
|
||||
# Create plugin first
|
||||
create_response = await remnawave.node_plugins.create_node_plugin(
|
||||
CreateNodePluginRequestDto(
|
||||
name=plugin_name
|
||||
)
|
||||
)
|
||||
plugin_uuid = str(create_response.uuid)
|
||||
|
||||
try:
|
||||
# Update plugin
|
||||
updated_name = f"updated_{plugin_name}"
|
||||
update_response = await remnawave.node_plugins.update_node_plugin(
|
||||
UpdateNodePluginRequestDto(
|
||||
uuid=plugin_uuid,
|
||||
name=updated_name,
|
||||
plugin_config={"enabled": False}
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(update_response, UpdateNodePluginResponseDto)
|
||||
|
||||
# Verify update
|
||||
get_response = await remnawave.node_plugins.get_node_plugin_by_uuid(uuid=plugin_uuid)
|
||||
assert get_response.name == updated_name
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
await remnawave.node_plugins.delete_node_plugin(uuid=plugin_uuid)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reorder_node_plugins(self, remnawave):
|
||||
"""Тест изменения порядка Node Plugins"""
|
||||
# Create two plugins
|
||||
plugin1_name = f"test_plugin_1_{generate_random_string(length=6)}"
|
||||
plugin2_name = f"test_plugin_2_{generate_random_string(length=6)}"
|
||||
|
||||
create1 = await remnawave.node_plugins.create_node_plugin(
|
||||
CreateNodePluginRequestDto(
|
||||
name=plugin1_name
|
||||
)
|
||||
)
|
||||
uuid1 = str(create1.uuid)
|
||||
|
||||
create2 = await remnawave.node_plugins.create_node_plugin(
|
||||
CreateNodePluginRequestDto(
|
||||
name=plugin2_name
|
||||
)
|
||||
)
|
||||
uuid2 = str(create2.uuid)
|
||||
|
||||
try:
|
||||
# Reorder plugins
|
||||
reorder_response = await remnawave.node_plugins.reorder_node_plugins(
|
||||
ReorderNodePluginsRequestDto(
|
||||
items=[
|
||||
ReorderNodePluginItem(view_position=0, uuid=uuid2),
|
||||
ReorderNodePluginItem(view_position=1, uuid=uuid1),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(reorder_response, ReorderNodePluginsResponseDto)
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
await remnawave.node_plugins.delete_node_plugin(uuid=uuid1)
|
||||
await remnawave.node_plugins.delete_node_plugin(uuid=uuid2)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clone_node_plugin(self, remnawave):
|
||||
"""Тест клонирования Node Plugin"""
|
||||
plugin_name = f"test_plugin_{generate_random_string(length=8)}"
|
||||
|
||||
# Create plugin
|
||||
create_response = await remnawave.node_plugins.create_node_plugin(
|
||||
CreateNodePluginRequestDto(
|
||||
name=plugin_name
|
||||
)
|
||||
)
|
||||
original_uuid = str(create_response.uuid)
|
||||
|
||||
try:
|
||||
# Clone plugin
|
||||
clone_response = await remnawave.node_plugins.clone_node_plugin(
|
||||
CloneNodePluginRequestDto(
|
||||
clone_from_uuid=original_uuid,
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(clone_response, CloneNodePluginResponseDto)
|
||||
cloned_uuid = str(clone_response.uuid)
|
||||
|
||||
# Verify clone
|
||||
get_cloned = await remnawave.node_plugins.get_node_plugin_by_uuid(uuid=cloned_uuid)
|
||||
assert get_cloned.uuid == clone_response.uuid
|
||||
|
||||
# Cleanup cloned plugin
|
||||
await remnawave.node_plugins.delete_node_plugin(uuid=cloned_uuid)
|
||||
|
||||
finally:
|
||||
# Cleanup original plugin
|
||||
await remnawave.node_plugins.delete_node_plugin(uuid=original_uuid)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_executor(self, remnawave):
|
||||
"""Тест выполнения команды на плагинах"""
|
||||
# This test assumes there's at least one node plugin configured
|
||||
# Create a test plugin first
|
||||
plugin_name = f"test_plugin_{generate_random_string(length=8)}"
|
||||
|
||||
create_response = await remnawave.node_plugins.create_node_plugin(
|
||||
CreateNodePluginRequestDto(
|
||||
name=plugin_name
|
||||
)
|
||||
)
|
||||
plugin_uuid = str(create_response.uuid)
|
||||
|
||||
try:
|
||||
# Execute command
|
||||
try:
|
||||
executor_response = await remnawave.node_plugins.plugin_executor(
|
||||
PluginExecutorRequestDto(
|
||||
command=BlockIpsCommandDto(
|
||||
command="blockIps",
|
||||
ips=[
|
||||
BlockIpItemDto(ip="192.168.1.1", timeout=60),
|
||||
BlockIpItemDto(ip="10.0.0.1", timeout=60),
|
||||
],
|
||||
),
|
||||
target_nodes=TargetAllNodesDto(target="allNodes"),
|
||||
)
|
||||
)
|
||||
assert isinstance(executor_response, PluginExecutorResponseDto)
|
||||
except NotFoundError:
|
||||
# В тестовых окружениях без подключенных нод API может вернуть 404
|
||||
pytest.skip("Node plugins executor is unavailable in this environment (no connected nodes)")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
await remnawave.node_plugins.delete_node_plugin(uuid=plugin_uuid)
|
||||
|
||||
|
||||
class TestTorrentBlocker:
|
||||
"""Тесты для Torrent Blocker функциональности"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_torrent_blocker_reports(self, remnawave):
|
||||
"""Тест получения отчетов Torrent Blocker"""
|
||||
response = await remnawave.node_plugins.get_torrent_blocker_reports(
|
||||
size=10,
|
||||
start=0
|
||||
)
|
||||
|
||||
assert isinstance(response, GetTorrentBlockerReportsResponseDto)
|
||||
assert hasattr(response, "records")
|
||||
assert isinstance(response.records, list)
|
||||
assert hasattr(response, "total")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_torrent_blocker_reports_without_pagination(self, remnawave):
|
||||
"""Тест получения отчетов Torrent Blocker без пагинации"""
|
||||
response = await remnawave.node_plugins.get_torrent_blocker_reports()
|
||||
|
||||
assert isinstance(response, GetTorrentBlockerReportsResponseDto)
|
||||
assert hasattr(response, "records")
|
||||
assert isinstance(response.records, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_torrent_blocker_stats(self, remnawave):
|
||||
"""Тест получения статистики Torrent Blocker"""
|
||||
response = await remnawave.node_plugins.get_torrent_blocker_reports_stats()
|
||||
|
||||
assert isinstance(response, GetTorrentBlockerReportsStatsResponseDto)
|
||||
assert hasattr(response, "stats")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_truncate_torrent_blocker_reports(self, remnawave):
|
||||
"""Тест очистки отчетов Torrent Blocker"""
|
||||
# This is a destructive operation, so be careful
|
||||
# Only run in test environment
|
||||
response = await remnawave.node_plugins.truncate_torrent_blocker_reports()
|
||||
|
||||
assert isinstance(response, TruncateTorrentBlockerReportsResponseDto)
|
||||
|
||||
# Verify truncation by checking reports are empty
|
||||
reports = await remnawave.node_plugins.get_torrent_blocker_reports()
|
||||
assert len(reports.records) == 0
|
||||
|
|
@ -50,11 +50,16 @@ class TestSubscriptionContent:
|
|||
@pytest.mark.asyncio
|
||||
async def test_get_subscription_with_type(self, remnawave):
|
||||
"""Тест получения подписки с типом"""
|
||||
subscription_with_type = await remnawave.subscription.get_subscription_with_type(
|
||||
short_uuid=REMNAWAVE_SHORT_UUID
|
||||
)
|
||||
assert isinstance(subscription_with_type, str)
|
||||
assert len(subscription_with_type) > 0
|
||||
try:
|
||||
subscription_with_type = await remnawave.subscription.get_subscription_with_type(
|
||||
short_uuid=REMNAWAVE_SHORT_UUID
|
||||
)
|
||||
assert isinstance(subscription_with_type, str)
|
||||
assert len(subscription_with_type) > 0
|
||||
except ApiError as e:
|
||||
if e.error.code == "HTTP_404":
|
||||
pytest.skip("Outline subscription endpoint is unavailable in this environment")
|
||||
raise
|
||||
|
||||
|
||||
class TestSubscriptionsManagement:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue