patristic-be / src /api /dto /admin_users.py
Mario33333's picture
deploy: reader-access security hardening (feature/audiobook@06a5ed8)
7c2d250 verified
Raw
History Blame Contribute Delete
2.01 kB
"""DTOs for ``/admin/users/*`` β€” admin management of DB-backed reader accounts.
These shape the wire for the admin UI that creates/lists/disables/deletes the
low-privilege ``reader`` accounts stored in ``app_users`` (see
:mod:`src.lib.auth.user_store`). Password hashes are NEVER returned β€” the list
DTO carries only safe-to-display fields.
All DTOs inherit :class:`ApiModel` so the wire is camelCase (CONTRACT.md Β§1).
"""
from __future__ import annotations
from pydantic import Field
from .common import ApiModel
class AppUserDTO(ApiModel):
"""One DB-backed account, projected for the admin list/management UI.
No ``passwordHash`` field by design β€” the hash never leaves the server.
"""
username: str
role: str
display_name: str | None = None
disabled: bool
created_at: str | None = None
class AppUserListResponse(ApiModel):
"""``GET /admin/users`` envelope."""
items: list[AppUserDTO]
class CreateAppUserRequest(ApiModel):
"""``POST /admin/users`` body.
Creates a ``reader`` account (the role is forced server-side β€” this body
intentionally has no ``role`` field so an admin can't mint another admin
through the DB). ``password`` is validated for a sane minimum length in the
router; ``displayName`` is optional and cosmetic.
"""
username: str = Field(
min_length=1,
max_length=64,
pattern=r"^[A-Za-z0-9._-]+$",
)
password: str = Field(min_length=8, max_length=256)
display_name: str | None = Field(default=None, max_length=128)
class UpdateAppUserRequest(ApiModel):
"""``PATCH /admin/users/{username}`` body.
Both fields optional: set ``disabled`` to enable/suspend the account, set
``password`` to reset it (re-hashed server-side). Sending neither is a
no-op 200 with the current row.
"""
disabled: bool | None = None
password: str | None = Field(default=None, max_length=256)