from datetime import datetime from pydantic import BaseModel, EmailStr, Field class UserResponse(BaseModel): id: int email: str name: str role: str is_unthrottled: int = 0 created_at: datetime # Whether there is one at all, never anything about it. Settings has to say # "Set a password" or "Change password", and it cannot tell from the outside. has_password: bool = False # What the role *means*, computed once here rather than in every page that # asks. The interface had been checking `user.is_moderator` for months on a # payload that has never carried it, so every moderator-only control was # hidden from moderators — including the AI draft panel, which is why # "Draft with AI" appeared to do nothing. is_moderator: bool = False is_admin: bool = False class Config: from_attributes = True @staticmethod def of(user) -> "UserResponse": return UserResponse(**{ "id": user.id, "email": user.email, "name": user.name, "role": user.role, "is_unthrottled": user.is_unthrottled or 0, "created_at": user.created_at, "has_password": bool(user.hashed_password), "is_moderator": bool(user.is_moderator), "is_admin": bool(user.is_admin), }) class Token(BaseModel): access_token: str token_type: str = "bearer" #: Seconds, so a client can schedule its own refresh rather than waiting to #: be told no. Absent means "we did not say" — not "it never expires". expires_in: int | None = None #: Only when one was asked for. A browser does not need it: it has a #: session it can renew by asking the person again. An app does. refresh_token: str | None = None class LoginRequest(BaseModel): email: EmailStr password: str #: An app asks for a refresh token; the web app does not, so nothing #: long-lived is minted for a browser that will never use it. refresh: bool = False #: How the client describes itself, shown to the person in their list of #: sessions. "PedsHub for iPhone", not a user agent string. device: str | None = Field(default=None, max_length=120) class RefreshRequest(BaseModel): refresh_token: str class LogoutRequest(BaseModel): #: Ends this session. Omit it and, with `everywhere`, all of them. refresh_token: str | None = None everywhere: bool = False class UserUpdateRole(BaseModel): role: str class UserUpdateMe(BaseModel): name: str | None = None class SsoExchangeRequest(BaseModel): """The one-time code the SSO redirect leaves in the address bar.""" code: str = Field(min_length=8, max_length=200)