"""Request/response models for the auth API (SPEC ยง9.1).""" from __future__ import annotations import uuid from typing import Literal from pydantic import BaseModel, Field class RegisterRequest(BaseModel): localpart: str = Field(min_length=3, max_length=30) password: str = Field(min_length=8, max_length=128) recovery_email: str | None = None # A friend's invitation (app/identity/invitations.py): signs up even while # public registration is closed, and is used up by it. invite: str | None = Field(default=None, max_length=200) class LoginRequest(BaseModel): # Accepts either a bare localpart or the full ``localpart@domain`` handle. handle: str = Field(min_length=1, max_length=320) password: str = Field(min_length=1, max_length=1024) totp: str | None = None # required when the account has 2FA enabled # With a correct code: don't ask this browser for one again (trusted device). remember_device: bool = False class TotpSetupResponse(BaseModel): secret: str # base32, for manual entry provisioning_uri: str # otpauth:// URI to render as a QR code class TotpEnableRequest(BaseModel): code: str = Field(min_length=6, max_length=10) class TotpDisableRequest(BaseModel): password: str = Field(min_length=1, max_length=1024) class SessionOut(BaseModel): sid: str created: str ua: str current: bool class PasswordResetRequestIn(BaseModel): handle: str = Field(min_length=1, max_length=320) class PasswordResetIn(BaseModel): token: str = Field(min_length=1) password: str = Field(min_length=8, max_length=128) totp: str | None = None # required when the account has 2FA enabled class PasswordResetCheckIn(BaseModel): token: str = Field(min_length=1) class PasswordResetCheckOut(BaseModel): valid: bool # Tells the page to expect the 2FA step; only the link's holder learns it. totp_required: bool = False class AppPasswordIn(BaseModel): name: str = Field(min_length=1, max_length=60) # "Thunderbird on laptop" password: str = Field(min_length=1, max_length=1024) # the account password, to confirm class AppPasswordOut(BaseModel): id: str name: str created_at: str last_used_at: str | None = None class AppPasswordCreatedOut(AppPasswordOut): # The generated password itself: in this answer only, never again. app_password: str class RecoveryEmailIn(BaseModel): password: str = Field(min_length=1, max_length=1024) # None (or blank) removes the recovery address. recovery_email: str | None = Field(default=None, max_length=320) class UserOut(BaseModel): id: uuid.UUID handle: str recovery_email: str | None settings: dict[str, object] totp_enabled: bool = False is_admin: bool = False # The hosted mail server's name for mail apps (IMAP 993, SMTP 465/587), or # None when there are no hosted mailboxes (SPEC App. D). mail_host: str | None = None class InvitationCheckIn(BaseModel): token: str = Field(min_length=1, max_length=200) class InvitationCheckOut(BaseModel): valid: bool invitee_name: str = "" inviter_name: str = "" class RegistrationStatusOut(BaseModel): """Public, unauthenticated: what the signup page should show.""" open: bool # True when the instance has no users yet, so signup creates the administrator. bootstrap: bool # The domain every handle lives under (localpart@domain), shown beside the # handle field. domain: str class AdminSettingsOut(BaseModel): registration_open: bool default_quota_bytes: int # Users may send friends sign-up invitations (app/identity/invitations.py). invitations_enabled: bool class AdminSettingsIn(BaseModel): """Merge semantics: omitted fields are left as they are.""" registration_open: bool | None = None invitations_enabled: bool | None = None # Applies to users created after the change; existing mailboxes keep theirs. default_quota_bytes: int | None = Field(default=None, gt=0) class AdminMailOut(BaseModel): smtp_host: str smtp_port: int smtp_security: str smtp_username: str # The password itself never leaves the server. has_password: bool smtp_from: str public_url: str # Host, From address and public address are all set: reset mail can go out. configured: bool class AdminMailIn(BaseModel): """Merge semantics: omitted fields are left as they are.""" smtp_host: str | None = Field(default=None, max_length=255) smtp_port: int | None = Field(default=None, ge=1, le=65535) smtp_security: Literal["starttls", "ssl", "none"] | None = None smtp_username: str | None = Field(default=None, max_length=320) # Omitted keeps the stored password; an empty string removes it. smtp_password: str | None = Field(default=None, max_length=1024) smtp_from: str | None = Field(default=None, max_length=320) public_url: str | None = Field(default=None, max_length=500) class AdminInviteTemplateOut(BaseModel): # Empty strings: the built-in subject / template / sender is used. subject: str html: str from_address: str default_subject: str default_html: str default_from: str placeholders: list[str] class AdminInviteTemplateIn(BaseModel): """Merge semantics: omitted fields are left as they are; an empty string goes back to the built-in one.""" subject: str | None = Field(default=None, max_length=300) html: str | None = None from_address: str | None = Field(default=None, max_length=320) class AdminInvitePreviewIn(BaseModel): subject: str = Field(default="", max_length=300) html: str = "" class AdminInvitePreviewOut(BaseModel): subject: str html: str class AdminMailTestIn(BaseModel): to: str = Field(min_length=3, max_length=320) class AdminUserOut(BaseModel): id: uuid.UUID handle: str is_admin: bool # ISO timestamp while blocked; null otherwise. blocked_at: str | None quota_bytes: int used_bytes: int created_at: str # The user's connected mail accounts, so the admin can tell who this is. accounts: list[str] class AdminUserPatch(BaseModel): """Merge semantics: omitted fields are left as they are.""" quota_bytes: int | None = Field(default=None, gt=0) blocked: bool | None = None class MessageOut(BaseModel): status: str = "ok"