feat: обновить маршруты и добавить поддержку профилей для модификации в контроллерах и моделях

This commit is contained in:
Artem 2026-01-17 12:00:30 +01:00
parent ba1a221593
commit 93987d7452
No known key found for this signature in database
GPG key ID: 833485276B7902CE
10 changed files with 117 additions and 18 deletions

View file

@ -20,7 +20,7 @@ from remnawave.rapid import BaseController, get
class BandWidthStatsController(BaseController):
# ============ Legacy Endpoints (Deprecated) ============
@get("/bandwidth-stats/users/{user_uuid}/legacy-old", response_class=GetUserUsageByRangeResponseDto)
@get("/bandwidth-stats/users/{userUuid}/legacy", response_class=GetUserUsageByRangeResponseDto)
async def get_user_usage_legacy_old(
self,
user_uuid: Annotated[str, Path(description="UUID of the user", alias="userUuid")],
@ -30,7 +30,7 @@ class BandWidthStatsController(BaseController):
"""Get User Usage by Range (Legacy - Deprecated)"""
...
@get("/bandwidth-stats/nodes/{node_uuid}/users/legacy-old", response_class=GetNodeUserUsageByRangeResponseDto)
@get("/bandwidth-stats/nodes/{nodeUuid}/users/legacy", response_class=GetNodeUserUsageByRangeResponseDto)
async def get_node_user_usage_legacy_old(
self,
node_uuid: Annotated[str, Path(description="UUID of the node", alias="nodeUuid")],

View file

@ -19,7 +19,9 @@ from remnawave.models import (
UpdateNodeResponseDto,
RestartAllNodesRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto
ResetNodeTrafficResponseDto,
ProfileModificationRequestDto,
ProfileModificationResponseDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
@ -110,4 +112,12 @@ class NodesController(BaseController):
body: Annotated[ResetNodeTrafficRequestDto, PydanticBody()],
) -> ResetNodeTrafficResponseDto:
"""Reset Traffic All Nodes"""
...
@post("/nodes/bulk-actions/profile-modification", response_class=ProfileModificationResponseDto)
async def profile_modification(
self,
body: Annotated[ProfileModificationRequestDto, PydanticBody()],
) -> ProfileModificationResponseDto:
"""Modify Inbounds & Profile for many nodes"""
...

View file

@ -11,11 +11,19 @@ from remnawave.models import (
EncryptHappCryptoLinkResponseDto,
DebugSrrMatcherRequestDto,
DebugSrrMatcherResponseDto,
GetMetadataResponseDto
)
from remnawave.rapid import BaseController, get, post
class SystemController(BaseController):
@get("/system/metadata", response_class=GetMetadataResponseDto)
async def get_metadata(
self,
) -> GetMetadataResponseDto:
"""Get Remnawave Information"""
...
@get("/system/stats", response_class=GetStatsResponseDto)
async def get_stats(
self,

View file

@ -193,7 +193,9 @@ from .nodes import (
RestartAllNodesRequestDto, # Legacy alias,
RestartAllNodesRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto
ResetNodeTrafficResponseDto,
ProfileModificationRequestDto,
ProfileModificationResponseDto,
)
from .nodes_usage_history import (
GetNodeUserUsageByRangeResponseDto,
@ -263,6 +265,7 @@ from .system import (
DebugSrrMatcherResponseDto,
EncryptHappCryptoLinkRequestDto,
EncryptHappCryptoLinkResponseDto,
GetMetadataResponseDto
)
from .users import (
# Request DTOs
@ -483,6 +486,8 @@ __all__ = [
"RestartAllNodesRequestBodyDto",
"ResetNodeTrafficRequestDto",
"ResetNodeTrafficResponseDto",
"ProfileModificationRequestDto",
"ProfileModificationResponseDto",
# Hosts models
"CreateHostRequestDto",
"CreateHostResponseDto",
@ -570,6 +575,7 @@ __all__ = [
"DebugSrrMatcherResponseDto",
"EncryptHappCryptoLinkRequestDto",
"EncryptHappCryptoLinkResponseDto",
"GetMetadataResponseDto"
# XRay config models
"ConfigResponseDto", # Legacy alias
"GetConfigResponseDto",

View file

@ -230,6 +230,27 @@ class ResetNodeTrafficRequestDto(BaseModel):
class ResetNodeTrafficResponseDto(RestartEventResponse):
pass
class ConfigProfileData(BaseModel):
"""Config profile data for modification"""
active_config_profile_uuid: str = Field(alias="activeConfigProfileUuid")
active_inbounds: List[str] = Field(alias="activeInbounds", min_length=1)
class ProfileModificationRequestDto(BaseModel):
"""Request to modify profiles for multiple nodes"""
uuids: List[str] = Field(min_length=1)
config_profile: ConfigProfileData = Field(alias="configProfile")
class ProfileModificationResponseData(BaseModel):
"""Profile modification response data"""
event_sent: bool = Field(alias="eventSent")
class ProfileModificationResponseDto(ProfileModificationResponseData):
"""Profile modification response"""
pass
# Для обратной совместимости
RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto
NodesResponseDto = NodeResponseDto

View file

@ -157,7 +157,7 @@ class RawHost(BaseModel):
mldsa65_verify: Optional[str] = Field(None, alias="mldsa65Verify")
encryption: Optional[str] = None
protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions")
db_data: RawHostDbData = Field(alias="dbData")
db_data: Optional[RawHostDbData] = Field(None, alias="dbData")
xray_json_template: Optional[Dict[str, Any]] = Field(None, alias="xrayJsonTemplate")

View file

@ -61,7 +61,8 @@ class CustomRemarksDto(BaseModel):
limited_users: List[str] = Field(alias="limitedUsers", min_length=1)
disabled_users: List[str] = Field(alias="disabledUsers", min_length=1)
empty_hosts: List[str] = Field(alias="emptyHosts", min_length=1)
empty_internal_squads: List[str] = Field(alias="emptyInternalSquads", min_length=1)
hwid_max_devices_exceeded: List[str] = Field(alias="HWIDMaxDevicesExceeded", min_length=1)
hwid_not_supported: List[str] = Field(alias="HWIDNotSupported", min_length=1)
class HwidSettingsDto(BaseModel):

View file

@ -171,4 +171,40 @@ class DebugSrrMatcherData(BaseModel):
class DebugSrrMatcherResponseDto(DebugSrrMatcherData):
pass
class BuildInfo(BaseModel):
"""Build information"""
time: str
number: str
class GitBackendInfo(BaseModel):
"""Git backend information"""
commit_sha: str = Field(alias="commitSha")
branch: str
commit_url: str = Field(alias="commitUrl")
class GitFrontendInfo(BaseModel):
"""Git frontend information"""
commit_sha: str = Field(alias="commitSha")
commit_url: str = Field(alias="commitUrl")
class GitInfo(BaseModel):
"""Git information"""
backend: GitBackendInfo
frontend: GitFrontendInfo
class MetadataResponse(BaseModel):
"""Metadata response data"""
version: str
build: BuildInfo
git: GitInfo
class GetMetadataResponseDto(MetadataResponse):
"""Get metadata response"""
pass

View file

@ -16,7 +16,7 @@ from remnawave.models import (
GetStatsNodeUsersUsageResponseDto,
GetStatsUserUsageResponseDto,
)
from tests.utils import generate_isoformat_range
from tests.utils import generate_date_range, generate_isoformat_range
@pytest.mark.asyncio
async def test_legacy_user_usage(remnawave):
@ -27,15 +27,19 @@ async def test_legacy_user_usage(remnawave):
pytest.skip("No users available for testing")
user_uuid = str(users.users[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
user_usage = await remnawave.bandwidthstats.get_user_usage_legacy_old(
user_uuid=user_uuid,
start=start,
end=end
)
assert isinstance(user_usage, GetUserUsageByRangeResponseDto)
assert len(user_usage) >= 0
assert hasattr(user_usage, 'root')
assert isinstance(user_usage.root, list)
if user_usage.root:
first_item = user_usage.root[0]
assert hasattr(first_item, 'user_uuid')
assert hasattr(first_item, 'node_uuid')
@pytest.mark.asyncio
@ -47,14 +51,21 @@ async def test_legacy_node_user_usage(remnawave):
pytest.skip("No nodes available for testing")
node_uuid = str(nodes[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
node_user_usage = await remnawave.bandwidthstats.get_node_user_usage_legacy_old(
node_uuid=node_uuid,
start=start,
end=end
)
assert isinstance(node_user_usage, GetNodeUserUsageByRangeResponseDto)
assert hasattr(node_user_usage, 'root')
assert isinstance(node_user_usage.root, list)
if node_user_usage.root:
first_item = node_user_usage.root[0]
assert hasattr(first_item, 'user_uuid')
assert hasattr(first_item, 'username')
assert hasattr(first_item, 'node_uuid')
assert hasattr(first_item, 'total')
assert len(node_user_usage) >= 0
@ -79,7 +90,7 @@ async def test_stats_nodes_realtime_usage(remnawave):
@pytest.mark.asyncio
async def test_stats_nodes_usage(remnawave):
"""Test new stats nodes usage endpoint with charts"""
start, end = generate_isoformat_range()
start, end = generate_date_range()
nodes_usage = await remnawave.bandwidthstats.get_stats_nodes_usage(
start=start,
@ -109,7 +120,7 @@ async def test_stats_node_users_usage(remnawave):
pytest.skip("No nodes available for testing")
node_uuid = str(nodes[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
node_users_usage = await remnawave.bandwidthstats.get_stats_node_users_usage(
uuid=node_uuid,
@ -138,7 +149,7 @@ async def test_stats_user_usage(remnawave):
pytest.skip("No users available for testing")
user_uuid = str(users.users[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
user_usage = await remnawave.bandwidthstats.get_stats_user_usage(
uuid=user_uuid,
@ -169,7 +180,7 @@ async def test_legacy_stats_user_usage(remnawave):
pytest.skip("No users available for testing")
user_uuid = str(users.users[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
legacy_user_usage = await remnawave.bandwidthstats.get_user_usage_legacy_stats(
uuid=user_uuid,
@ -198,7 +209,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave):
pytest.skip("No nodes available for testing")
node_uuid = str(nodes[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
legacy_node_users = await remnawave.bandwidthstats.get_node_users_usage_legacy_stats(
uuid=node_uuid,
@ -220,7 +231,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave):
@pytest.mark.asyncio
async def test_bandwidth_data_structure(remnawave):
"""Test bandwidth stats data structure validity"""
start, end = generate_isoformat_range()
start, end = generate_date_range()
# Get realtime data
realtime = await remnawave.bandwidthstats.get_nodes_realtime_usage()

View file

@ -22,3 +22,9 @@ def generate_isoformat_range() -> Tuple[str, str]:
start = (datetime.now() - timedelta(days=7)).isoformat(timespec="seconds")
end = datetime.now().isoformat(timespec="seconds")
return start, end
def generate_date_range() -> tuple[str, str]:
"""Generate date range in YYYY-MM-DD format for the past 7 days"""
end = datetime.now()
start = end - timedelta(days=7)
return start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d')