feat: Refactor API endpoints and models for HWID management; add new HWID user controller and models

This commit is contained in:
Artem 2025-05-02 15:05:31 +02:00
parent 2b29f74087
commit 42f1bfa066
No known key found for this signature in database
GPG key ID: 833485276B7902CE
21 changed files with 214 additions and 95 deletions

1
.gitignore vendored
View file

@ -177,3 +177,4 @@ test.py
.DS_Store
docs/
openapi/
.DS_Store

View file

@ -49,6 +49,8 @@ addopts = [ "-n", "logical", "-ra", "--strict-config", "--strict-markers" ]
testpaths = ["tests"]
log_cli_level = "INFO"
xfail_strict = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]

View file

@ -1,2 +1,3 @@
[pytest]
asyncio_mode = auto
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function

View file

@ -14,7 +14,7 @@ from remnawave_api.rapid import BaseController, delete, get, post
class APITokensManagementController(BaseController):
@post("/tokens/create", response_class=CreateApiTokenResponseDto)
@post("/tokens", response_class=CreateApiTokenResponseDto)
async def create(
self,
body: Annotated[CreateApiTokenRequestDto, PydanticBody()],
@ -22,7 +22,7 @@ class APITokensManagementController(BaseController):
"""Create new API token"""
...
@delete("/tokens/delete/{uuid}", response_class=DeleteApiTokenResponseDto)
@delete("/tokens/{uuid}", response_class=DeleteApiTokenResponseDto)
async def delete(
self,
uuid: Annotated[str, Path(description="UUID of the API token")],

View file

@ -2,7 +2,7 @@ from typing import Annotated
from rapid_api_client import Query
from remnawave_api.models import NodesUsageResponseDto
from remnawave_api.models import NodesUsageResponseDto, NodesRealtimeUsageResponseDto
from remnawave_api.rapid import BaseController, get
@ -15,3 +15,11 @@ class BandWidthStatsController(BaseController):
) -> NodesUsageResponseDto:
"""Get Nodes Usage By Range"""
...
@get("/nodes/usage/realtime", response_class=NodesRealtimeUsageResponseDto)
async def get_nodes_usage_realtime(
self,
) -> NodesRealtimeUsageResponseDto:
"""Get Nodes Usage Realtime"""
...

View file

@ -12,11 +12,11 @@ from remnawave_api.models import (
ReorderHostResponseDto,
UpdateHostRequestDto,
)
from remnawave_api.rapid import AttributeBody, BaseController, delete, get, post
from remnawave_api.rapid import AttributeBody, BaseController, delete, get, post, patch
class HostsController(BaseController):
@post("/hosts/create", response_class=HostResponseDto)
@post("/hosts", response_class=HostResponseDto)
async def create_host(
self,
body: Annotated[CreateHostRequestDto, PydanticBody()],
@ -24,7 +24,7 @@ class HostsController(BaseController):
"""Create Host"""
...
@post("/hosts/update", response_class=HostResponseDto)
@patch("/hosts", response_class=HostResponseDto)
async def update_host(
self,
body: Annotated[UpdateHostRequestDto, PydanticBody()],
@ -32,14 +32,22 @@ class HostsController(BaseController):
"""Update Host"""
...
@get("/hosts/all", response_class=HostsResponseDto)
@get("/hosts", response_class=HostsResponseDto)
async def get_all_hosts(
self,
) -> HostsResponseDto:
"""Get All Hosts"""
...
@delete("/hosts/{uuid}", response_class=DeleteHostResponseDto)
async def delete_host(
self,
uuid: Annotated[str, Path(description="UUID of the host")],
) -> DeleteHostResponseDto:
"""Delete Host"""
...
@get("/hosts/get-one/{uuid}", response_class=HostResponseDto)
@get("/hosts/{uuid}", response_class=HostResponseDto)
async def get_one_host(
self,
uuid: Annotated[str, Path(description="UUID of the host")],
@ -47,18 +55,10 @@ class HostsController(BaseController):
"""Get One Host"""
...
@post("/hosts/reorder", response_class=ReorderHostResponseDto)
@post("/hosts/actions/reorder", response_class=ReorderHostResponseDto)
async def reorder_hosts(
self,
hosts: Annotated[List[ReorderHostRequestDto], AttributeBody()],
) -> ReorderHostResponseDto:
"""Reorder Hosts"""
...
@delete("/hosts/delete/{uuid}", response_class=DeleteHostResponseDto)
async def delete_host(
self,
uuid: Annotated[str, Path(description="UUID of the host")],
) -> DeleteHostResponseDto:
"""Delete Host"""
...

View file

@ -0,0 +1,38 @@
from typing import Annotated
from uuid import UUID
from rapid_api_client import PydanticBody
from remnawave_api.models import (
HWIDUserResponseDto,
HWIDUserResponseDtoList,
CreateHWIDUser,
HWIDDeleteRequest
)
from remnawave_api.rapid import AttributeBody, BaseController, post
class HWIDUserController(BaseController):
@post("/hwid/devices", response_class=HWIDUserResponseDtoList)
async def add_hwid_to_users(
self,
body: Annotated[CreateHWIDUser, PydanticBody()],
) -> HWIDUserResponseDtoList:
"""Create a user HWID device"""
...
@post("/hwid/devices/delete", response_class=HWIDUserResponseDtoList)
async def delete_hwid_to_user(
self,
body: Annotated[HWIDDeleteRequest, PydanticBody()],
) -> HWIDUserResponseDtoList:
"""Delete a user HWID device"""
...
@post("/hwid/devices/{uuid}", response_class=HWIDUserResponseDto)
async def get_hwid_user(
self,
uuid: Annotated[UUID, AttributeBody()],
) -> HWIDUserResponseDto:
"""Get a user HWID device"""
...

View file

@ -3,7 +3,7 @@ from remnawave_api.rapid import BaseController, get
class KeygenController(BaseController):
@get("/keygen/get", response_class=PubKeyResponseDto)
@get("/keygen", response_class=PubKeyResponseDto)
async def generate_key(
self,
) -> PubKeyResponseDto:

View file

@ -16,7 +16,7 @@ from remnawave_api.rapid import AttributeBody, BaseController, delete, get, patc
class NodesController(BaseController):
@post("/nodes/create", response_class=NodeResponseDto)
@post("/nodes", response_class=NodeResponseDto)
async def create_node(
self,
body: Annotated[CreateNodeRequestDto, PydanticBody()],
@ -24,46 +24,30 @@ class NodesController(BaseController):
"""Create Node"""
...
@get("/nodes/get-all", response_class=NodesResponseDto)
@get("/nodes", response_class=NodesResponseDto)
async def get_all_nodes(
self,
) -> NodesResponseDto:
"""Get All Nodes"""
...
@get("/nodes/get-one/{uuid}", response_class=NodeResponseDto)
@get("/nodes/{uuid}", response_class=NodeResponseDto)
async def get_one_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Get One Node"""
...
@patch("/nodes/enable/{uuid}", response_class=NodeResponseDto)
async def enable_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Enable Node"""
...
@patch("/nodes/disable/{uuid}", response_class=NodeResponseDto)
async def disable_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Disable Node"""
...
@delete("/nodes/delete/{uuid}", response_class=DeleteNodeResponseDto)
@delete("/nodes/{uuid}", response_class=DeleteNodeResponseDto)
async def delete_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> DeleteNodeResponseDto:
"""Delete Node"""
...
@post("/nodes/update", response_class=NodeResponseDto)
@patch("/nodes", response_class=NodeResponseDto)
async def update_node(
self,
body: Annotated[UpdateNodeRequestDto, PydanticBody()],
@ -71,7 +55,23 @@ class NodesController(BaseController):
"""Update Node"""
...
@get("/nodes/restart/{uuid}", response_class=RestartNodeResponseDto)
@post("/nodes/{uuid}/actions/enable", response_class=NodeResponseDto)
async def enable_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Enable Node"""
...
@post("/nodes/{uuid}/actions/disable", response_class=NodeResponseDto)
async def disable_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Disable Node"""
...
@post("/nodes/{uuid}/actions/restart", response_class=RestartNodeResponseDto)
async def restart_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
@ -79,14 +79,14 @@ class NodesController(BaseController):
"""Restart Node"""
...
@patch("/nodes/restart-all", response_class=RestartNodeResponseDto)
@post("/nodes/actions/restart-all", response_class=RestartNodeResponseDto)
async def restart_all_nodes(
self,
) -> RestartNodeResponseDto:
"""Restart All Nodes"""
...
@post("/nodes/reorder", response_class=NodesResponseDto)
@post("/nodes/actions/reorder", response_class=NodesResponseDto)
async def reorder_nodes(
self,
nodes: Annotated[List[ReorderNodeRequestDto], AttributeBody()],

View file

@ -6,19 +6,19 @@ from remnawave_api.models import (
SubscriptionSettingsResponseDto,
UpdateSubscriptionSettingsRequestDto,
)
from remnawave_api.rapid import BaseController, get, post
from remnawave_api.rapid import BaseController, get, patch
class SubscriptionsSettingsController(BaseController):
@get("/subscription-settings/get", response_class=SubscriptionSettingsResponseDto)
@get("/subscription-settings", response_class=SubscriptionSettingsResponseDto)
async def get_settings(
self,
) -> SubscriptionSettingsResponseDto:
"""Get Subscription Settings"""
...
@post(
"/subscription-settings/update",
@patch(
"/subscription-settings",
response_class=SubscriptionSettingsResponseDto,
)
async def update_settings(

View file

@ -4,12 +4,12 @@ from rapid_api_client.annotations import Path, PydanticBody
from remnawave_api.enums import TemplateType
from remnawave_api.models import TemplateResponseDto, UpdateTemplateRequestDto
from remnawave_api.rapid import BaseController, get, post
from remnawave_api.rapid import BaseController, get, put
class SubscriptionsTemplateController(BaseController):
@get(
"/subscription-templates/get-template/{template_type}",
"/subscription-templates/{template_type}",
response_class=TemplateResponseDto,
)
async def get_template(
@ -19,8 +19,8 @@ class SubscriptionsTemplateController(BaseController):
"""Get Template"""
...
@post(
"/subscription-templates/update-template",
@put(
"/subscription-templates",
response_class=TemplateResponseDto,
)
async def update_template(

View file

@ -14,14 +14,14 @@ class SystemController(BaseController):
"""Get System Stats"""
...
@get("/system/bandwidth", response_class=BandwidthStatisticResponseDto)
@get("/system/stats/bandwidth", response_class=BandwidthStatisticResponseDto)
async def get_bandwidth_stats(
self,
) -> BandwidthStatisticResponseDto:
"""Get System Bandwidth Statistics"""
...
@get("/system/statistics/nodes", response_class=NodesStatisticResponseDto)
@get("/system/stats/nodes", response_class=NodesStatisticResponseDto)
async def get_nodes_statistics(
self,
) -> NodesStatisticResponseDto:

View file

@ -24,7 +24,7 @@ class UsersController(BaseController):
"""Create User"""
...
@post("/users/update", response_class=UserResponseDto)
@patch("/users", response_class=UserResponseDto)
async def update_user(
self,
body: Annotated[UpdateUserRequestDto, PydanticBody()],
@ -32,11 +32,11 @@ class UsersController(BaseController):
"""Update User"""
...
@get("/users/v2", response_class=UsersResponseDto)
@get("/users", response_class=UsersResponseDto)
async def get_all_users_v2(
self,
start: Annotated[int, Query(description="Index to start pagination from")],
size: Annotated[int, Query(description="Number of users per page")],
start: Annotated[int, Query(default=0, ge=0, description="Index to start pagination from")],
size: Annotated[int, Query(default=25, ge=1, description="Number of users per page")],
) -> UsersResponseDto:
"""
Get users page from the end.
@ -49,24 +49,8 @@ class UsersController(BaseController):
UsersResponseDto
"""
...
@patch("/users/revoke/{uuid}", response_class=UserResponseDto)
async def revoke_user_subscription(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Revoke User Subscription"""
...
@patch("/users/disable/{uuid}", response_class=UserResponseDto)
async def disable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Disable User"""
...
@delete("/users/delete/{uuid}", response_class=DeleteUserResponseDto)
@delete("/users/{uuid}", response_class=DeleteUserResponseDto)
async def delete_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
@ -74,7 +58,23 @@ class UsersController(BaseController):
"""Delete User"""
...
@patch("/users/enable/{uuid}", response_class=UserResponseDto)
@post("users/{uuid}/actions/revoke", response_class=UserResponseDto)
async def revoke_user_subscription(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Revoke User Subscription"""
...
@post("/users/{uuid}/actions/disable", response_class=UserResponseDto)
async def disable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Disable User"""
...
@post("/users/{uuid}/actions/enable", response_class=UserResponseDto)
async def enable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
@ -82,15 +82,23 @@ class UsersController(BaseController):
"""Enable User"""
...
@patch("/users/reset-traffic/{uuid}", response_class=UserResponseDto)
@post("/users/{uuid}/actions/reset-traffic", response_class=UserResponseDto)
async def reset_user_traffic(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Reset User Traffic"""
...
@post("/users/{uuid}/actions/activate-all-inbounds", response_class=UserResponseDto)
async def activate_all_inbounds(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Activate All Inbounds"""
...
@get("/users/short-uuid/{short_uuid}", response_class=UserResponseDto)
@get("/users/by-short-uuid/{short_uuid}", response_class=UserResponseDto)
async def get_user_by_short_uuid(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
@ -98,7 +106,7 @@ class UsersController(BaseController):
"""Get User By Short UUID"""
...
@get("/users/sub-uuid/{subscription_uuid}", response_class=UserResponseDto)
@get("/users/by-subscription-uuid/{subscription_uuid}", response_class=UserResponseDto)
async def get_user_by_subscription_uuid(
self,
subscription_uuid: Annotated[str, Path(description="UUID of the subscription")],
@ -106,7 +114,7 @@ class UsersController(BaseController):
"""Get User By Subscription UUID"""
...
@get("/users/uuid/{uuid}", response_class=UserResponseDto)
@get("/users/{uuid}", response_class=UserResponseDto)
async def get_user_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
@ -114,7 +122,7 @@ class UsersController(BaseController):
"""Get User By UUID"""
...
@get("/users/username/{username}", response_class=UserResponseDto)
@get("/users/by-username/{username}", response_class=UserResponseDto)
async def get_user_by_username(
self,
username: Annotated[str, Path(description="Username of the user")],
@ -123,7 +131,7 @@ class UsersController(BaseController):
...
@get(
"/users/tg/{telegram_id}",
"/users/by-telegram-id/{telegram_id}",
response_class=TelegramUserResponseDto,
)
async def get_users_by_telegram_id(
@ -133,7 +141,7 @@ class UsersController(BaseController):
"""Get Users By Telegram ID"""
...
@get("/users/email/{email}", response_class=EmailUserResponseDto)
@get("/users/by-email/{email}", response_class=EmailUserResponseDto)
async def get_users_by_email(
self,
email: Annotated[str, Path(description="Email of the user")],

View file

@ -78,7 +78,7 @@ class UsersBulkActionsController(BaseController):
"""Bulk Update All Users"""
...
@patch(
@post(
"/users/bulk/all/reset-traffic",
response_class=BulkAllResetTrafficUsersResponseDto,
)

View file

@ -8,7 +8,7 @@ from remnawave_api.rapid import BaseController, get
class UsersStatsController(BaseController):
@get(
"/users/stats/usage/range/{uuid}",
"/users/stats/usage/{uuid}/range",
response_class=UserUsageByRangeResponseDto,
)
async def get_user_usage_by_range(

View file

@ -3,18 +3,18 @@ from typing import Annotated
from rapid_api_client.annotations import JsonBody
from remnawave_api.models import ConfigResponseDto
from remnawave_api.rapid import BaseController, get, post
from remnawave_api.rapid import BaseController, get, put
class XrayConfigController(BaseController):
@get("/xray/get-config", response_class=ConfigResponseDto)
@get("/xray", response_class=ConfigResponseDto)
async def get_config(
self,
) -> ConfigResponseDto:
"""Get Xray Config"""
...
@post("/xray/update-config", response_class=ConfigResponseDto)
@put("/xray", response_class=ConfigResponseDto)
async def update_config(
self,
body: Annotated[dict, JsonBody()],

View file

@ -11,7 +11,7 @@ from .auth import (
RegisterResponseDto,
StatusResponseDto,
)
from .bandwidthstats import NodesUsageResponseDto, NodeUsageResponseDto
from .bandwidthstats import NodesUsageResponseDto, NodeUsageResponseDto, NodesRealtimeUsageResponseDto, NodeRealtimeUsageResponseDto
from .hosts import (
CreateHostRequestDto,
DeleteHostResponseDto,
@ -92,6 +92,12 @@ from .users_bulk_actions import (
)
from .users_stats import UserUsageByRange, UserUsageByRangeResponseDto
from .xray_config import ConfigResponseDto
from .hwid import (
CreateHWIDUser,
HWIDUserResponseDto,
HWIDUserResponseDtoList,
HWIDDeleteRequest
)
__all__ = [
"CPUStatistic",
@ -172,4 +178,10 @@ __all__ = [
"CreateApiTokenResponseDto",
"CreateApiTokenRequestDto",
"DeleteApiTokenResponseDto",
"NodesRealtimeUsageResponseDto",
"NodeRealtimeUsageResponseDto",
"CreateHWIDUser",
"HWIDUserResponseDto",
"HWIDUserResponseDtoList",
"HWIDDeleteRequest"
]

View file

@ -1,7 +1,7 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
class CreateApiTokenRequestDto(BaseModel):
@ -39,5 +39,4 @@ class FindAllApiTokensResponseDto(BaseModel):
api_keys: List[ApiTokenDto] = Field(..., alias="apiKeys")
docs: DocsInfoDto
class Config:
populate_by_name = True
model_config = ConfigDict(populate_by_name=True)

View file

@ -19,3 +19,18 @@ class NodeUsageResponseDto(BaseModel):
class NodesUsageResponseDto(BaseModel):
response: List[NodeUsageResponseDto]
class NodeRealtimeUsageResponseDto(BaseModel):
node_uuid: UUID = Field(alias="nodeUuid")
node_name: str = Field(alias="nodeName")
country_code: str = Field(alias="countryCode")
download_bytes: float = Field(alias="downloadBytes")
upload_bytes: float = Field(alias="uploadBytes")
total_bytes: float = Field(alias="totalBytes")
download_speed_bps: float = Field(alias="downloadSpeedBps")
upload_speed_bps: float = Field(alias="uploadSpeedBps")
total_speed_bps: float = Field(alias="totalSpeedBps")
class NodesRealtimeUsageResponseDto(BaseModel):
response: List[NodeRealtimeUsageResponseDto]

View file

@ -0,0 +1,30 @@
from typing import Any, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
class CreateHWIDUser(BaseModel):
hwid: str
userUuid: UUID
platform: str
osVersion: str
deviceModel: str
userAgent: str
class HWIDUserResponseDto(BaseModel):
hwid: str
user_uuid: UUID = Field(alias="userUuid")
platform: str
os_version: str = Field(alias="osVersion")
device_model: str = Field(alias="deviceModel")
user_agent: str = Field(alias="userAgent")
created_at: str = Field(alias="createdAt")
updated_at: str = Field(alias="updatedAt")
class HWIDUserResponseDtoList(BaseModel):
response: List[HWIDUserResponseDto]
class HWIDDeleteRequest(BaseModel):
hwid: str
userUuid: UUID

View file

@ -25,7 +25,6 @@ class SubscriptionSettingsResponseDto(BaseModel):
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
class UpdateSubscriptionSettingsRequestDto(BaseModel):
uuid: UUID
profile_title: Optional[str] = Field(None, serialization_alias="profileTitle")
@ -42,6 +41,9 @@ class UpdateSubscriptionSettingsRequestDto(BaseModel):
add_username_to_base_subscription: Optional[bool] = Field(
None, serialization_alias="addUsernameToBaseSubscription"
)
is_show_custom_remarks: Optional[bool] = Field(
None, serialization_alias="isShowCustomRemarks"
)
happ_announce: Annotated[Optional[str], StringConstraints(max_length=200)] = Field(
None, serialization_alias="happAnnounce"
)
@ -55,3 +57,6 @@ class UpdateSubscriptionSettingsRequestDto(BaseModel):
disabled_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="disabledUsersRemarks"
)
custom_response_headers: Optional[dict] = Field(
None, serialization_alias="customResponseHeaders"
)