"""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]