diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py index 252d9a13..c699fd9f 100644 --- a/app/cabinet/routes/admin_users.py +++ b/app/cabinet/routes/admin_users.py @@ -686,48 +686,67 @@ async def get_user_node_usage( start_date = end_date - timedelta(days=days) async with service.get_api_client() as api: - # Get bandwidth stats for user - stats = await api.get_bandwidth_stats_user( - user.remnawave_uuid, - start_date.strftime('%Y-%m-%dT%H:%M:%S.000Z'), - end_date.strftime('%Y-%m-%dT%H:%M:%S.000Z'), - ) + # Get user's accessible nodes + accessible_nodes = await api.get_user_accessible_nodes(user.remnawave_uuid) + if not accessible_nodes: + return UserNodeUsageResponse(items=[], period_days=days) - # Get all nodes for name resolution - nodes = await api.get_all_nodes() - node_map = {n.uuid: n.name for n in nodes} + node_name_map = {n.uuid: n.node_name for n in accessible_nodes} + node_cc_map = {n.uuid: n.country_code for n in accessible_nodes} + + # Get bandwidth stats for user (use date-only format) + start_str = start_date.strftime('%Y-%m-%d') + end_str = end_date.strftime('%Y-%m-%d') items = [] - # Stats response contains per-node breakdown - if isinstance(stats, list): - for entry in stats: - node_uuid = entry.get('nodeUuid', '') - total = entry.get('totalBytes', 0) or entry.get('total', 0) - if node_uuid and total > 0: - items.append( - UserNodeUsageItem( - node_uuid=node_uuid, - node_name=node_map.get(node_uuid, node_uuid[:8]), - total_bytes=total, + try: + stats = await api.get_bandwidth_stats_user(user.remnawave_uuid, start_str, end_str) + + if isinstance(stats, list): + for entry in stats: + node_uuid = entry.get('nodeUuid', '') + total = entry.get('totalBytes', 0) or entry.get('total', 0) + if node_uuid and total > 0: + items.append( + UserNodeUsageItem( + node_uuid=node_uuid, + node_name=node_name_map.get(node_uuid, node_uuid[:8]), + country_code=node_cc_map.get(node_uuid, ''), + total_bytes=total, + ) ) - ) - elif isinstance(stats, dict): - # Handle dict format with node entries - for node_uuid, data in stats.items(): - if isinstance(data, dict): - total = data.get('totalBytes', 0) or data.get('total', 0) - elif isinstance(data, (int, float)): - total = int(data) - else: - continue - if total > 0: - items.append( - UserNodeUsageItem( - node_uuid=node_uuid, - node_name=node_map.get(node_uuid, node_uuid[:8]), - total_bytes=total, + elif isinstance(stats, dict): + for node_uuid, data in stats.items(): + if isinstance(data, dict): + total = data.get('totalBytes', 0) or data.get('total', 0) + elif isinstance(data, (int, float)): + total = int(data) + else: + continue + if total > 0: + items.append( + UserNodeUsageItem( + node_uuid=node_uuid, + node_name=node_name_map.get(node_uuid, node_uuid[:8]), + country_code=node_cc_map.get(node_uuid, ''), + total_bytes=total, + ) ) + except Exception: + logger.warning(f'Failed to get bandwidth stats for user {user_id}, returning nodes without traffic') + + # Add accessible nodes with zero traffic if not in stats + seen_uuids = {item.node_uuid for item in items} + for node in accessible_nodes: + if node.uuid not in seen_uuids: + items.append( + UserNodeUsageItem( + node_uuid=node.uuid, + node_name=node.node_name, + country_code=node.country_code, + total_bytes=0, ) + ) # Sort by traffic descending items.sort(key=lambda x: x.total_bytes, reverse=True) diff --git a/app/cabinet/schemas/users.py b/app/cabinet/schemas/users.py index 3bb407e7..ffdb90c3 100644 --- a/app/cabinet/schemas/users.py +++ b/app/cabinet/schemas/users.py @@ -229,6 +229,7 @@ class UserNodeUsageItem(BaseModel): node_uuid: str node_name: str + country_code: str = '' total_bytes: int diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py index 4e7db3ab..82bbe1ba 100644 --- a/app/external/remnawave_api.py +++ b/app/external/remnawave_api.py @@ -564,6 +564,33 @@ class RemnaWaveAPI: user = self._parse_user(response['response']) return await self.enrich_user_with_happ_link(user) + async def get_user_accessible_nodes(self, uuid: str) -> list[RemnaWaveAccessibleNode]: + """Получает список доступных нод для пользователя""" + try: + response = await self._make_request('GET', f'/api/users/{uuid}/accessible-nodes') + nodes_data = response.get('response', {}).get('activeNodes', []) + result = [] + for node in nodes_data: + # Collect inbounds from activeSquads + inbounds: list[str] = [] + for squad in node.get('activeSquads', []): + inbounds.extend(squad.get('activeInbounds', [])) + result.append( + RemnaWaveAccessibleNode( + uuid=node['uuid'], + node_name=node['nodeName'], + country_code=node['countryCode'], + config_profile_uuid=node.get('configProfileUuid', ''), + config_profile_name=node.get('configProfileName', ''), + active_inbounds=inbounds, + ) + ) + return result + except RemnaWaveAPIError as e: + if e.status_code == 404: + return [] + raise + async def get_all_users(self, start: int = 0, size: int = 100, enrich_happ_links: bool = False) -> dict[str, Any]: params = {'start': start, 'size': size} response = await self._make_request('GET', '/api/users', params=params)