from datetime import datetime from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, EmailStr, Field, SecretStr, field_validator Password = Annotated[SecretStr, Field(min_length=15, max_length=128)] class AuthInput(BaseModel): model_config = ConfigDict(extra="forbid") class ProfileInput(AuthInput): nickname: str | None = Field(None, min_length=1, max_length=80) email: EmailStr | None = Field(None, max_length=254) @field_validator("nickname", "email", mode="before") @classmethod def optional_text(cls, value): return (value.strip() or None) if isinstance(value, str) else value @field_validator("nickname") @classmethod def no_control_characters(cls, value): if value and any(ord(char) < 32 or ord(char) == 127 for char in value): raise ValueError("昵称不能包含控制字符") return value class RegisterInput(ProfileInput): username: str = Field(min_length=3, max_length=32, pattern=r"^[a-z][a-z0-9_]{2,31}$") password: Password @field_validator("username", mode="before") @classmethod def canonical_username(cls, value): return value.strip().lower() if isinstance(value, str) else value class LoginInput(AuthInput): username: str = Field(min_length=3, max_length=32, pattern=r"^[a-z][a-z0-9_]{2,31}$") password: SecretStr = Field(min_length=1, max_length=128) @field_validator("username", mode="before") @classmethod def canonical_username(cls, value): return value.strip().lower() if isinstance(value, str) else value class PasswordChangeInput(AuthInput): current_password: SecretStr = Field(min_length=1, max_length=128) new_password: Password class UserRead(BaseModel): model_config = ConfigDict(from_attributes=True) id: int username: str nickname: str | None email: str | None role: Literal["user", "admin"] created_at: datetime class SessionRead(UserRead): csrf_token: str