"""Authentication request/response schemas.""" from __future__ import annotations import datetime as dt import uuid from pydantic import BaseModel, EmailStr, Field class TelegramProfile(BaseModel): username: str | None = None first_name: str | None = None last_name: str | None = None language_code: str | None = None class TelegramBotAuthRequest(BaseModel): telegram_id: int = Field(..., gt=0, description="Verified Telegram user id from aiogram") profile: TelegramProfile | None = None class TelegramWebAuthRequest(BaseModel): id: int = Field(..., gt=0) first_name: str | None = None last_name: str | None = None username: str | None = None photo_url: str | None = None auth_date: int hash: str class TelegramMiniAppAuthRequest(BaseModel): init_data: str = Field(..., description="Raw initData query string from Telegram.WebApp") class AuthResponse(BaseModel): access_token: str token_type: str = "bearer" expires_in: int user_id: str telegram_id: int class TokenIntrospectResponse(BaseModel): sub: str telegram_id: int type: str exp: int class WebUserPublic(BaseModel): """User profile subset safe to return to the webUI.""" id: str email: str | None = None name: str | None = None telegram_id: int | None = None credits_left: int is_active: bool created_at: dt.datetime class MeResponse(TokenIntrospectResponse): """Extends the introspection shape with web-user fields (additive).""" email: str | None = None name: str | None = None credits_left: int = 0 is_active: bool = True created_at: dt.datetime | None = None class RegisterRequest(BaseModel): email: EmailStr name: str = Field(..., min_length=1, max_length=128) password: str = Field(..., min_length=8, max_length=128) class LoginRequest(BaseModel): email: EmailStr password: str = Field(..., min_length=1, max_length=128) class TokenPairResponse(BaseModel): """JWT pair returned by register/login.""" access_token: str refresh_token: str token_type: str = "bearer" expires_in: int # access TTL in seconds user: WebUserPublic class LogoutRequest(BaseModel): refresh_token: str = Field(..., min_length=1) class ForgotPasswordRequest(BaseModel): email: EmailStr class ResetPasswordRequest(BaseModel): token: str = Field(..., min_length=1, max_length=256) password: str = Field(..., min_length=8, max_length=128) class OkResponse(BaseModel): """Generic `{ok: true, ...}` payload for state-mutating auth endpoints.""" ok: bool = True detail: str | None = None class PasskeyRpInfo(BaseModel): id: str name: str origin: str class PasskeyUserInfo(BaseModel): id: str name: str displayName: str class PubKeyCredParam(BaseModel): alg: int type: str class CredDescriptor(BaseModel): id: str type: str class AuthenticatorSelectionInfo(BaseModel): residentKey: str userVerification: str class PasskeyRegistrationOptions(BaseModel): """PublicKeyCredentialCreationOptions — matches the frontend zod schema.""" challenge: str rp: PasskeyRpInfo user: PasskeyUserInfo pubKeyCredParams: list[PubKeyCredParam] timeout: int attestation: str excludeCredentials: list[CredDescriptor] authenticatorSelection: AuthenticatorSelectionInfo class PasskeyAuthenticationOptions(BaseModel): """PublicKeyCredentialRequestOptions — matches the frontend zod schema.""" challenge: str timeout: int rpId: str allowCredentials: list[CredDescriptor] userVerification: str class PasskeyCredentialPublic(BaseModel): """Stored credential — matches passkeyCredentialSchema on the frontend.""" id: str credentialID: str credentialPublicKey: str counter: int userId: str deviceName: str createdAt: dt.datetime class PasskeyRegistrationFinishResponse(BaseModel): verified: bool credentialId: str class SingleTokenAuthResponse(BaseModel): """Shared by passkey-auth finish and magic-link verify (frontend schemas).""" access_token: str token_type: str = "bearer" expires_in: int user_id: uuid.UUID email: str class MagicLinkResponse(BaseModel): """Matches magicLinkResponseSchema on the frontend.""" success: bool = True message: str expires_in: int # link TTL in seconds class PasskeyRegisterStartRequest(BaseModel): device_name: str | None = Field(default=None, max_length=128) class PasskeyRegistrationCredentialData(BaseModel): """`navigator.credentials.create()` result, base64url-encoded.""" clientDataJSON: str attestationObject: str transports: list[str] | None = None class PasskeyRegistrationCredentialPayload(BaseModel): id: str rawId: str type: str response: PasskeyRegistrationCredentialData class PasskeyRegisterFinishRequest(BaseModel): credential: PasskeyRegistrationCredentialPayload device_name: str | None = Field(default=None, max_length=128) class PasskeyAuthenticationCredentialData(BaseModel): """`navigator.credentials.get()` result, base64url-encoded.""" clientDataJSON: str authenticatorData: str signature: str userHandle: str | None = None class PasskeyAuthenticationCredentialPayload(BaseModel): id: str rawId: str type: str response: PasskeyAuthenticationCredentialData class PasskeyAuthenticateFinishRequest(BaseModel): challenge: str = Field(..., min_length=16, max_length=512) credential: PasskeyAuthenticationCredentialPayload class MagicLinkRequest(BaseModel): email: EmailStr class MagicLinkVerifyRequest(BaseModel): token: str = Field(..., min_length=1, max_length=256)