Files
mqtt-broker-stack/backend/app/schemas.py
T
2026-09-16 06:08:37 +02:00

210 lines
5.0 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 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]