feat: Enhance subscription and external squad management

- Updated NodeUsageDto to include node_uuid and changed date to datetime.
- Changed total fields in subscription and subscription request history models from float to int.
- Introduced response rules and conditions for subscription settings, including new models for response modifications.
- Added external squads management with CRUD operations and associated models.
- Implemented passkey management with registration and verification endpoints.
- Enhanced snippets management with full CRUD operations and validation.
- Added OAuth2 provider enum for better authentication handling.
This commit is contained in:
Artem 2025-10-28 00:44:15 +01:00
parent 31a189ca42
commit 9e2822774a
No known key found for this signature in database
GPG key ID: 833485276B7902CE
39 changed files with 1355 additions and 181 deletions

View file

@ -63,7 +63,8 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.1.19 | >=2.1.19 |
| 2.2.13 | >=2.2.0 |
| 2.1.19 | >=2.1.19, <2.2.0 |
| 2.1.18 | >=2.1.18 |
| 2.1.17 | >=2.1.16, <=2.1.17 |
| 2.1.16 | >=2.1.16 |

View file

@ -1,7 +1,7 @@
[project]
name = "remnawave"
version = "2.1.19.post.1"
description = "A Python SDK for interacting with the Remnawave API v2.1.19."
version = "2.2.3"
description = "A Python SDK for interacting with the Remnawave API v2.2.3."
authors = [
{name = "Artem",email = "dev@forestsnet.com"}
]

View file

@ -29,7 +29,11 @@ from remnawave.controllers import (
UsersStatsController,
WebhookUtility,
XrayConfigController,
SubscriptionRequestHistoryController
SubscriptionRequestHistoryController,
PasskeysController,
ExternalSquadsController,
SnippetsController,
RemnawaveSettingsController,
# WebhookUtility is not a controller, but it's included in the controllers module for convenience
)
@ -93,6 +97,10 @@ class RemnawaveSDK:
self.users_stats = UsersStatsController(self._client)
self.webhook_utility = WebhookUtility()
self.xray_config = XrayConfigController(self._client)
self.passkeys = PasskeysController(self._client)
self.external_squads = ExternalSquadsController(self._client)
self.snippets = SnippetsController(self._client)
self.remnawave_settings = RemnawaveSettingsController(self._client)
def _validate_params(self) -> None:
if self._client is None:

View file

@ -23,6 +23,11 @@ from .users_stats import UsersStatsController
from .webhooks import WebhookUtility
from .xray_config import XrayConfigController
from .subscriptions_request import SubscriptionRequestHistoryController
from .passkeys import PasskeysController
from .external_squads import ExternalSquadsController
from .snippets import SnippetsController
from .remnawave_settings import RemnawaveSettingsController
__all__ = [
"APITokensManagementController",
@ -51,4 +56,8 @@ __all__ = [
"WebhookUtility",
"XrayConfigController",
"SubscriptionRequestHistoryController",
"PasskeysController",
"ExternalSquadsController",
"SnippetsController",
"RemnawaveSettingsController",
]

View file

@ -6,6 +6,13 @@ from remnawave.models import (
GetStatusResponseDto,
LoginRequestDto,
LoginResponseDto,
OAuth2AuthorizeRequestDto,
OAuth2AuthorizeResponseDto,
OAuth2CallbackRequestDto,
OAuth2CallbackResponseDto,
VerifyPasskeyAuthenticationRequestDto,
VerifyPasskeyAuthenticationResponseDto,
GetPasskeyAuthenticationOptionsResponseDto,
RegisterRequestDto,
RegisterResponseDto,
TelegramCallbackRequestDto,
@ -44,4 +51,35 @@ class AuthController(BaseController):
body: Annotated[TelegramCallbackRequestDto, PydanticBody()],
) -> TelegramCallbackResponseDto:
"""OAuth2 Telegram callback"""
...
@post("/auth/oauth2/authorize", response_class=OAuth2AuthorizeResponseDto)
async def oauth2_authorize(
self,
body: Annotated[OAuth2AuthorizeRequestDto, PydanticBody()],
) -> OAuth2AuthorizeResponseDto:
"""Initiate OAuth2 authorization"""
...
@post("/auth/oauth2/callback", response_class=OAuth2CallbackResponseDto)
async def oauth2_callback(
self,
body: Annotated[OAuth2CallbackRequestDto, PydanticBody()],
) -> OAuth2CallbackResponseDto:
"""Callback from OAuth2"""
...
@get("/auth/passkey/authentication/options", response_class=GetPasskeyAuthenticationOptionsResponseDto)
async def passkey_authentication_options(
self,
) -> GetPasskeyAuthenticationOptionsResponseDto:
"""Get the authentication options for passkey"""
...
@post("/auth/passkey/authentication/verify", response_class=VerifyPasskeyAuthenticationResponseDto)
async def passkey_authentication_verify(
self,
body: Annotated[VerifyPasskeyAuthenticationRequestDto, PydanticBody()],
) -> VerifyPasskeyAuthenticationResponseDto:
"""Verify the authentication for passkey"""
...

View file

@ -67,3 +67,12 @@ class ConfigProfilesController(BaseController):
) -> DeleteConfigProfileResponseDto:
"""Delete config profile"""
...
# Get computed config profile by uuid
@get("/config-profiles/{uuid}/computed-config", response_class=GetConfigProfileByUuidResponseDto)
async def get_computed_config_profile_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the config profile")],
) -> GetConfigProfileByUuidResponseDto:
"""Get computed config profile by uuid"""
...

View file

@ -0,0 +1,73 @@
from typing import Annotated
from rapid_api_client.annotations import PydanticBody
from remnawave.models import (
AddUsersToExternalSquadResponseDto,
CreateExternalSquadRequestDto,
CreateExternalSquadResponseDto,
DeleteExternalSquadResponseDto,
GetExternalSquadByUuidResponseDto,
GetExternalSquadsResponseDto,
RemoveUsersFromExternalSquadResponseDto,
UpdateExternalSquadRequestDto,
UpdateExternalSquadResponseDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
class ExternalSquadsController(BaseController):
@get("/external-squads", response_class=GetExternalSquadsResponseDto)
async def get_external_squads(
self,
) -> GetExternalSquadsResponseDto:
"""Get all external squads"""
...
@post("/external-squads", response_class=CreateExternalSquadResponseDto)
async def create_external_squad(
self,
body: Annotated[CreateExternalSquadRequestDto, PydanticBody()],
) -> CreateExternalSquadResponseDto:
"""Create external squad"""
...
@patch("/external-squads", response_class=UpdateExternalSquadResponseDto)
async def update_external_squad(
self,
body: Annotated[UpdateExternalSquadRequestDto, PydanticBody()],
) -> UpdateExternalSquadResponseDto:
"""Update external squad"""
...
@get("/external-squads/{uuid}", response_class=GetExternalSquadByUuidResponseDto)
async def get_external_squad_by_uuid(
self,
uuid: str,
) -> GetExternalSquadByUuidResponseDto:
"""Get external squad by uuid"""
...
@delete("/external-squads/{uuid}", response_class=DeleteExternalSquadResponseDto)
async def delete_external_squad(
self,
uuid: str,
) -> DeleteExternalSquadResponseDto:
"""Delete external squad"""
...
@post("/external-squads/{uuid}/bulk-actions/add-users", response_class=AddUsersToExternalSquadResponseDto)
async def add_users_to_external_squad(
self,
uuid: str,
) -> AddUsersToExternalSquadResponseDto:
"""Add all users to external squad"""
...
@delete("/external-squads/{uuid}/bulk-actions/remove-users", response_class=RemoveUsersFromExternalSquadResponseDto)
async def remove_users_from_external_squad(
self,
uuid: str,
) -> RemoveUsersFromExternalSquadResponseDto:
"""Delete users from external squad"""
...

View file

@ -17,7 +17,7 @@ from remnawave.models import (
RestartNodeResponseDto,
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestDto,
RestartAllNodesRequestBodyDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
@ -89,7 +89,7 @@ class NodesController(BaseController):
@post("/nodes/actions/restart-all", response_class=RestartAllNodesResponseDto)
async def restart_all_nodes(
self,
body: Annotated[RestartAllNodesRequestDto, PydanticBody()],
body: Annotated[RestartAllNodesRequestBodyDto, PydanticBody()],
) -> RestartAllNodesResponseDto:
"""Restart All Nodes"""
...
@ -100,4 +100,4 @@ class NodesController(BaseController):
body: Annotated[ReorderNodeRequestDto, PydanticBody()],
) -> ReorderNodeResponseDto:
"""Reorder Nodes"""
...
...

View file

@ -0,0 +1,45 @@
from typing import Annotated
from rapid_api_client.annotations import PydanticBody
from remnawave.models import (
DeletePasskeyRequestDto,
DeletePasskeyResponseDto,
GetAllPasskeysResponseDto,
GetPasskeyRegistrationOptionsResponseDto,
VerifyPasskeyRegistrationRequestDto,
VerifyPasskeyRegistrationResponseDto,
)
from remnawave.rapid import BaseController, delete, get, post
class PasskeysController(BaseController):
@get("/passkeys/registration/options", response_class=GetPasskeyRegistrationOptionsResponseDto)
async def passkey_registration_options(
self,
) -> GetPasskeyRegistrationOptionsResponseDto:
"""Get registration options for passkey"""
...
@post("/passkeys/registration/verify", response_class=VerifyPasskeyRegistrationResponseDto)
async def passkey_registration_verify(
self,
body: Annotated[VerifyPasskeyRegistrationRequestDto, PydanticBody()],
) -> VerifyPasskeyRegistrationResponseDto:
"""Verify registration for passkey"""
...
@get("/passkeys", response_class=GetAllPasskeysResponseDto)
async def get_active_passkeys(
self,
) -> GetAllPasskeysResponseDto:
"""Get all passkeys"""
...
@delete("/passkeys", response_class=DeletePasskeyResponseDto)
async def delete_passkey(
self,
body: Annotated[DeletePasskeyRequestDto, PydanticBody()],
) -> DeletePasskeyResponseDto:
"""Delete a passkey by ID"""
...

View file

@ -0,0 +1,25 @@
from typing import Annotated
from rapid_api_client.annotations import PydanticBody
from remnawave.models import (
GetRemnawaveSettingsResponseDto,
UpdateRemnawaveSettingsRequestDto,
UpdateRemnawaveSettingsResponseDto,
)
from remnawave.rapid import BaseController, get, patch
class RemnawaveSettingsController(BaseController):
@get("/remnawave-settings", response_class=GetRemnawaveSettingsResponseDto)
async def get_settings(self) -> GetRemnawaveSettingsResponseDto:
"""Get Remnawave settings"""
...
@patch("/remnawave-settings", response_class=UpdateRemnawaveSettingsResponseDto)
async def update_settings(
self,
body: Annotated[UpdateRemnawaveSettingsRequestDto, PydanticBody()],
) -> UpdateRemnawaveSettingsResponseDto:
"""Update Remnawave settings"""
...

View file

@ -0,0 +1,45 @@
from typing import Annotated
from rapid_api_client.annotations import PydanticBody
from remnawave.models import (
CreateSnippetRequestDto,
CreateSnippetResponseDto,
DeleteSnippetRequestDto,
DeleteSnippetResponseDto,
GetSnippetsResponseDto,
UpdateSnippetRequestDto,
UpdateSnippetResponseDto,
)
from remnawave.rapid import BaseController, delete, get, post, patch
class SnippetsController(BaseController):
@get("/snippets", response_class=GetSnippetsResponseDto)
async def get_snippets(self) -> GetSnippetsResponseDto:
"""Get snippets"""
...
@post("/snippets", response_class=CreateSnippetResponseDto)
async def create_snippet(
self,
body: Annotated[CreateSnippetRequestDto, PydanticBody()],
) -> CreateSnippetResponseDto:
"""Create snippet"""
...
@patch("/snippets", response_class=UpdateSnippetResponseDto)
async def update_snippet(
self,
body: Annotated[UpdateSnippetRequestDto, PydanticBody()],
) -> UpdateSnippetResponseDto:
"""Update snippet"""
...
@delete("/snippets", response_class=DeleteSnippetResponseDto)
async def delete_snippet_by_name(
self,
body: Annotated[DeleteSnippetRequestDto, PydanticBody()],
) -> DeleteSnippetResponseDto:
"""Delete snippet"""
...

View file

@ -3,29 +3,52 @@ from typing import Annotated
from rapid_api_client.annotations import Path, PydanticBody
from remnawave.enums import TemplateType
from remnawave.models import GetTemplateResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto
from remnawave.rapid import BaseController, get, put
from remnawave.models import (
CreateSubscriptionTemplateRequestDto,
CreateSubscriptionTemplateResponseDto,
DeleteSubscriptionTemplateResponseDto,
GetTemplateResponseDto,
GetTemplatesResponseDto,
UpdateTemplateRequestDto,
UpdateTemplateResponseDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
class SubscriptionsTemplateController(BaseController):
@get(
"/subscription-templates/{template_type}",
response_class=GetTemplateResponseDto,
)
async def get_template(
self,
template_type: Annotated[TemplateType, Path(description="Template type")],
) -> GetTemplateResponseDto:
"""Get Template"""
@get("/subscription-templates", response_class=GetTemplatesResponseDto)
async def get_all_templates(self) -> GetTemplatesResponseDto:
"""Get all subscription templates (without content)"""
...
@put(
"/subscription-templates",
response_class=UpdateTemplateResponseDto,
)
@post("/subscription-templates", response_class=CreateSubscriptionTemplateResponseDto)
async def create_template(
self,
body: Annotated[CreateSubscriptionTemplateRequestDto, PydanticBody()],
) -> CreateSubscriptionTemplateResponseDto:
"""Create subscription template"""
...
@patch("/subscription-templates", response_class=UpdateTemplateResponseDto)
async def update_template(
self,
body: Annotated[UpdateTemplateRequestDto, PydanticBody()],
) -> UpdateTemplateResponseDto:
"""Update Template"""
"""Update subscription template"""
...
@get("/subscription-templates/{uuid}", response_class=GetTemplateResponseDto)
async def get_template_by_uuid(
self,
uuid: Annotated[str, Path(description="Template UUID")],
) -> GetTemplateResponseDto:
"""Get subscription template by uuid"""
...
@delete("/subscription-templates/{uuid}", response_class=DeleteSubscriptionTemplateResponseDto)
async def delete_template(
self,
uuid: Annotated[str, Path(description="Template UUID")],
) -> DeleteSubscriptionTemplateResponseDto:
"""Delete subscription template"""
...

View file

@ -1,12 +1,18 @@
from typing import Annotated
from rapid_api_client import PydanticBody
from remnawave.models import (
GetBandwidthStatsResponseDto,
GetNodesStatisticsResponseDto,
GetStatsResponseDto,
GetNodesMetricsResponseDto,
GetRemnawaveHealthResponseDto,
GetX25519KeyPairResponseDto
GetX25519KeyPairResponseDto,
EncryptHappCryptoLinkRequestDto,
EncryptHappCryptoLinkResponseDto,
DebugSrrMatcherRequestDto,
DebugSrrMatcherResponseDto,
)
from remnawave.rapid import BaseController, get
from remnawave.rapid import BaseController, get, post
class SystemController(BaseController):
@ -50,4 +56,20 @@ class SystemController(BaseController):
self,
) -> GetX25519KeyPairResponseDto:
"""Get X25519 Key Pair"""
...
@post("/system/tools/happ/encrypt", response_class=EncryptHappCryptoLinkResponseDto)
async def encrypt_happ_crypto_link(
self,
body: Annotated[EncryptHappCryptoLinkRequestDto, PydanticBody()],
) -> EncryptHappCryptoLinkResponseDto:
"""Encrypt Happ Crypto Link"""
...
@post("/system/testers/srr-matcher", response_class=DebugSrrMatcherResponseDto)
async def debug_srr_matcher(
self,
body: Annotated[DebugSrrMatcherRequestDto, PydanticBody()],
) -> DebugSrrMatcherResponseDto:
"""Test SRR Matcher"""
...

View file

@ -87,3 +87,15 @@ class UsersBulkActionsController(BaseController):
) -> BulkAllResetTrafficUsersResponseDto:
"""Bulk Reset All Users Traffic"""
...
@post(
"/users/bulk/update-squads",
response_class=BulkResponseDto,
)
async def bulk_update_users_internal_squad(
self,
uuids: Annotated[List[UUID], AttributeBody()],
active_internal_squads: Annotated[List[UUID], AttributeBody(serialization_alias="activeInternalSquads")],
) -> BulkResponseDto:
"""Bulk Update Users External Squad"""
...

View file

@ -8,7 +8,16 @@ from .users import TrafficLimitStrategy, UserStatus
from .webhook import (
TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus
)
from .auth import OAuth2Provider
from .subscriptions_settings import (
ResponseRuleConditionOperator,
ResponseRuleOperator,
ResponseRuleVersion,
ResponseType,
)
__all__ = [
"OAuth2Provider",
"TrafficLimitStrategy",
"UserStatus",
"ErrorCode",
@ -17,6 +26,10 @@ __all__ = [
"Fingerprint",
"SecurityLayer",
"TemplateType",
"ResponseRuleConditionOperator",
"ResponseRuleOperator",
"ResponseRuleVersion",
"ResponseType",
# Webhook enums
"TNodeEvents",
"TUserEvents",

7
remnawave/enums/auth.py Normal file
View file

@ -0,0 +1,7 @@
from enum import StrEnum
class OAuth2Provider(StrEnum):
"""OAuth2 Provider enum"""
GITHUB = "github"
POCKETID = "pocketid"
YANDEX = "yandex"

View file

@ -0,0 +1,40 @@
from enum import StrEnum
class ResponseRuleOperator(StrEnum):
"""Response rule logical operators"""
AND = "AND"
OR = "OR"
class ResponseRuleConditionOperator(StrEnum):
"""Response rule condition operators"""
EQUALS = "EQUALS"
NOT_EQUALS = "NOT_EQUALS"
CONTAINS = "CONTAINS"
NOT_CONTAINS = "NOT_CONTAINS"
STARTS_WITH = "STARTS_WITH"
NOT_STARTS_WITH = "NOT_STARTS_WITH"
ENDS_WITH = "ENDS_WITH"
NOT_ENDS_WITH = "NOT_ENDS_WITH"
REGEX = "REGEX"
NOT_REGEX = "NOT_REGEX"
class ResponseType(StrEnum):
"""Response types for subscription rules"""
XRAY_JSON = "XRAY_JSON"
XRAY_BASE64 = "XRAY_BASE64"
MIHOMO = "MIHOMO"
STASH = "STASH"
CLASH = "CLASH"
SINGBOX = "SINGBOX"
BROWSER = "BROWSER"
BLOCK = "BLOCK"
STATUS_CODE_404 = "STATUS_CODE_404"
STATUS_CODE_451 = "STATUS_CODE_451"
SOCKET_DROP = "SOCKET_DROP"
class ResponseRuleVersion(StrEnum):
"""Response rules config version"""
V1 = "1"

View file

@ -14,6 +14,13 @@ from .auth import (
TelegramCallbackRequestDto,
TelegramCallbackResponseDto,
LoginTelegramRequestDto, # Legacy alias
OAuth2AuthorizeRequestDto,
OAuth2AuthorizeResponseDto,
OAuth2CallbackRequestDto,
OAuth2CallbackResponseDto,
VerifyPasskeyAuthenticationRequestDto,
VerifyPasskeyAuthenticationResponseDto,
GetPasskeyAuthenticationOptionsResponseDto,
)
from .bandwidthstats import (
GetNodeUserUsageByRangeResponseDto,
@ -36,6 +43,7 @@ from .config_profiles import (
GetConfigProfileByUuidResponseDto,
GetInboundsByProfileUuidResponseDto,
InboundDto,
NodesProfileDto,
UpdateConfigProfileRequestDto,
UpdateConfigProfileResponseDto,
)
@ -162,7 +170,8 @@ from .nodes import (
RestartNodeResponseDto,
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestDto,
RestartAllNodesRequestDto, # Legacy alias,
RestartAllNodesRequestBodyDto,
)
from .nodes_usage_history import (
GetNodeUserUsageByRangeResponseDto,
@ -184,11 +193,21 @@ from .subscription import (
)
from .subscriptions_settings import (
GetSubscriptionSettingsResponseDto,
ResponseModificationHeader,
ResponseModifications,
ResponseRule,
ResponseRuleCondition,
ResponseRules,
SubscriptionSettingsResponseDto,
UpdateSubscriptionSettingsRequestDto,
UpdateSubscriptionSettingsResponseDto,
)
from .subscriptions_template import (
CreateSubscriptionTemplateRequestDto,
CreateSubscriptionTemplateResponseDto,
DeleteSubscriptionTemplateResponseDto,
GetTemplatesResponseDto,
TemplateInfoDto,
GetTemplateResponseDto,
TemplateResponseDto,
UpdateTemplateRequestDto,
@ -212,6 +231,10 @@ from .system import (
GetNodesMetricsResponseDto,
GetX25519KeyPairResponseDto,
X25519KeyPair,
DebugSrrMatcherRequestDto,
DebugSrrMatcherResponseDto,
EncryptHappCryptoLinkRequestDto,
EncryptHappCryptoLinkResponseDto,
)
from .users import (
ActiveInternalSquadDto,
@ -271,6 +294,56 @@ from .webhook import (
CrmEventDto,
WebhookPayloadDto,
)
from .passkeys import (
DeletePasskeyRequestDto,
DeletePasskeyResponseDto,
GetAllPasskeysResponseDto,
GetPasskeyRegistrationOptionsResponseDto,
PasskeyDto,
VerifyPasskeyRegistrationRequestDto,
VerifyPasskeyRegistrationResponseDto,
)
from .external_squads import (
AddUsersToExternalSquadResponseDto,
CreateExternalSquadRequestDto,
CreateExternalSquadResponseDto,
DeleteExternalSquadResponseDto,
ExternalSquadDto,
ExternalSquadInfoDto,
ExternalSquadSubscriptionSettingsDto,
ExternalSquadTemplateDto,
GetExternalSquadByUuidResponseDto,
GetExternalSquadsResponseDto,
RemoveUsersFromExternalSquadResponseDto,
TemplateType,
UpdateExternalSquadRequestDto,
UpdateExternalSquadResponseDto,
)
from .snippets import (
CreateSnippetRequestDto,
CreateSnippetResponseDto,
DeleteSnippetRequestDto,
DeleteSnippetResponseDto,
GetSnippetsResponseDto,
SnippetItem,
SnippetsData,
UpdateSnippetRequestDto,
UpdateSnippetResponseDto,
)
from .remnawave_settings import (
BrandingSettings,
GetRemnawaveSettingsResponseDto,
GitHubOAuth2Settings,
OAuth2Settings,
PasskeySettings,
PasswordSettings,
PocketIdOAuth2Settings,
RemnawaveSettingsData,
TelegramAuthSettings,
UpdateRemnawaveSettingsRequestDto,
UpdateRemnawaveSettingsResponseDto,
YandexOAuth2Settings,
)
__all__ = [
# Auth models
@ -283,6 +356,13 @@ __all__ = [
"TelegramCallbackRequestDto",
"TelegramCallbackResponseDto",
"LoginTelegramRequestDto", # Legacy alias
"OAuth2AuthorizeRequestDto",
"OAuth2AuthorizeResponseDto",
"OAuth2CallbackRequestDto",
"OAuth2CallbackResponseDto",
"VerifyPasskeyAuthenticationRequestDto",
"VerifyPasskeyAuthenticationResponseDto",
"GetPasskeyAuthenticationOptionsResponseDto",
# Nodes models
"CreateNodeRequestDto",
"CreateNodeResponseDto",
@ -302,7 +382,8 @@ __all__ = [
"UpdateNodeResponseDto",
"NodeConfigProfileDto",
"NodeConfigProfileRequestDto",
"RestartAllNodesRequestDto",
"RestartAllNodesRequestDto", # Legacy alias
"RestartAllNodesRequestBodyDto",
# Hosts models
"CreateHostRequestDto",
"CreateHostResponseDto",
@ -347,11 +428,21 @@ __all__ = [
"SubscriptionSettingsResponseDto",
"UpdateSubscriptionSettingsRequestDto",
"UpdateSubscriptionSettingsResponseDto",
"ResponseModificationHeader",
"ResponseModifications",
"ResponseRule",
"ResponseRuleCondition",
"ResponseRules",
# Subscription template models
"GetTemplateResponseDto",
"TemplateResponseDto",
"UpdateTemplateRequestDto",
"UpdateTemplateResponseDto",
"CreateSubscriptionTemplateRequestDto",
"CreateSubscriptionTemplateResponseDto",
"DeleteSubscriptionTemplateResponseDto",
"GetTemplatesResponseDto",
"TemplateInfoDto",
# System models
"BandwidthStatistic",
"BandwidthStatisticResponseDto",
@ -370,6 +461,10 @@ __all__ = [
"GetNodesMetricsResponseDto",
"GetX25519KeyPairResponseDto",
"X25519KeyPair",
"DebugSrrMatcherRequestDto",
"DebugSrrMatcherResponseDto",
"EncryptHappCryptoLinkRequestDto",
"EncryptHappCryptoLinkResponseDto",
# XRay config models
"ConfigResponseDto", # Legacy alias
"GetConfigResponseDto",
@ -463,6 +558,7 @@ __all__ = [
"GetConfigProfileByUuidResponseDto",
"GetInboundsByProfileUuidResponseDto",
"InboundDto",
"NodesProfileDto",
"UpdateConfigProfileRequestDto",
"UpdateConfigProfileResponseDto",
"GetAllConfigProfilesResponsePaginated",
@ -553,4 +649,56 @@ __all__ = [
# WEBHOOK PAYLOAD
"WebhookPayloadDto",
# Passkeys models
"DeletePasskeyRequestDto",
"DeletePasskeyResponseDto",
"GetAllPasskeysResponseDto",
"GetPasskeyRegistrationOptionsResponseDto",
"PasskeyDto",
"VerifyPasskeyRegistrationRequestDto",
"VerifyPasskeyRegistrationResponseDto",
# External squads models
"AddUsersToExternalSquadResponseDto",
"CreateExternalSquadRequestDto",
"CreateExternalSquadResponseDto",
"DeleteExternalSquadResponseDto",
"ExternalSquadDto",
"ExternalSquadInfoDto",
"ExternalSquadSubscriptionSettingsDto",
"ExternalSquadTemplateDto",
"GetExternalSquadByUuidResponseDto",
"GetExternalSquadsResponseDto",
"RemoveUsersFromExternalSquadResponseDto",
"TemplateType",
"UpdateExternalSquadRequestDto",
"UpdateExternalSquadResponseDto",
# Snippets models
"CreateSnippetRequestDto",
"CreateSnippetResponseDto",
"DeleteSnippetRequestDto",
"DeleteSnippetResponseDto",
"GetSnippetsResponseDto",
"SnippetItem",
"SnippetsData",
"UpdateSnippetRequestDto",
"UpdateSnippetResponseDto",
# Remnawave settings models
"BrandingSettings",
"GetRemnawaveSettingsResponseDto",
"GitHubOAuth2Settings",
"OAuth2Settings",
"PasskeySettings",
"PasswordSettings",
"PocketIdOAuth2Settings",
"RemnawaveSettingsData",
"TelegramAuthSettings",
"UpdateRemnawaveSettingsRequestDto",
"UpdateRemnawaveSettingsResponseDto",
"YandexOAuth2Settings",
]

View file

@ -1,7 +1,9 @@
from typing import Annotated, Optional
from typing import Annotated, Any, Dict, Optional
from pydantic import BaseModel, Field, StringConstraints
from remnawave.enums.auth import OAuth2Provider
class AuthTokenResponseData(BaseModel):
access_token: str = Field(alias="accessToken")
@ -53,6 +55,47 @@ class TelegramCallbackResponseDto(AuthTokenResponseData):
pass
# OAuth2 Authorization models
class OAuth2AuthorizeRequestDto(BaseModel):
"""Request to initiate OAuth2 authorization"""
provider: OAuth2Provider
class OAuth2AuthorizeResponseDto(BaseModel):
"""Response with OAuth2 authorization URL"""
authorization_url: Optional[str] = Field(alias="authorizationUrl")
# OAuth2 Callback models
class OAuth2CallbackRequestDto(BaseModel):
"""Request for OAuth2 callback"""
provider: OAuth2Provider
code: str
state: str
class OAuth2CallbackResponseDto(BaseModel):
"""Response with access token from OAuth2 callback"""
access_token: str = Field(alias="accessToken")
# 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]
class VerifyPasskeyAuthenticationRequestDto(BaseModel):
"""Request to verify passkey authentication"""
# Passkey authentication response is complex WebAuthn object
response: Dict[str, Any]
class VerifyPasskeyAuthenticationResponseDto(BaseModel):
"""Response with access token after successful passkey authentication"""
access_token: str = Field(alias="accessToken")
# Legacy alias for backward compatibility
StatusResponseDto = GetStatusResponseDto
LoginTelegramRequestDto = TelegramCallbackRequestDto
LoginTelegramRequestDto = TelegramCallbackRequestDto

View file

@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, RootModel
class NodeUsageResponseDto(BaseModel):
node_uuid: UUID = Field(alias="nodeUuid")
node_name: str = Field(alias="nodeName")
total: float
total: int
total_download: float = Field(alias="totalDownload")
total_upload: float = Field(alias="totalUpload")
human_readable_total: str = Field(alias="humanReadableTotal")
@ -65,7 +65,7 @@ class UserUsageByRangeItem(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
node_uuid: UUID = Field(alias="nodeUuid")
node_name: str = Field(alias="nodeName")
total: float
total: int
date: str
@ -81,7 +81,7 @@ class NodeUserUsageItem(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
username: str
node_uuid: UUID = Field(alias="nodeUuid")
total: float
total: int
date: str

View file

@ -15,13 +15,17 @@ class InboundDto(BaseModel):
port: Optional[int] = None
raw_inbound: Optional[Any] = Field(None, alias="rawInbound")
class NodesProfileDto(BaseModel):
uuid: UUID
name: str
country_code: str = Field(alias="countryCode")
class ConfigProfileDto(BaseModel):
uuid: UUID
name: str
config: Dict[str, Any]
inbounds: List[InboundDto]
nodes: List[Any] = [] # Can be empty list
nodes: List[NodesProfileDto] = []
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")

View file

@ -0,0 +1,102 @@
from datetime import datetime
from enum import StrEnum
from typing import List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
class TemplateType(StrEnum):
"""Template type enum"""
XRAY_JSON = "XRAY_JSON"
XRAY_BASE64 = "XRAY_BASE64"
MIHOMO = "MIHOMO"
STASH = "STASH"
CLASH = "CLASH"
SINGBOX = "SINGBOX"
class ExternalSquadInfoDto(BaseModel):
"""External squad info"""
members_count: float = Field(alias="membersCount")
class ExternalSquadTemplateDto(BaseModel):
"""External squad template"""
template_uuid: UUID = Field(alias="templateUuid")
template_type: TemplateType = Field(alias="templateType")
class ExternalSquadSubscriptionSettingsDto(BaseModel):
"""External squad subscription settings"""
profile_title: str = Field(alias="profileTitle")
support_link: str = Field(alias="supportLink")
profile_update_interval: int = Field(alias="profileUpdateInterval", ge=1)
is_profile_webpage_url_enabled: bool = Field(alias="isProfileWebpageUrlEnabled")
serve_json_at_base_subscription: bool = Field(alias="serveJsonAtBaseSubscription")
add_username_to_base_subscription: bool = Field(alias="addUsernameToBaseSubscription")
is_show_custom_remarks: bool = Field(alias="isShowCustomRemarks")
happ_announce: Optional[str] = Field(None, alias="happAnnounce")
happ_routing: Optional[str] = Field(None, alias="happRouting")
randomize_hosts: bool = Field(alias="randomizeHosts")
class ExternalSquadDto(BaseModel):
"""External squad data model"""
uuid: UUID
name: str
info: ExternalSquadInfoDto
templates: List[ExternalSquadTemplateDto]
subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(alias="subscriptionSettings")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
# Request/Response models
class GetExternalSquadsResponseDto(BaseModel):
"""Response with all external squads"""
total: float
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
class GetExternalSquadByUuidResponseDto(ExternalSquadDto):
"""Response with external squad by UUID"""
pass
class CreateExternalSquadRequestDto(BaseModel):
"""Request to create external squad"""
name: str = Field(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")
class CreateExternalSquadResponseDto(ExternalSquadDto):
"""Response after creating external squad"""
pass
class UpdateExternalSquadRequestDto(BaseModel):
"""Request to update external squad"""
uuid: UUID
name: Optional[str] = Field(None, min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")
templates: Optional[List[ExternalSquadTemplateDto]] = None
subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(None, serialization_alias="subscriptionSettings")
class UpdateExternalSquadResponseDto(ExternalSquadDto):
"""Response after updating external squad"""
pass
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")
class RemoveUsersFromExternalSquadResponseDto(BaseModel):
"""Response after removing users from external squad"""
event_sent: bool = Field(alias="eventSent")

View file

@ -23,9 +23,19 @@ class ReorderHostRequestDto(BaseModel):
hosts: List[ReorderHostItem]
class HostInboundData(BaseModel):
config_profile_uuid: Optional[UUID] = Field(alias="configProfileUuid")
config_profile_inbound_uuid: Optional[UUID] = Field(alias="configProfileInboundUuid")
class CreateHostInboundData(BaseModel):
config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid")
config_profile_inbound_uuid: UUID = Field(serialization_alias="configProfileInboundUuid")
class UpdateHostRequestDto(BaseModel):
uuid: UUID
inbound_uuid: Optional[UUID] = Field(None, serialization_alias="inboundUuid")
inbound: Optional[CreateHostInboundData] = None
remark: Annotated[Optional[str], StringConstraints(max_length=40)] = None
address: Optional[str] = None
port: Optional[int] = None
@ -42,17 +52,7 @@ class UpdateHostRequestDto(BaseModel):
server_description: Optional[str] = Field(
None, serialization_alias="serverDescription", max_length=30
)
mux_params: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field(
None, serialization_alias="tag"
)
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
is_hidden: Optional[bool] = Field(
None,
serialization_alias="isHidden",
@ -79,12 +79,20 @@ class UpdateHostRequestDto(BaseModel):
None,
serialization_alias="xHttpExtraParams",
)
nodes: Optional[List[str]] = None
mux_params: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
nodes: Optional[List[UUID]] = None
class HostInboundData(BaseModel):
config_profile_uuid: Optional[UUID] = Field(alias="configProfileUuid")
config_profile_inbound_uuid: Optional[UUID] = Field(alias="configProfileInboundUuid")
# Legacy compatibility properties
@property
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid if self.inbound else None
class HostResponseDto(BaseModel):
@ -121,7 +129,7 @@ class HostResponseDto(BaseModel):
)
shuffle_host: bool = Field(alias="shuffleHost")
mihomo_x25519: bool = Field(alias="mihomoX25519")
nodes: List[str]
nodes: List[UUID]
is_disabled: bool = Field(
default=False,
alias="isDisabled",
@ -149,54 +157,9 @@ class HostResponseDto(BaseModel):
return self.inbound.config_profile_inbound_uuid
class HostsResponseDto(List[HostResponseDto]):
pass
class CreateHostResponseDto(HostResponseDto):
pass
class UpdateHostResponseDto(HostResponseDto):
pass
class GetAllHostTagsResponseDto(BaseModel):
tags: list[str] = None
class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]):
root: List[HostResponseDto]
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class GetOneHostResponseDto(HostResponseDto):
pass
class ReorderHostResponseDto(BaseModel):
is_updated: bool = Field(alias="isUpdated", default=True)
class DeleteHostResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
class CreateHostInboundData(BaseModel):
config_profile_uuid: Optional[UUID] = Field(serialization_alias="configProfileUuid")
config_profile_inbound_uuid: Optional[UUID] = Field(
serialization_alias="configProfileInboundUuid"
)
class CreateHostRequestDto(BaseModel):
inbound: CreateHostInboundData
remark: Annotated[str, StringConstraints(max_length=40)]
remark: Annotated[str, StringConstraints(min_length=1, max_length=40)]
address: str
port: int
path: Optional[str] = None
@ -219,9 +182,7 @@ class CreateHostRequestDto(BaseModel):
server_description: Optional[str] = Field(
None, serialization_alias="serverDescription", max_length=30
)
tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field(
None, serialization_alias="tag"
)
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
vless_route_id: Optional[int] = Field(
None,
serialization_alias="vlessRouteId",
@ -236,7 +197,7 @@ class CreateHostRequestDto(BaseModel):
False,
serialization_alias="mihomoX25519",
)
nodes: List[str] = Field(default_factory=list)
nodes: List[UUID] = Field(default_factory=list)
allow_insecure: bool = Field(
False,
serialization_alias="allowInsecure",
@ -278,4 +239,42 @@ class CreateHostRequestDto(BaseModel):
or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"),
config_profile_inbound_uuid=inbound_uuid,
)
super().__init__(**data)
super().__init__(**data)
class GetAllHostTagsResponseDto(BaseModel):
tags: List[str]
class CreateHostResponseDto(HostResponseDto):
pass
class UpdateHostResponseDto(HostResponseDto):
pass
class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]):
root: List[HostResponseDto]
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class GetOneHostResponseDto(HostResponseDto):
pass
class ReorderHostResponseDto(BaseModel):
is_updated: bool = Field(alias="isUpdated", default=True)
class DeleteHostResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
# Legacy compatibility
HostsResponseDto = List[HostResponseDto]

View file

@ -17,7 +17,7 @@ class InboundResponseDto(BaseModel):
class AllInboundsData(BaseModel):
total: float
total: int
inbounds: List[InboundResponseDto]
@ -26,7 +26,7 @@ class GetAllInboundsResponseDto(AllInboundsData):
class InboundsByProfileData(BaseModel):
total: float
total: int
inbounds: List[InboundResponseDto]

View file

@ -102,7 +102,7 @@ class UpdateInfraProviderResponseDto(InfraProviderDto):
class AllInfraProvidersData(BaseModel):
total: float = Field(alias="total")
total: int = Field(alias="total")
providers: List[InfraProviderDto]

View file

@ -28,8 +28,18 @@ class ReorderNodeItem(BaseModel):
uuid: UUID
class NodeProviderDto(BaseModel):
"""Node provider information"""
uuid: UUID
name: str
favicon_link: Optional[str] = Field(None, alias="faviconLink")
login_url: Optional[str] = Field(None, alias="loginUrl")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
class NodeConfigProfileDto(BaseModel):
active_config_profile_uuid: UUID = Field(alias="activeConfigProfileUuid")
active_config_profile_uuid: Optional[UUID] = Field(alias="activeConfigProfileUuid")
active_inbounds: List[InboundsDto] = Field(alias="activeInbounds")
@ -39,30 +49,31 @@ class NodeConfigProfileRequestDto(BaseModel):
class CreateNodeRequestDto(BaseModel):
name: Annotated[str, StringConstraints(min_length=3)]
name: Annotated[str, StringConstraints(min_length=3, max_length=30)]
address: Annotated[str, StringConstraints(min_length=2)]
port: Optional[int] = Field(None, strict=True, ge=1)
port: Optional[int] = Field(None, ge=1, le=65535)
is_traffic_tracking_active: Optional[bool] = Field(
None,
False,
serialization_alias="isTrafficTrackingActive",
)
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
None, serialization_alias="trafficLimitBytes", ge=0
)
notify_percent: Optional[int] = Field(
None, serialization_alias="notifyPercent", strict=True, ge=0
None, serialization_alias="notifyPercent", ge=0, le=100
)
traffic_reset_day: Optional[int] = Field(
None, serialization_alias="trafficResetDay", strict=True, ge=1
None, serialization_alias="trafficResetDay", ge=1, le=31
)
excluded_inbounds: Optional[List[UUID]] = Field(
None, serialization_alias="excludedInbounds"
)
country_code: Annotated[Optional[str], StringConstraints(max_length=2)] = Field(
None, serialization_alias="countryCode"
"XX",
serialization_alias="countryCode"
)
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier"
None, serialization_alias="consumptionMultiplier", ge=0.1
)
config_profile: NodeConfigProfileRequestDto = Field(
serialization_alias="configProfile"
@ -72,18 +83,20 @@ class CreateNodeRequestDto(BaseModel):
class UpdateNodeRequestDto(BaseModel):
uuid: UUID
name: Annotated[Optional[str], StringConstraints(min_length=3)] = None
name: Annotated[Optional[str], StringConstraints(min_length=3, max_length=30)] = None
address: Annotated[Optional[str], StringConstraints(min_length=2)] = None
port: Optional[int] = None
port: Optional[float] = Field(None, ge=1, le=65535) # ИСПРАВЛЕН тип на float
is_traffic_tracking_active: Optional[bool] = Field(
None, serialization_alias="isTrafficTrackingActive"
)
traffic_limit_bytes: Optional[float] = Field(
None, serialization_alias="trafficLimitBytes"
None, serialization_alias="trafficLimitBytes", ge=0
)
notify_percent: Optional[float] = Field(
None, serialization_alias="notifyPercent", ge=0, le=100
)
notify_percent: Optional[float] = Field(None, serialization_alias="notifyPercent")
traffic_reset_day: Optional[float] = Field(
None, serialization_alias="trafficResetDay"
None, serialization_alias="trafficResetDay", ge=1, le=31
)
excluded_inbounds: Optional[List[UUID]] = Field(
None, serialization_alias="excludedInbounds"
@ -92,8 +105,12 @@ class UpdateNodeRequestDto(BaseModel):
None, serialization_alias="countryCode"
)
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier"
None, serialization_alias="consumptionMultiplier", ge=0.1
)
config_profile: Optional[NodeConfigProfileRequestDto] = Field(
None, serialization_alias="configProfile"
)
provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid")
class ReorderNodeRequestDto(BaseModel):
@ -113,6 +130,7 @@ class NodeResponseDto(BaseModel):
last_status_change: Optional[datetime] = Field(None, alias="lastStatusChange")
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")
is_traffic_tracking_active: bool = Field(alias="isTrafficTrackingActive")
traffic_reset_day: Optional[int] = Field(None, alias="trafficResetDay")
@ -129,16 +147,8 @@ class NodeResponseDto(BaseModel):
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
config_profile: NodeConfigProfileDto = Field(alias="configProfile")
class NodesResponseDto(RootModel[List[NodeResponseDto]]):
root: List[NodeResponseDto]
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
provider_uuid: Optional[UUID] = Field(None, alias="providerUuid")
provider: Optional[NodeProviderDto] = None
class CreateNodeResponseDto(NodeResponseDto):
@ -172,14 +182,16 @@ class DisableNodeResponseDto(NodeResponseDto):
class RestartNodeResponseDto(BaseModel):
message: str
event_sent: bool = Field(alias="eventSent")
class RestartAllNodesResponseDto(BaseModel):
message: str
event_sent: bool = Field(alias="eventSent")
class ReorderNodeResponseDto(RootModel[List[NodeResponseDto]]):
root: List[NodeResponseDto]
def __iter__(self):
return iter(self.root)
@ -194,5 +206,10 @@ class DeleteNodeResponseDto(BaseModel):
return self.is_deleted
class RestartAllNodesRequestDto(BaseModel):
class RestartAllNodesRequestBodyDto(BaseModel):
force_restart: bool = Field(default=False, alias="forceRestart")
# Для обратной совместимости
RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto
NodesResponseDto = NodeResponseDto

View file

@ -1,3 +1,4 @@
from datetime import datetime
from typing import List, Dict, Any
from uuid import UUID
@ -18,11 +19,13 @@ class GetUserAccessibleNodesResponseDto(GetUserAccessibleNodesResponse):
class NodeUsageDto(BaseModel):
date: str
upload: int
download: int
"""Individual node usage item"""
node_uuid: UUID = Field(alias="nodeUuid")
date: datetime
upload: int = Field(0, alias="totalBytes")
download: int = Field(0, alias="totalBytes")
class GetNodesUsageByRangeResponseDto(RootModel[List[NodeUsageDto]]):
def __iter__(self):
return iter(self.root)

View file

@ -0,0 +1,46 @@
from datetime import datetime
from typing import Any, Dict, List
from pydantic import BaseModel, Field
class PasskeyDto(BaseModel):
"""Passkey data model"""
id: str
name: str
created_at: datetime = Field(alias="createdAt")
last_used_at: datetime = Field(alias="lastUsedAt")
# Registration models
class GetPasskeyRegistrationOptionsResponseDto(BaseModel):
"""Response with passkey registration options"""
# WebAuthn registration options are complex objects, using Any for flexibility
response: Dict[str, Any]
class VerifyPasskeyRegistrationRequestDto(BaseModel):
"""Request to verify passkey registration"""
# WebAuthn registration response is complex object
response: Dict[str, Any]
class VerifyPasskeyRegistrationResponseDto(BaseModel):
"""Response with passkey registration verification result"""
verified: bool
# Passkeys management models
class GetAllPasskeysResponseDto(BaseModel):
"""Response with all user's passkeys"""
passkeys: List[PasskeyDto]
class DeletePasskeyRequestDto(BaseModel):
"""Request to delete a passkey"""
id: str
class DeletePasskeyResponseDto(BaseModel):
"""Response with updated passkeys list after deletion"""
passkeys: List[PasskeyDto]

View file

@ -0,0 +1,88 @@
from typing import List, Optional
from pydantic import BaseModel, Field, HttpUrl
class PasskeySettings(BaseModel):
"""Passkey authentication settings"""
enabled: bool
rp_id: Optional[str] = Field(None, alias="rpId")
origin: Optional[str] = None
class GitHubOAuth2Settings(BaseModel):
"""GitHub OAuth2 settings"""
enabled: bool
client_id: Optional[str] = Field(None, alias="clientId")
client_secret: Optional[str] = Field(None, alias="clientSecret")
allowed_emails: List[str] = Field(alias="allowedEmails")
class PocketIdOAuth2Settings(BaseModel):
"""PocketID OAuth2 settings"""
enabled: bool
client_id: Optional[str] = Field(None, alias="clientId")
client_secret: Optional[str] = Field(None, alias="clientSecret")
plain_domain: Optional[str] = Field(None, alias="plainDomain")
allowed_emails: List[str] = Field(alias="allowedEmails")
class YandexOAuth2Settings(BaseModel):
"""Yandex OAuth2 settings"""
enabled: bool
client_id: Optional[str] = Field(None, alias="clientId")
client_secret: Optional[str] = Field(None, alias="clientSecret")
allowed_emails: List[str] = Field(alias="allowedEmails")
class OAuth2Settings(BaseModel):
"""OAuth2 authentication settings"""
github: GitHubOAuth2Settings
pocketid: PocketIdOAuth2Settings
yandex: YandexOAuth2Settings
class TelegramAuthSettings(BaseModel):
"""Telegram authentication settings"""
enabled: bool
bot_token: Optional[str] = Field(None, alias="botToken")
admin_ids: List[str] = Field(alias="adminIds")
class PasswordSettings(BaseModel):
"""Password authentication settings"""
enabled: bool
class BrandingSettings(BaseModel):
"""Branding settings"""
title: Optional[str] = None
logo_url: Optional[HttpUrl] = Field(None, alias="logoUrl")
class RemnawaveSettingsData(BaseModel):
"""Remnawave settings data"""
passkey_settings: Optional[PasskeySettings] = Field(None, alias="passkeySettings")
oauth2_settings: Optional[OAuth2Settings] = Field(None, alias="oauth2Settings")
tg_auth_settings: Optional[TelegramAuthSettings] = Field(None, alias="tgAuthSettings")
password_settings: Optional[PasswordSettings] = Field(None, alias="passwordSettings")
branding_settings: Optional[BrandingSettings] = Field(None, alias="brandingSettings")
class GetRemnawaveSettingsResponseDto(BaseModel):
"""Get Remnawave settings response"""
response: RemnawaveSettingsData
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")
class UpdateRemnawaveSettingsResponseDto(BaseModel):
"""Update Remnawave settings response"""
response: RemnawaveSettingsData

View file

@ -0,0 +1,56 @@
from typing import Annotated, Any, List
from pydantic import BaseModel, Field, StringConstraints, RootModel
class SnippetItem(BaseModel):
"""Individual snippet item"""
name: str
snippet: Any # Can be any JSON object or array
class SnippetsData(BaseModel):
"""Snippets response data"""
total: int
snippets: List[SnippetItem]
# Изменяем структуру - API возвращает данные напрямую
class GetSnippetsResponseDto(SnippetsData):
"""Get all snippets response - extends SnippetsData directly"""
pass
class CreateSnippetResponseDto(SnippetsData):
"""Create snippet response - extends SnippetsData directly"""
pass
class UpdateSnippetResponseDto(SnippetsData):
"""Update snippet response - extends SnippetsData directly"""
pass
class DeleteSnippetResponseDto(SnippetsData):
"""Delete snippet response - extends SnippetsData directly"""
pass
class CreateSnippetRequestDto(BaseModel):
"""Create snippet request"""
name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")]
snippet: List[dict] # Array of objects
class UpdateSnippetRequestDto(BaseModel):
"""Update snippet request"""
name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")]
snippet: List[dict] # Array of objects
class DeleteSnippetRequestDto(BaseModel):
"""Delete snippet request"""
name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")]
class DeleteSnippetResponseDto(SnippetsData):
"""Delete snippet response"""

View file

@ -185,7 +185,7 @@ class SubscriptionWithoutHapp(BaseModel):
class GetAllSubscriptionsResponseDto(BaseModel):
subscriptions: List[SubscriptionWithoutHapp]
total: float
total: int
class GetSubscriptionByUsernameResponseDto(BaseModel):

View file

@ -15,7 +15,7 @@ class SubscriptionRequestHistoryRecord(BaseModel):
class SubscriptionRequestHistoryData(BaseModel):
records: List[SubscriptionRequestHistoryRecord]
total: float
total: int
class GetAllSubscriptionRequestHistoryResponseDto(SubscriptionRequestHistoryData):

View file

@ -4,6 +4,56 @@ from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints
from remnawave.enums import (
ResponseRuleConditionOperator,
ResponseRuleOperator,
ResponseRuleVersion,
ResponseType,
)
class ResponseRuleCondition(BaseModel):
"""Condition to check against request headers"""
header_name: Annotated[str, StringConstraints(pattern=r"^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$")] = Field(
alias="headerName"
)
operator: ResponseRuleConditionOperator
value: Annotated[str, StringConstraints(min_length=1, max_length=255)]
case_sensitive: bool = Field(alias="caseSensitive")
class ResponseModificationHeader(BaseModel):
"""Response header modification"""
key: Annotated[str, StringConstraints(pattern=r"^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$")]
value: Annotated[str, StringConstraints(min_length=1)]
class ResponseModifications(BaseModel):
"""Response modifications to apply when rule matches"""
headers: Optional[List[ResponseModificationHeader]] = None
subscription_template: Optional[Annotated[str, StringConstraints(min_length=1)]] = Field(
None, alias="subscriptionTemplate"
)
class ResponseRule(BaseModel):
"""Individual response rule configuration"""
name: Annotated[str, StringConstraints(min_length=1, max_length=50)]
description: Optional[Annotated[str, StringConstraints(min_length=1, max_length=250)]] = None
enabled: bool
operator: ResponseRuleOperator
conditions: List[ResponseRuleCondition]
response_type: ResponseType = Field(alias="responseType")
response_modifications: Optional[ResponseModifications] = Field(
None, alias="responseModifications"
)
class ResponseRules(BaseModel):
"""Response rules configuration"""
version: ResponseRuleVersion
rules: List[ResponseRule]
class SubscriptionSettingsResponseDto(BaseModel):
uuid: UUID
@ -27,6 +77,7 @@ class SubscriptionSettingsResponseDto(BaseModel):
None, alias="customResponseHeaders"
)
randomize_hosts: bool = Field(alias="randomizeHosts")
response_rules: Optional[ResponseRules] = Field(None, alias="responseRules")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
@ -41,37 +92,38 @@ class UpdateSubscriptionSettingsResponseDto(SubscriptionSettingsResponseDto):
class UpdateSubscriptionSettingsRequestDto(BaseModel):
uuid: UUID
profile_title: Optional[str] = Field(None, serialization_alias="profileTitle")
support_link: Optional[str] = Field(None, serialization_alias="supportLink")
profile_title: Optional[str] = Field(None, alias="profileTitle")
support_link: Optional[str] = Field(None, alias="supportLink")
profile_update_interval: Optional[int] = Field(
None, serialization_alias="profileUpdateInterval"
None, alias="profileUpdateInterval"
)
is_profile_webpage_url_enabled: Optional[bool] = Field(
None, serialization_alias="isProfileWebpageUrlEnabled"
None, alias="isProfileWebpageUrlEnabled"
)
serve_json_at_base_subscription: Optional[bool] = Field(
None, serialization_alias="serveJsonAtBaseSubscription"
None, alias="serveJsonAtBaseSubscription"
)
add_username_to_base_subscription: Optional[bool] = Field(
None, serialization_alias="addUsernameToBaseSubscription"
None, alias="addUsernameToBaseSubscription"
)
is_show_custom_remarks: Optional[bool] = Field(
None, serialization_alias="isShowCustomRemarks"
None, alias="isShowCustomRemarks"
)
happ_announce: Annotated[Optional[str], StringConstraints(max_length=200)] = Field(
None, serialization_alias="happAnnounce"
None, alias="happAnnounce"
)
happ_routing: Optional[str] = Field(None, serialization_alias="happRouting")
happ_routing: Optional[str] = Field(None, alias="happRouting")
expired_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="expiredUsersRemarks"
None, alias="expiredUsersRemarks"
)
limited_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="limitedUsersRemarks"
None, alias="limitedUsersRemarks"
)
disabled_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="disabledUsersRemarks"
None, alias="disabledUsersRemarks"
)
custom_response_headers: Optional[Dict[str, str]] = Field(
None, serialization_alias="customResponseHeaders"
None, alias="customResponseHeaders"
)
randomize_hosts: Optional[bool] = Field(None, serialization_alias="randomizeHosts")
randomize_hosts: Optional[bool] = Field(None, alias="randomizeHosts")
response_rules: Optional[ResponseRules] = Field(None, alias="responseRules")

View file

@ -1,13 +1,23 @@
from typing import Any, Optional
from typing import Annotated, Any, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, StringConstraints
from remnawave.enums import TemplateType
class TemplateResponseDto(BaseModel):
uuid: UUID
name: str
template_type: TemplateType = Field(alias="templateType")
template_json: Optional[Any] = Field(None, alias="templateJson")
encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml")
class TemplateInfoDto(BaseModel):
"""Template info without content - used in list responses"""
uuid: UUID
name: str
template_type: TemplateType = Field(alias="templateType")
template_json: Optional[Any] = Field(None, alias="templateJson")
encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml")
@ -16,9 +26,26 @@ class TemplateResponseDto(BaseModel):
class GetTemplateResponseDto(TemplateResponseDto):
pass
class GetTemplatesData(BaseModel):
total: int
templates: List[TemplateInfoDto]
class GetTemplatesResponseDto(GetTemplatesData):
pass
class CreateSubscriptionTemplateRequestDto(BaseModel):
name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")]
template_type: TemplateType = Field(serialization_alias="templateType")
class CreateSubscriptionTemplateResponseDto(TemplateResponseDto):
pass
class UpdateTemplateRequestDto(BaseModel):
template_type: TemplateType = Field(serialization_alias="templateType")
uuid: UUID
name: Optional[Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")]] = None
template_json: Optional[dict] = Field(None, serialization_alias="templateJson")
encoded_template_yaml: Optional[str] = Field(
None, serialization_alias="encodedTemplateYaml"
@ -27,3 +54,23 @@ class UpdateTemplateRequestDto(BaseModel):
class UpdateTemplateResponseDto(TemplateResponseDto):
pass
class DeleteTemplateData(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
class DeleteSubscriptionTemplateResponseDto(DeleteTemplateData):
pass
# Legacy aliases for backward compatibility
class UpdateTemplateRequestDtoLegacy(BaseModel):
template_type: TemplateType = Field(serialization_alias="templateType")
template_json: Optional[dict] = Field(None, serialization_alias="templateJson")
encoded_template_yaml: Optional[str] = Field(
None, serialization_alias="encodedTemplateYaml"
)
class UpdateTemplateResponseDtoLegacy(TemplateResponseDto):
pass

View file

@ -1,8 +1,11 @@
import datetime
from typing import List
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
from remnawave.enums import ResponseType
from remnawave.models.subscriptions_settings import ResponseRule, ResponseRules
class NodeStatistic(BaseModel):
node_name: str = Field(alias="nodeName")
@ -105,26 +108,60 @@ class GetRemnawaveHealthResponseDto(BaseModel):
pm2_stats: List[PM2Stat] = Field(alias="pm2Stats")
class NodeMetric(BaseModel):
uuid: str
name: str
address: str
is_online: bool = Field(alias="isOnline")
cpu_usage: float = Field(alias="cpuUsage")
memory_usage: float = Field(alias="memoryUsage")
network_upload: int = Field(alias="networkUpload")
network_download: int = Field(alias="networkDownload")
uptime: int
last_seen: datetime.datetime = Field(alias="lastSeen")
connected_users: int = Field(alias="connectedUsers")
"""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
class GetNodesMetricsResponseDto(BaseModel):
nodes: List[NodeMetric]
class X25519KeyPair(BaseModel):
public_key: str = Field(alias="publicKey")
private_key: str = Field(alias="privateKey")
class GetX25519KeyPairResponseDto(BaseModel):
key_pairs: List[X25519KeyPair] = Field(alias="keyPairs")
key_pairs: List[X25519KeyPair] = Field(alias="keyPairs")
class EncryptHappCryptoLinkRequestDto(BaseModel):
link_to_encrypt: str = Field(serialization_alias="linkToEncrypt")
class EncryptHappCryptoLinkData(BaseModel):
encrypted_link: str = Field(alias="encryptedLink")
class EncryptHappCryptoLinkResponseDto(BaseModel):
response: EncryptHappCryptoLinkData
class DebugSrrMatcherRequestDto(BaseModel):
response_rules: ResponseRules = Field(serialization_alias="responseRules")
class DebugSrrMatcherData(BaseModel):
matched: bool
response_type: ResponseType = Field(alias="responseType")
matched_rule: Optional[ResponseRule] = Field(alias="matchedRule")
input_headers: Dict[str, str] = Field(alias="inputHeaders")
output_headers: Dict[str, str] = Field(alias="outputHeaders")
class DebugSrrMatcherResponseDto(BaseModel):
response: DebugSrrMatcherData

View file

@ -68,6 +68,7 @@ class CreateUserRequestDto(BaseModel):
active_internal_squads: list[str] | None = Field(
None, serialization_alias="activeInternalSquads"
)
external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid")
uuid: Optional[UUID] = Field(None, description="UUID of the user. Optional. If not provided, a new UUID will be generated by Remnawave.")
@ -128,6 +129,7 @@ class UserResponseDto(BaseModel):
)
happ: HappCrypto | None = Field(None, alias="happ")
tag: str | None = Field(None, alias="tag")
external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
@ -158,7 +160,7 @@ class TelegramUserResponseDto(RootModel[list[UserResponseDto]]):
class UsersResponseDto(BaseModel):
users: list[UserResponseDto]
total: float
total: int
class DeleteUserResponseDto(BaseModel):
@ -229,7 +231,7 @@ class SubscriptionRequestRecord(BaseModel):
class SubscriptionRequestsResponseData(BaseModel):
total: float
total: int
records: List[SubscriptionRequestRecord]

View file

@ -33,6 +33,13 @@ class UpdateUserFields(BaseModel):
)
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
hwid_device_limit: Optional[int] = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
telegram_id: Optional[int] = Field(
None, serialization_alias="telegramId"
)
class BulkAllUpdateUsersRequestDto(BaseModel):
@ -66,6 +73,10 @@ class BulkAllUpdateUsersRequestDto(BaseModel):
serialization_alias="telegramId"
)
hwid_device_limit: Optional[int] = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
email: Optional[str] = None
tag: Optional[TagStr] = Field(

77
tests/test_snippets.py Normal file
View file

@ -0,0 +1,77 @@
import pytest
from remnawave.models import (
CreateSnippetRequestDto,
DeleteSnippetRequestDto,
GetSnippetsResponseDto,
UpdateSnippetRequestDto,
)
def random_string(length=10):
import random
import string
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
@pytest.mark.asyncio
async def test_snippets_full_workflow(remnawave):
# Test getting all snippets
snippets = await remnawave.snippets.get_snippets()
assert isinstance(snippets, GetSnippetsResponseDto)
initial_count = snippets.total
# Test creating a snippet with unique name
rand_name = random_string()
create_request = CreateSnippetRequestDto(
name=rand_name,
snippet=[
{"type": "vmess", "port": 443},
{"type": "vless", "encryption": "none"}
]
)
created = await remnawave.snippets.create_snippet(create_request)
assert created.total == initial_count + 1
# Verify snippet was created
snippets_after_create = await remnawave.snippets.get_snippets()
snippet_names = [s.name for s in snippets_after_create.snippets]
assert rand_name in snippet_names
# Test updating the snippet
update_request = UpdateSnippetRequestDto(
name=rand_name,
snippet=[
{"type": "shadowsocks", "method": "aes-256-gcm"},
{"type": "trojan", "password": "test-password"}
]
)
updated = await remnawave.snippets.update_snippet(update_request)
assert updated.total == initial_count + 1
# Test deleting the snippet
delete_request = DeleteSnippetRequestDto(name=rand_name)
deleted = await remnawave.snippets.delete_snippet_by_name(delete_request)
assert deleted.total == initial_count
# Verify snippet was deleted
snippets_after_delete = await remnawave.snippets.get_snippets()
snippet_names_after = [s.name for s in snippets_after_delete.snippets]
assert rand_name not in snippet_names_after
@pytest.mark.asyncio
async def test_snippet_name_validation(remnawave):
"""Test that snippet names are properly validated"""
# Valid names
valid_names = ["Test Snippet", "My_Snippet", "Snippet-123", "A B C"]
for name in valid_names:
request = CreateSnippetRequestDto(name=name, snippet=[{"test": "data"}])
# Should not raise validation error
assert request.name == name
# Invalid names would be caught by Pydantic validation
with pytest.raises(ValueError):
CreateSnippetRequestDto(name="x", snippet=[]) # Too short
with pytest.raises(ValueError):
CreateSnippetRequestDto(name="", snippet=[]) # Empty name

View file

@ -1,19 +1,91 @@
import pytest
from remnawave.enums import TemplateType
from remnawave.models import GetTemplateResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto
from remnawave.models import (
CreateSubscriptionTemplateRequestDto,
CreateSubscriptionTemplateResponseDto,
DeleteSubscriptionTemplateResponseDto,
GetTemplateResponseDto,
GetTemplatesResponseDto,
UpdateTemplateRequestDto,
UpdateTemplateResponseDto,
)
def random_string(length=10):
import random
import string
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
@pytest.mark.asyncio
async def test_get_all_templates(remnawave):
"""Проверка получения всех шаблонов"""
templates = await remnawave.subscriptions_template.get_all_templates()
assert isinstance(templates, GetTemplatesResponseDto)
@pytest.mark.asyncio
async def test_subscriptions_template(remnawave):
template_type: TemplateType = TemplateType.SINGBOX
template = await remnawave.subscriptions_template.get_template(
template_type=template_type
async def test_create_template(remnawave):
"""Проверка создания шаблона"""
rand_name = random_string()
create_request = CreateSubscriptionTemplateRequestDto(
name=rand_name,
template_type=TemplateType.SINGBOX,
)
created_template = await remnawave.subscriptions_template.create_template(create_request)
assert isinstance(created_template, CreateSubscriptionTemplateResponseDto)
assert created_template.name == rand_name
assert created_template.template_type == TemplateType.SINGBOX
# Удаляем после проверки
await remnawave.subscriptions_template.delete_template(str(created_template.uuid))
@pytest.fixture
async def created_template(remnawave):
"""Фикстура: создать временный шаблон и удалить после теста"""
create_request = CreateSubscriptionTemplateRequestDto(
name="Temp Template",
template_type=TemplateType.SINGBOX,
)
template = await remnawave.subscriptions_template.create_template(create_request)
yield template
await remnawave.subscriptions_template.delete_template(str(template.uuid))
@pytest.mark.asyncio
async def test_get_template_by_uuid(remnawave, created_template):
"""Проверка получения шаблона по UUID"""
template = await remnawave.subscriptions_template.get_template_by_uuid(
str(created_template.uuid)
)
assert isinstance(template, GetTemplateResponseDto)
assert template.uuid == created_template.uuid
update_template = await remnawave.subscriptions_template.update_template(
UpdateTemplateRequestDto(template_type=template_type)
@pytest.mark.asyncio
async def test_update_template(remnawave, created_template):
"""Проверка обновления шаблона"""
update_request = UpdateTemplateRequestDto(
uuid=created_template.uuid,
name="Updated Template Name",
)
assert isinstance(update_template, UpdateTemplateResponseDto)
assert update_template.template_type == template_type
updated_template = await remnawave.subscriptions_template.update_template(update_request)
assert isinstance(updated_template, UpdateTemplateResponseDto)
assert updated_template.name == "Updated Template Name"
@pytest.mark.asyncio
async def test_delete_template(remnawave):
"""Проверка удаления шаблона"""
# Сначала создаем
create_request = CreateSubscriptionTemplateRequestDto(
name="Temp Delete Template",
template_type=TemplateType.SINGBOX,
)
created = await remnawave.subscriptions_template.create_template(create_request)
# Теперь удаляем
delete_response = await remnawave.subscriptions_template.delete_template(
str(created.uuid)
)
assert isinstance(delete_response, DeleteSubscriptionTemplateResponseDto)
assert delete_response.is_deleted is True