231 lines
5.6 KiB
Python
231 lines
5.6 KiB
Python
"""Pydantic request/response schemas shared across routers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Annotated, Any, Literal
|
|
|
|
from pydantic import BaseModel, Field, StringConstraints
|
|
|
|
from app.models import ManagementRole
|
|
|
|
UsernameStr = Annotated[str, StringConstraints(pattern=r"^[a-zA-Z0-9_-]{1,100}$")]
|
|
TopicFilterStr = Annotated[str, StringConstraints(min_length=1, max_length=200)]
|
|
|
|
ACL_TYPES = Literal[
|
|
"publishClientSend",
|
|
"publishClientReceive",
|
|
"subscribeLiteral",
|
|
"subscribePattern",
|
|
"unsubscribeLiteral",
|
|
"unsubscribePattern",
|
|
]
|
|
|
|
|
|
# --- Auth ------------------------------------------------------------------
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class SessionInfo(BaseModel):
|
|
username: str
|
|
role: ManagementRole
|
|
must_change_password: bool
|
|
|
|
|
|
class PasswordChangeRequest(BaseModel):
|
|
current_password: str
|
|
new_password: str = Field(min_length=12)
|
|
|
|
|
|
# --- Management users -------------------------------------------------------
|
|
|
|
|
|
class ManagementUserOut(BaseModel):
|
|
id: uuid.UUID
|
|
username: str
|
|
display_name: str | None
|
|
role: ManagementRole
|
|
enabled: bool
|
|
must_change_password: bool
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class ManagementUserCreate(BaseModel):
|
|
username: UsernameStr
|
|
password: str = Field(min_length=12)
|
|
display_name: str | None = None
|
|
role: ManagementRole = ManagementRole.OPERATOR
|
|
|
|
|
|
class ManagementUserUpdate(BaseModel):
|
|
display_name: str | None = None
|
|
role: ManagementRole | None = None
|
|
enabled: bool | None = None
|
|
|
|
|
|
# --- MQTT clients ------------------------------------------------------------
|
|
|
|
|
|
class AclRuleIn(BaseModel):
|
|
"""One ACL rule as edited in the UI (spec section 14)."""
|
|
|
|
acltype: ACL_TYPES
|
|
topic: TopicFilterStr
|
|
allow: bool = True
|
|
priority: int = Field(default=0, ge=0, le=100000)
|
|
|
|
|
|
class RoleIn(BaseModel):
|
|
rolename: UsernameStr
|
|
text_name: str | None = None
|
|
text_description: str | None = None
|
|
acls: list[AclRuleIn] = Field(default_factory=list)
|
|
|
|
|
|
class RoleUpdate(BaseModel):
|
|
"""PUT /roles/{name} payload: rolename comes from the URL path."""
|
|
|
|
acls: list[AclRuleIn]
|
|
|
|
|
|
class GroupRoleIn(BaseModel):
|
|
rolename: str
|
|
priority: int = 1
|
|
|
|
|
|
class GroupIn(BaseModel):
|
|
groupname: UsernameStr
|
|
text_name: str | None = None
|
|
text_description: str | None = None
|
|
roles: list[GroupRoleIn] = Field(default_factory=list)
|
|
|
|
|
|
class ClientRoleIn(BaseModel):
|
|
rolename: str
|
|
priority: int = 1
|
|
|
|
|
|
class ClientGroupIn(BaseModel):
|
|
groupname: str
|
|
priority: int = 1
|
|
|
|
|
|
class ClientRoleAssign(BaseModel):
|
|
"""POST /clients/{username}/roles body — add one role incrementally."""
|
|
|
|
rolename: str = Field(min_length=1)
|
|
priority: int = Field(default=-1, ge=-1, le=100000)
|
|
|
|
|
|
class GroupRoleAssign(BaseModel):
|
|
"""POST /groups/{groupname}/roles body — add one role incrementally."""
|
|
|
|
rolename: str = Field(min_length=1)
|
|
priority: int = Field(default=-1, ge=-1, le=100000)
|
|
|
|
|
|
class GroupMemberAssign(BaseModel):
|
|
"""POST /groups/{groupname}/clients body — add one client to a group."""
|
|
|
|
username: str = Field(min_length=1)
|
|
priority: int = Field(default=-1, ge=-1, le=100000)
|
|
|
|
|
|
class MqttClientCreate(BaseModel):
|
|
username: UsernameStr
|
|
password: str | None = Field(
|
|
default=None,
|
|
description="If omitted a random password is generated and returned once.",
|
|
)
|
|
clientid: str | None = None
|
|
text_name: str | None = None
|
|
text_description: str | None = None
|
|
groups: list[ClientGroupIn] = Field(default_factory=list)
|
|
roles: list[ClientRoleIn] = Field(default_factory=list)
|
|
# PostgreSQL-side metadata (spec section 11)
|
|
owner: str | None = None
|
|
purpose: str | None = None
|
|
tags: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class MqttClientOut(BaseModel):
|
|
username: str
|
|
clientid: str | None
|
|
text_name: str | None
|
|
text_description: str | None
|
|
enabled: bool
|
|
groups: list[dict[str, Any]]
|
|
roles: list[dict[str, Any]]
|
|
owner: str | None
|
|
purpose: str | None
|
|
tags: list[str]
|
|
last_known_connection: datetime | None
|
|
|
|
|
|
class MqttPasswordReset(BaseModel):
|
|
"""Response for create/reset — plaintext shown exactly once (spec 11)."""
|
|
|
|
username: str
|
|
password: str
|
|
|
|
|
|
# --- Broker configuration ----------------------------------------------------
|
|
|
|
|
|
class BrokerSettingsIn(BaseModel):
|
|
key: str = Field(min_length=1, max_length=100)
|
|
value: str
|
|
value_type: Literal["string", "int", "bool"] = "string"
|
|
|
|
|
|
class ListenerIn(BaseModel):
|
|
name: str = Field(min_length=1, max_length=100)
|
|
enabled: bool = True
|
|
port: int = Field(ge=1, le=65535)
|
|
bind_address: str | None = None
|
|
protocol: Literal["mqtt", "websockets"] = "mqtt"
|
|
allow_anonymous: bool = False
|
|
max_connections: int = -1
|
|
tls_enabled: bool = False
|
|
tls_min_version: Literal["tlsv1.2", "tlsv1.3"] | None = None
|
|
require_certificate: bool = False
|
|
use_identity_as_username: bool = False
|
|
|
|
|
|
class ListenerOut(ListenerIn):
|
|
id: uuid.UUID
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class ConfigPreviewOut(BaseModel):
|
|
config_text: str
|
|
warnings: list[str]
|
|
|
|
|
|
class DeployRequest(BaseModel):
|
|
comment: str | None = None
|
|
|
|
|
|
class RevisionOut(BaseModel):
|
|
id: uuid.UUID
|
|
revision_number: int
|
|
created_by: str | None
|
|
comment: str | None
|
|
deployed: bool
|
|
deployed_at: datetime | None
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class RevisionDetailOut(RevisionOut):
|
|
config_text: str
|
|
validation_result: dict[str, Any] |