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