mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-01-22 05:38:32 -05:00
759 lines
27 KiB
Python
759 lines
27 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
NetAlertX API Schema Definitions (Pydantic v2)
|
|
|
|
This module defines strict Pydantic models for all API request and response payloads.
|
|
These schemas serve as the single source of truth for:
|
|
1. Runtime validation of incoming requests
|
|
2. OpenAPI specification generation
|
|
3. MCP tool input schema derivation
|
|
|
|
Philosophy: "Code First, Spec Second" — these models ARE the contract.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import ipaddress
|
|
from typing import Optional, List, Literal, Any, Dict
|
|
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict, RootModel
|
|
|
|
# Internal helper imports
|
|
from helper import sanitize_string
|
|
from plugin_helper import normalize_mac, is_mac
|
|
|
|
|
|
# =============================================================================
|
|
# COMMON PATTERNS & VALIDATORS
|
|
# =============================================================================
|
|
|
|
MAC_PATTERN = r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
|
|
IP_PATTERN = r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
|
COLUMN_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_]+$")
|
|
|
|
# Security whitelists & Literals for documentation
|
|
ALLOWED_DEVICE_COLUMNS = Literal[
|
|
"devName", "devOwner", "devType", "devVendor",
|
|
"devGroup", "devLocation", "devComments", "devFavorite",
|
|
"devParentMAC"
|
|
]
|
|
|
|
ALLOWED_NMAP_MODES = Literal[
|
|
"quick", "intense", "ping", "comprehensive", "fast", "normal", "detail", "skipdiscovery",
|
|
"-sS", "-sT", "-sU", "-sV", "-O"
|
|
]
|
|
|
|
NOTIFICATION_LEVELS = Literal["info", "warning", "error", "alert"]
|
|
|
|
ALLOWED_TABLES = Literal["Devices", "Events", "Sessions", "Settings", "CurrentScan", "Online_History", "Plugins_Objects"]
|
|
|
|
ALLOWED_LOG_FILES = Literal[
|
|
"app.log", "app_front.log", "IP_changes.log", "stdout.log", "stderr.log",
|
|
"app.php_errors.log", "execution_queue.log", "db_is_locked.log"
|
|
]
|
|
|
|
|
|
def validate_mac(value: str) -> str:
|
|
"""Validate and normalize MAC address format."""
|
|
# Allow "Internet" as a special case for the gateway/WAN device
|
|
if value.lower() == "internet":
|
|
return "Internet"
|
|
|
|
if not is_mac(value):
|
|
raise ValueError(f"Invalid MAC address format: {value}")
|
|
|
|
return normalize_mac(value)
|
|
|
|
|
|
def validate_ip(value: str) -> str:
|
|
"""Validate IP address format (IPv4 or IPv6) using stdlib ipaddress.
|
|
|
|
Returns the canonical string form of the IP address.
|
|
"""
|
|
try:
|
|
return str(ipaddress.ip_address(value))
|
|
except ValueError as err:
|
|
raise ValueError(f"Invalid IP address: {value}") from err
|
|
|
|
|
|
def validate_column_identifier(value: str) -> str:
|
|
"""Validate a column identifier to prevent SQL injection."""
|
|
if not COLUMN_NAME_PATTERN.match(value):
|
|
raise ValueError("Invalid column name format")
|
|
return value
|
|
|
|
|
|
# =============================================================================
|
|
# BASE RESPONSE MODELS
|
|
# =============================================================================
|
|
|
|
|
|
class BaseResponse(BaseModel):
|
|
"""Standard API response wrapper."""
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
success: bool = Field(..., description="Whether the operation succeeded")
|
|
message: Optional[str] = Field(None, description="Human-readable message")
|
|
error: Optional[str] = Field(None, description="Error message if success=False")
|
|
|
|
|
|
class PaginatedResponse(BaseResponse):
|
|
"""Response with pagination metadata."""
|
|
total: int = Field(0, description="Total number of items")
|
|
page: int = Field(1, ge=1, description="Current page number")
|
|
per_page: int = Field(50, ge=1, le=500, description="Items per page")
|
|
|
|
|
|
# =============================================================================
|
|
# DEVICE SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class DeviceSearchRequest(BaseModel):
|
|
"""Request payload for searching devices."""
|
|
model_config = ConfigDict(str_strip_whitespace=True)
|
|
|
|
query: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=256,
|
|
description="Search term: IP address, MAC address, device name, or vendor",
|
|
json_schema_extra={"examples": ["192.168.1.1", "Apple", "00:11:22:33:44:55"]}
|
|
)
|
|
limit: int = Field(
|
|
50,
|
|
ge=1,
|
|
le=500,
|
|
description="Maximum number of results to return"
|
|
)
|
|
|
|
|
|
class DeviceInfo(BaseModel):
|
|
"""Detailed device information model (Raw record)."""
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
devMac: str = Field(..., description="Device MAC address")
|
|
devName: Optional[str] = Field(None, description="Device display name/alias")
|
|
devLastIP: Optional[str] = Field(None, description="Last known IP address")
|
|
devPrimaryIPv4: Optional[str] = Field(None, description="Primary IPv4 address")
|
|
devPrimaryIPv6: Optional[str] = Field(None, description="Primary IPv6 address")
|
|
devVlan: Optional[str] = Field(None, description="VLAN identifier")
|
|
devForceStatus: Optional[str] = Field(None, description="Force device status (online/offline/dont_force)")
|
|
devVendor: Optional[str] = Field(None, description="Hardware vendor from OUI lookup")
|
|
devOwner: Optional[str] = Field(None, description="Device owner")
|
|
devType: Optional[str] = Field(None, description="Device type classification")
|
|
devFavorite: Optional[int] = Field(0, description="Favorite flag (0 or 1)")
|
|
devPresentLastScan: Optional[int] = Field(None, description="Present in last scan (0 or 1)")
|
|
devStatus: Optional[str] = Field(None, description="Online/Offline status")
|
|
devMacSource: Optional[str] = Field(None, description="Source of devMac (USER, LOCKED, or plugin prefix)")
|
|
devNameSource: Optional[str] = Field(None, description="Source of devName")
|
|
devFqdnSource: Optional[str] = Field(None, description="Source of devFQDN")
|
|
devLastIpSource: Optional[str] = Field(None, description="Source of devLastIP")
|
|
devVendorSource: Optional[str] = Field(None, description="Source of devVendor")
|
|
devSsidSource: Optional[str] = Field(None, description="Source of devSSID")
|
|
devParentMacSource: Optional[str] = Field(None, description="Source of devParentMAC")
|
|
devParentPortSource: Optional[str] = Field(None, description="Source of devParentPort")
|
|
devParentRelTypeSource: Optional[str] = Field(None, description="Source of devParentRelType")
|
|
devVlanSource: Optional[str] = Field(None, description="Source of devVlan")
|
|
|
|
|
|
class DeviceSearchResponse(BaseResponse):
|
|
"""Response payload for device search."""
|
|
devices: List[DeviceInfo] = Field(default_factory=list, description="List of matching devices")
|
|
|
|
|
|
class DeviceListRequest(BaseModel):
|
|
"""Request for listing devices by status."""
|
|
status: Optional[Literal[
|
|
"connected", "down", "favorites", "new", "archived", "all", "my",
|
|
"offline"
|
|
]] = Field(
|
|
None,
|
|
description="Filter devices by status (connected, down, favorites, new, archived, all, my, offline)"
|
|
)
|
|
|
|
|
|
class DeviceListResponse(RootModel):
|
|
"""Response with list of devices."""
|
|
root: List[DeviceInfo] = Field(default_factory=list, description="List of devices")
|
|
|
|
|
|
class DeviceListWrapperResponse(BaseResponse):
|
|
"""Wrapped response with list of devices."""
|
|
devices: List[DeviceInfo] = Field(default_factory=list, description="List of devices")
|
|
|
|
|
|
class GetDeviceRequest(BaseModel):
|
|
"""Path parameter for getting a specific device."""
|
|
mac: str = Field(
|
|
...,
|
|
description="Device MAC address",
|
|
json_schema_extra={"examples": ["00:11:22:33:44:55"]}
|
|
)
|
|
|
|
@field_validator("mac")
|
|
@classmethod
|
|
def validate_mac_address(cls, v: str) -> str:
|
|
return validate_mac(v)
|
|
|
|
|
|
class GetDeviceResponse(BaseResponse):
|
|
"""Wrapped response for getting device details."""
|
|
device: Optional[DeviceInfo] = Field(None, description="Device details if found")
|
|
|
|
|
|
class GetDeviceWrapperResponse(BaseResponse):
|
|
"""Wrapped response for getting a single device (e.g. latest)."""
|
|
device: Optional[DeviceInfo] = Field(None, description="Device details")
|
|
|
|
|
|
class SetDeviceAliasRequest(BaseModel):
|
|
"""Request to set a device alias/name."""
|
|
alias: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=128,
|
|
description="New display name/alias for the device"
|
|
)
|
|
|
|
@field_validator("alias")
|
|
@classmethod
|
|
def sanitize_alias(cls, v: str) -> str:
|
|
return sanitize_string(v)
|
|
|
|
|
|
class DeviceTotalsResponse(RootModel):
|
|
"""Response with device statistics."""
|
|
root: List[int] = Field(default_factory=list, description="List of counts: [all, online, favorites, new, offline, archived]")
|
|
|
|
|
|
class DeviceExportRequest(BaseModel):
|
|
"""Request for exporting devices."""
|
|
format: Literal["csv", "json"] = Field(
|
|
"csv",
|
|
description="Export format: csv or json"
|
|
)
|
|
|
|
|
|
class DeviceExportResponse(BaseModel):
|
|
"""Raw response for device export in JSON format."""
|
|
columns: List[str] = Field(..., description="Column names")
|
|
data: List[Dict[str, Any]] = Field(..., description="Device records")
|
|
|
|
|
|
class DeviceImportRequest(BaseModel):
|
|
"""Request for importing devices."""
|
|
content: Optional[str] = Field(
|
|
None,
|
|
description="Base64-encoded CSV or JSON content to import"
|
|
)
|
|
|
|
|
|
class DeviceImportResponse(BaseResponse):
|
|
"""Response for device import operation."""
|
|
imported: int = Field(0, description="Number of devices imported")
|
|
skipped: int = Field(0, description="Number of devices skipped")
|
|
errors: List[str] = Field(default_factory=list, description="List of import errors")
|
|
|
|
|
|
class CopyDeviceRequest(BaseModel):
|
|
"""Request to copy device settings."""
|
|
macFrom: str = Field(..., description="Source MAC address")
|
|
macTo: str = Field(..., description="Destination MAC address")
|
|
|
|
@field_validator("macFrom", "macTo")
|
|
@classmethod
|
|
def validate_mac_addresses(cls, v: str) -> str:
|
|
return validate_mac(v)
|
|
|
|
|
|
class UpdateDeviceColumnRequest(BaseModel):
|
|
"""Request to update a specific device database column."""
|
|
columnName: ALLOWED_DEVICE_COLUMNS = Field(..., description="Database column name")
|
|
columnValue: Any = Field(..., description="New value for the column")
|
|
|
|
|
|
class LockDeviceFieldRequest(BaseModel):
|
|
"""Request to lock/unlock a device field."""
|
|
fieldName: Optional[str] = Field(None, description="Field name to lock/unlock (devMac, devName, devLastIP, etc.)")
|
|
lock: bool = Field(True, description="True to lock the field, False to unlock")
|
|
|
|
|
|
class DeviceUpdateRequest(BaseModel):
|
|
"""Request to update device fields (create/update)."""
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
devName: Optional[str] = Field(None, description="Device name")
|
|
devOwner: Optional[str] = Field(None, description="Device owner")
|
|
devType: Optional[str] = Field(None, description="Device type")
|
|
devVendor: Optional[str] = Field(None, description="Device vendor")
|
|
devGroup: Optional[str] = Field(None, description="Device group")
|
|
devLocation: Optional[str] = Field(None, description="Device location")
|
|
devComments: Optional[str] = Field(None, description="Comments")
|
|
createNew: bool = Field(False, description="Create new device if not exists")
|
|
|
|
@field_validator("devName", "devOwner", "devType", "devVendor", "devGroup", "devLocation", "devComments")
|
|
@classmethod
|
|
def sanitize_text_fields(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is None:
|
|
return v
|
|
return sanitize_string(v)
|
|
|
|
|
|
class DeleteDevicesRequest(BaseModel):
|
|
"""Request to delete multiple devices."""
|
|
macs: List[str] = Field([], description="List of MACs to delete")
|
|
confirm_delete_all: bool = Field(False, description="Explicit flag to delete ALL devices when macs is empty")
|
|
|
|
@field_validator("macs")
|
|
@classmethod
|
|
def validate_mac_list(cls, v: List[str]) -> List[str]:
|
|
return [validate_mac(mac) for mac in v]
|
|
|
|
@model_validator(mode="after")
|
|
def check_delete_all_safety(self) -> DeleteDevicesRequest:
|
|
if not self.macs and not self.confirm_delete_all:
|
|
raise ValueError("Must provide at least one MAC or set confirm_delete_all=True")
|
|
return self
|
|
|
|
|
|
# =============================================================================
|
|
# NETWORK TOOLS SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class TriggerScanRequest(BaseModel):
|
|
"""Request to trigger a network scan."""
|
|
type: str = Field(
|
|
"ARPSCAN",
|
|
description="Scan plugin type to execute (e.g., ARPSCAN, NMAPDEV, NMAP)",
|
|
json_schema_extra={"examples": ["ARPSCAN", "NMAPDEV", "NMAP"]}
|
|
)
|
|
|
|
|
|
class TriggerScanResponse(BaseResponse):
|
|
"""Response for scan trigger."""
|
|
scan_type: Optional[str] = Field(None, description="Type of scan that was triggered")
|
|
|
|
|
|
class OpenPortsRequest(BaseModel):
|
|
"""Request for getting open ports."""
|
|
target: str = Field(
|
|
...,
|
|
description="Target IP address or MAC address to check ports for",
|
|
json_schema_extra={"examples": ["192.168.1.50", "00:11:22:33:44:55"]}
|
|
)
|
|
|
|
@field_validator("target")
|
|
@classmethod
|
|
def validate_target(cls, v: str) -> str:
|
|
"""Validate target is either a valid IP or MAC address."""
|
|
# Try IP first
|
|
try:
|
|
return validate_ip(v)
|
|
except ValueError:
|
|
pass
|
|
# Try MAC
|
|
return validate_mac(v)
|
|
|
|
|
|
class OpenPortsResponse(BaseResponse):
|
|
"""Response with open ports information."""
|
|
target: str = Field(..., description="Target that was scanned")
|
|
open_ports: List[Any] = Field(default_factory=list, description="List of open port objects or numbers")
|
|
|
|
|
|
class WakeOnLanRequest(BaseModel):
|
|
"""Request to send Wake-on-LAN packet."""
|
|
devMac: Optional[str] = Field(
|
|
None,
|
|
description="Target device MAC address",
|
|
json_schema_extra={"examples": ["00:11:22:33:44:55"]}
|
|
)
|
|
devLastIP: Optional[str] = Field(
|
|
None,
|
|
alias="ip",
|
|
description="Target device IP (MAC will be resolved if not provided)",
|
|
json_schema_extra={"examples": ["192.168.1.50"]}
|
|
)
|
|
# Note: alias="ip" means input JSON can use "ip".
|
|
# But Pydantic V2 with populate_by_name=True allows both "devLastIP" and "ip".
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
@field_validator("devMac")
|
|
@classmethod
|
|
def validate_mac_if_provided(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is not None:
|
|
return validate_mac(v)
|
|
return v
|
|
|
|
@field_validator("devLastIP")
|
|
@classmethod
|
|
def validate_ip_if_provided(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is not None:
|
|
return validate_ip(v)
|
|
return v
|
|
|
|
@model_validator(mode="after")
|
|
def require_mac_or_ip(self) -> "WakeOnLanRequest":
|
|
"""Ensure at least one of devMac or devLastIP is provided."""
|
|
if self.devMac is None and self.devLastIP is None:
|
|
raise ValueError("Either 'devMac' or 'devLastIP' (alias 'ip') must be provided")
|
|
return self
|
|
|
|
|
|
class WakeOnLanResponse(BaseResponse):
|
|
"""Response for Wake-on-LAN operation."""
|
|
output: Optional[str] = Field(None, description="Command output")
|
|
|
|
|
|
class TracerouteRequest(BaseModel):
|
|
"""Request to perform traceroute."""
|
|
devLastIP: str = Field(
|
|
...,
|
|
description="Target IP address for traceroute",
|
|
json_schema_extra={"examples": ["8.8.8.8", "192.168.1.1"]}
|
|
)
|
|
|
|
@field_validator("devLastIP")
|
|
@classmethod
|
|
def validate_ip_address(cls, v: str) -> str:
|
|
return validate_ip(v)
|
|
|
|
|
|
class TracerouteResponse(BaseResponse):
|
|
"""Response with traceroute results."""
|
|
output: List[str] = Field(default_factory=list, description="Traceroute hop output lines")
|
|
|
|
|
|
class NmapScanRequest(BaseModel):
|
|
"""Request to perform NMAP scan."""
|
|
scan: str = Field(
|
|
...,
|
|
description="Target IP address for NMAP scan"
|
|
)
|
|
mode: ALLOWED_NMAP_MODES = Field(
|
|
...,
|
|
description="NMAP scan mode/arguments (restricted to safe options)"
|
|
)
|
|
|
|
@field_validator("scan")
|
|
@classmethod
|
|
def validate_scan_target(cls, v: str) -> str:
|
|
return validate_ip(v)
|
|
|
|
|
|
class NslookupRequest(BaseModel):
|
|
"""Request for DNS lookup."""
|
|
devLastIP: str = Field(
|
|
...,
|
|
description="IP address to perform reverse DNS lookup"
|
|
)
|
|
|
|
@field_validator("devLastIP")
|
|
@classmethod
|
|
def validate_ip_address(cls, v: str) -> str:
|
|
return validate_ip(v)
|
|
|
|
|
|
class NslookupResponse(BaseResponse):
|
|
"""Response for DNS lookup operation."""
|
|
output: List[str] = Field(default_factory=list, description="Nslookup output lines")
|
|
|
|
|
|
class NmapScanResponse(BaseResponse):
|
|
"""Response for NMAP scan operation."""
|
|
mode: Optional[str] = Field(None, description="NMAP scan mode")
|
|
ip: Optional[str] = Field(None, description="Target IP address")
|
|
output: List[str] = Field(default_factory=list, description="NMAP scan output lines")
|
|
|
|
|
|
class NetworkTopologyResponse(BaseResponse):
|
|
"""Response with network topology data."""
|
|
nodes: List[dict] = Field(default_factory=list, description="Network nodes")
|
|
links: List[dict] = Field(default_factory=list, description="Network connections")
|
|
|
|
|
|
class InternetInfoResponse(BaseResponse):
|
|
"""Response for internet information."""
|
|
output: Dict[str, Any] = Field(..., description="Details about the internet connection.")
|
|
|
|
|
|
class NetworkInterfacesResponse(BaseResponse):
|
|
"""Response with network interface information."""
|
|
interfaces: Dict[str, Any] = Field(..., description="Details about network interfaces.")
|
|
|
|
|
|
# =============================================================================
|
|
# EVENTS SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class EventInfo(BaseModel):
|
|
"""Event/alert information."""
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
eveRowid: Optional[int] = Field(None, description="Event row ID")
|
|
eveMAC: Optional[str] = Field(None, description="Device MAC address")
|
|
eveIP: Optional[str] = Field(None, description="Device IP address")
|
|
eveDateTime: Optional[str] = Field(None, description="Event timestamp")
|
|
eveEventType: Optional[str] = Field(None, description="Type of event")
|
|
evePreviousIP: Optional[str] = Field(None, description="Previous IP if changed")
|
|
|
|
|
|
class RecentEventsRequest(BaseModel):
|
|
"""Request for recent events."""
|
|
hours: int = Field(
|
|
24,
|
|
ge=1,
|
|
le=720,
|
|
description="Number of hours to look back for events"
|
|
)
|
|
limit: int = Field(
|
|
100,
|
|
ge=1,
|
|
le=1000,
|
|
description="Maximum number of events to return"
|
|
)
|
|
|
|
|
|
class RecentEventsResponse(BaseResponse):
|
|
"""Response with recent events."""
|
|
hours: int = Field(..., description="The time window in hours")
|
|
events: List[EventInfo] = Field(default_factory=list, description="List of recent events")
|
|
|
|
|
|
class LastEventsResponse(BaseResponse):
|
|
"""Response with last N events."""
|
|
events: List[EventInfo] = Field(default_factory=list, description="List of last events")
|
|
|
|
|
|
class CreateEventRequest(BaseModel):
|
|
"""Request to create a device event."""
|
|
ip: Optional[str] = Field("0.0.0.0", description="Device IP")
|
|
event_type: str = Field("Device Down", description="Event type")
|
|
additional_info: Optional[str] = Field("", description="Additional info")
|
|
pending_alert: int = Field(1, description="Pending alert flag")
|
|
event_time: Optional[str] = Field(None, description="Event timestamp (ISO)")
|
|
|
|
@field_validator("ip", mode="before")
|
|
@classmethod
|
|
def validate_ip_field(cls, v: Optional[str]) -> str:
|
|
"""Validate and normalize IP address, defaulting to 0.0.0.0."""
|
|
if v is None or v == "":
|
|
return "0.0.0.0"
|
|
return validate_ip(v)
|
|
|
|
|
|
# =============================================================================
|
|
# SESSIONS SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class SessionInfo(BaseModel):
|
|
"""Session information."""
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
sesRowid: Optional[int] = Field(None, description="Session row ID")
|
|
sesMac: Optional[str] = Field(None, description="Device MAC address")
|
|
sesDateTimeConnection: Optional[str] = Field(None, description="Connection timestamp")
|
|
sesDateTimeDisconnection: Optional[str] = Field(None, description="Disconnection timestamp")
|
|
sesIPAddress: Optional[str] = Field(None, description="IP address during session")
|
|
|
|
|
|
class CreateSessionRequest(BaseModel):
|
|
"""Request to create a session."""
|
|
mac: str = Field(..., description="Device MAC")
|
|
ip: str = Field(..., description="Device IP")
|
|
start_time: str = Field(..., description="Start time")
|
|
end_time: Optional[str] = Field(None, description="End time")
|
|
event_type_conn: str = Field("Connected", description="Connection event type")
|
|
event_type_disc: str = Field("Disconnected", description="Disconnection event type")
|
|
|
|
@field_validator("mac")
|
|
@classmethod
|
|
def validate_mac_address(cls, v: str) -> str:
|
|
return validate_mac(v)
|
|
|
|
@field_validator("ip")
|
|
@classmethod
|
|
def validate_ip_address(cls, v: str) -> str:
|
|
return validate_ip(v)
|
|
|
|
|
|
class DeleteSessionRequest(BaseModel):
|
|
"""Request to delete sessions for a MAC."""
|
|
mac: str = Field(..., description="Device MAC")
|
|
|
|
@field_validator("mac")
|
|
@classmethod
|
|
def validate_mac_address(cls, v: str) -> str:
|
|
return validate_mac(v)
|
|
|
|
|
|
# =============================================================================
|
|
# MESSAGING / IN-APP NOTIFICATIONS SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class InAppNotification(BaseModel):
|
|
"""In-app notification model."""
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
id: Optional[int] = Field(None, description="Notification ID")
|
|
guid: Optional[str] = Field(None, description="Unique notification GUID")
|
|
text: str = Field(..., description="Notification text content")
|
|
level: NOTIFICATION_LEVELS = Field("info", description="Notification level")
|
|
read: Optional[int] = Field(0, description="Read status (0 or 1)")
|
|
created_at: Optional[str] = Field(None, description="Creation timestamp")
|
|
|
|
|
|
class CreateNotificationRequest(BaseModel):
|
|
"""Request to create an in-app notification."""
|
|
content: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=1024,
|
|
description="Notification content"
|
|
)
|
|
level: NOTIFICATION_LEVELS = Field(
|
|
"info",
|
|
description="Notification severity level"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# SYNC SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class SyncPushRequest(BaseModel):
|
|
"""Request to push data to sync."""
|
|
data: dict = Field(..., description="Data to sync")
|
|
node_name: str = Field(..., description="Name of the node sending data")
|
|
plugin: str = Field(..., description="Plugin identifier")
|
|
|
|
|
|
class SyncPullResponse(BaseResponse):
|
|
"""Response with sync data."""
|
|
data: Optional[dict] = Field(None, description="Synchronized data")
|
|
last_sync: Optional[str] = Field(None, description="Last sync timestamp")
|
|
|
|
|
|
# =============================================================================
|
|
# DB QUERY SCHEMAS (Raw SQL)
|
|
# =============================================================================
|
|
|
|
|
|
class DbQueryRequest(BaseModel):
|
|
"""
|
|
Request for raw database query.
|
|
WARNING: This is a highly privileged operation.
|
|
"""
|
|
rawSql: str = Field(
|
|
...,
|
|
description="Base64-encoded SQL query. (UNSAFE: Use only for administrative tasks)"
|
|
)
|
|
# Legacy compatibility: removed strict safety check
|
|
# TODO: SECURITY CRITICAL - Re-enable strict safety checks.
|
|
# The `confirm_dangerous_query` default was relaxed to `True` to maintain backward compatibility
|
|
# with the legacy frontend which sends raw SQL directly.
|
|
#
|
|
# CONTEXT: This explicit safety check was introduced with the new Pydantic validation layer.
|
|
# The legacy PHP frontend predates these formal schemas and does not send the
|
|
# `confirm_dangerous_query` flag, causing 422 Validation Errors when this check is enforced.
|
|
#
|
|
# Actionable Advice:
|
|
# 1. Implement a parser to strictly whitelist only `SELECT` statements if raw SQL is required.
|
|
# 2. Migrate the frontend to use structured endpoints (e.g., `/devices/search`, `/dbquery/read`) instead of raw SQL.
|
|
# 3. Once migrated, revert `confirm_dangerous_query` default to `False` and enforce the check.
|
|
confirm_dangerous_query: bool = Field(
|
|
True,
|
|
description="Required to be True to acknowledge the risks of raw SQL execution"
|
|
)
|
|
|
|
|
|
class DbQueryUpdateRequest(BaseModel):
|
|
"""Request for DB update query."""
|
|
columnName: str = Field(..., description="Column to filter by")
|
|
id: List[Any] = Field(..., description="List of IDs to update")
|
|
dbtable: ALLOWED_TABLES = Field(..., description="Table name")
|
|
columns: List[str] = Field(..., description="Columns to update")
|
|
values: List[Any] = Field(..., description="New values")
|
|
|
|
@field_validator("columnName")
|
|
@classmethod
|
|
def validate_column_name(cls, v: str) -> str:
|
|
return validate_column_identifier(v)
|
|
|
|
@field_validator("columns")
|
|
@classmethod
|
|
def validate_column_list(cls, values: List[str]) -> List[str]:
|
|
return [validate_column_identifier(value) for value in values]
|
|
|
|
@model_validator(mode="after")
|
|
def validate_columns_values(self) -> "DbQueryUpdateRequest":
|
|
if len(self.columns) != len(self.values):
|
|
raise ValueError("columns and values must have the same length")
|
|
return self
|
|
|
|
|
|
class DbQueryDeleteRequest(BaseModel):
|
|
"""Request for DB delete query."""
|
|
columnName: str = Field(..., description="Column to filter by")
|
|
id: List[Any] = Field(..., description="List of IDs to delete")
|
|
dbtable: ALLOWED_TABLES = Field(..., description="Table name")
|
|
|
|
@field_validator("columnName")
|
|
@classmethod
|
|
def validate_column_name(cls, v: str) -> str:
|
|
return validate_column_identifier(v)
|
|
|
|
|
|
class DbQueryResponse(BaseResponse):
|
|
"""Response from database query."""
|
|
data: Any = Field(None, description="Query result data")
|
|
columns: Optional[List[str]] = Field(None, description="Column names if applicable")
|
|
|
|
|
|
# =============================================================================
|
|
# LOGS SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class CleanLogRequest(BaseModel):
|
|
"""Request to clean/truncate a log file."""
|
|
logFile: ALLOWED_LOG_FILES = Field(
|
|
...,
|
|
description="Name of the log file to clean"
|
|
)
|
|
|
|
|
|
class LogResource(BaseModel):
|
|
"""Log file resource information."""
|
|
name: str = Field(..., description="Log file name")
|
|
path: str = Field(..., description="Full path to log file")
|
|
size_bytes: int = Field(0, description="File size in bytes")
|
|
modified: Optional[str] = Field(None, description="Last modification timestamp")
|
|
|
|
|
|
class AddToQueueRequest(BaseModel):
|
|
"""Request to add action to execution queue."""
|
|
action: str = Field(..., description="Action string (e.g. update_api|devices)")
|
|
|
|
|
|
# =============================================================================
|
|
# SETTINGS SCHEMAS
|
|
# =============================================================================
|
|
|
|
|
|
class SettingValue(BaseModel):
|
|
"""A single setting value."""
|
|
key: str = Field(..., description="Setting key name")
|
|
value: Any = Field(..., description="Setting value")
|
|
|
|
|
|
class GetSettingResponse(BaseResponse):
|
|
"""Response for getting a setting value."""
|
|
value: Any = Field(None, description="The setting value")
|