mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-01-22 05:38:32 -05:00
New Features: - API endpoints now support comprehensive input validation with detailed error responses via Pydantic models. - OpenAPI specification endpoint (/openapi.json) and interactive Swagger UI documentation (/docs) now available for API discovery. - Enhanced MCP session lifecycle management with create, retrieve, and delete operations. - Network diagnostic tools: traceroute, nslookup, NMAP scanning, and network topology viewing exposed via API. - Device search, filtering by status (including 'offline'), and bulk operations (copy, delete, update). - Wake-on-LAN functionality for remote device management. - Added dynamic tool disablement and status reporting. Bug Fixes: - Fixed get_tools_status in registry to correctly return boolean values instead of None for enabled tools. - Improved error handling for invalid API inputs with standardized validation responses. - Fixed OPTIONS request handling for cross-origin requests. Refactoring: - Significant refactoring of api_server_start.py to use decorator-based validation (@validate_request).
159 lines
5.1 KiB
Python
159 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import threading
|
|
from copy import deepcopy
|
|
from typing import List, Dict, Any, Literal, Optional, Type, Set
|
|
from pydantic import BaseModel
|
|
|
|
# Thread-safe registry
|
|
_registry: List[Dict[str, Any]] = []
|
|
_registry_lock = threading.Lock()
|
|
_operation_ids: Set[str] = set()
|
|
_disabled_tools: Set[str] = set()
|
|
|
|
|
|
class DuplicateOperationIdError(Exception):
|
|
"""Raised when an operationId is registered more than once."""
|
|
pass
|
|
|
|
|
|
def set_tool_disabled(operation_id: str, disabled: bool = True) -> bool:
|
|
"""
|
|
Enable or disable a tool by operation_id.
|
|
|
|
Args:
|
|
operation_id: The unique operation_id of the tool
|
|
disabled: True to disable, False to enable
|
|
|
|
Returns:
|
|
bool: True if operation_id exists, False otherwise
|
|
"""
|
|
with _registry_lock:
|
|
if operation_id not in _operation_ids:
|
|
return False
|
|
|
|
if disabled:
|
|
_disabled_tools.add(operation_id)
|
|
else:
|
|
_disabled_tools.discard(operation_id)
|
|
return True
|
|
|
|
|
|
def is_tool_disabled(operation_id: str) -> bool:
|
|
"""
|
|
Check if a tool is disabled.
|
|
Checks both the unique operation_id and the original_operation_id.
|
|
"""
|
|
with _registry_lock:
|
|
if operation_id in _disabled_tools:
|
|
return True
|
|
|
|
# Also check if the original base ID is disabled
|
|
for entry in _registry:
|
|
if entry["operation_id"] == operation_id:
|
|
orig_id = entry.get("original_operation_id")
|
|
if orig_id and orig_id in _disabled_tools:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_disabled_tools() -> List[str]:
|
|
"""Get list of all disabled operation_ids."""
|
|
with _registry_lock:
|
|
return list(_disabled_tools)
|
|
|
|
|
|
def get_tools_status() -> List[Dict[str, Any]]:
|
|
"""
|
|
Get a list of all registered tools and their disabled status.
|
|
Useful for backend-to-frontend communication.
|
|
"""
|
|
tools = []
|
|
with _registry_lock:
|
|
disabled_snapshot = _disabled_tools.copy()
|
|
for entry in _registry:
|
|
op_id = entry["operation_id"]
|
|
orig_id = entry.get("original_operation_id")
|
|
is_disabled = bool(op_id in disabled_snapshot or (orig_id and orig_id in disabled_snapshot))
|
|
tools.append({
|
|
"operation_id": op_id,
|
|
"summary": entry["summary"],
|
|
"disabled": is_disabled
|
|
})
|
|
return tools
|
|
|
|
|
|
def register_tool(
|
|
path: str,
|
|
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
operation_id: str,
|
|
summary: str,
|
|
description: str,
|
|
request_model: Optional[Type[BaseModel]] = None,
|
|
response_model: Optional[Type[BaseModel]] = None,
|
|
path_params: Optional[List[Dict[str, Any]]] = None,
|
|
query_params: Optional[List[Dict[str, Any]]] = None,
|
|
tags: Optional[List[str]] = None,
|
|
deprecated: bool = False,
|
|
original_operation_id: Optional[str] = None,
|
|
allow_multipart_payload: bool = False
|
|
) -> None:
|
|
"""
|
|
Register an API endpoint for OpenAPI spec generation.
|
|
|
|
Args:
|
|
path: URL path (e.g., "/devices/{mac}")
|
|
method: HTTP method
|
|
operation_id: Unique identifier for this operation (MUST be unique across entire spec)
|
|
summary: Short summary for the operation
|
|
description: Detailed description
|
|
request_model: Pydantic model for request body (POST/PUT/PATCH)
|
|
response_model: Pydantic model for success response
|
|
path_params: List of path parameter definitions
|
|
query_params: List of query parameter definitions
|
|
tags: OpenAPI tags for grouping
|
|
deprecated: Whether this endpoint is deprecated
|
|
original_operation_id: The base ID before suffixing (for disablement mapping)
|
|
allow_multipart_payload: Whether to allow multipart/form-data payloads
|
|
|
|
Raises:
|
|
DuplicateOperationIdError: If operation_id already exists in registry
|
|
"""
|
|
with _registry_lock:
|
|
if operation_id in _operation_ids:
|
|
raise DuplicateOperationIdError(
|
|
f"operationId '{operation_id}' is already registered. "
|
|
"Each operationId must be unique across the entire API."
|
|
)
|
|
_operation_ids.add(operation_id)
|
|
|
|
_registry.append({
|
|
"path": path,
|
|
"method": method.upper(),
|
|
"operation_id": operation_id,
|
|
"original_operation_id": original_operation_id,
|
|
"summary": summary,
|
|
"description": description,
|
|
"request_model": request_model,
|
|
"response_model": response_model,
|
|
"path_params": path_params or [],
|
|
"query_params": query_params or [],
|
|
"tags": tags or ["default"],
|
|
"deprecated": deprecated,
|
|
"allow_multipart_payload": allow_multipart_payload
|
|
})
|
|
|
|
|
|
def clear_registry() -> None:
|
|
"""Clear all registered endpoints (useful for testing)."""
|
|
with _registry_lock:
|
|
_registry.clear()
|
|
_operation_ids.clear()
|
|
_disabled_tools.clear()
|
|
|
|
|
|
def get_registry() -> List[Dict[str, Any]]:
|
|
"""Get a deep copy of the current registry to prevent external mutation."""
|
|
with _registry_lock:
|
|
return deepcopy(_registry)
|