from datetime import datetime from typing import Literal from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator from shared.phone import normalize_mobile, normalize_otp_digits class AuthConfigResponse(BaseModel): """پیکربندی OIDC برای frontend — بدون hardcode در کلاینت.""" issuer: str client_id: str authorization_endpoint: str token_endpoint: str logout_endpoint: str userinfo_endpoint: str callback_url: str class LoginUrlResponse(BaseModel): """آدرس ورود Keycloak به همراه state (PKCE سمت سرور).""" authorization_url: str state: str class TokenExchangeRequest(BaseModel): code: str = Field(..., min_length=1) redirect_uri: str = Field(..., min_length=1) state: str | None = Field(default=None) code_verifier: str | None = Field(default=None, min_length=43, max_length=128) class TokenResponse(BaseModel): access_token: str refresh_token: str | None = None expires_in: int token_type: str = "Bearer" class MeResponse(BaseModel): user_id: str keycloak_sub: str email: str | None username: str | None display_name: str | None mobile: str | None mobile_verified: bool requires_mobile_verification: bool roles: list[str] tenant_memberships: list[dict] class ProfileUpdateRequest(BaseModel): """به‌روزرسانی پروفایل کاربر جاری (نام نمایشی و ایمیل).""" display_name: str | None = Field(default=None, max_length=255) email: EmailStr | None = None class PasswordChangeRequest(BaseModel): """تغییر یا تنظیم رمز عبور کاربر جاری.""" current_password: str | None = Field(default=None, max_length=128) new_password: str = Field(..., min_length=8, max_length=128) class PasswordChangeResponse(BaseModel): message: str class RegisterRequest(BaseModel): """ثبت‌نام با نام کاربری، رمز عبور و شماره موبایل.""" email: EmailStr username: str = Field(..., min_length=3, max_length=150) password: str = Field(..., min_length=8) display_name: str | None = None mobile: str = Field(..., min_length=10, max_length=15) @field_validator("mobile") @classmethod def validate_mobile(cls, value: str) -> str: return normalize_mobile(value) class MobileOtpRequest(BaseModel): mobile: str = Field(..., min_length=10, max_length=15) force_sms: bool = False @field_validator("mobile") @classmethod def validate_mobile(cls, value: str) -> str: return normalize_mobile(value) class MobileOtpVerify(BaseModel): mobile: str = Field(..., min_length=10, max_length=15) code: str = Field(..., min_length=4, max_length=8) @field_validator("mobile") @classmethod def validate_mobile(cls, value: str) -> str: return normalize_mobile(value) @field_validator("code") @classmethod def validate_code(cls, value: str) -> str: normalized = normalize_otp_digits(value) if not normalized: raise ValueError("کد تأیید نامعتبر است") return normalized class MobileRegisterVerify(MobileOtpVerify): """تکمیل ورود/ثبت‌نام یکپارچه — فیلدهای پروفایل فقط برای ثبت‌نام جدید.""" display_name: str | None = None email: EmailStr | None = None username: str | None = Field(default=None, min_length=3, max_length=150) password: str | None = Field(default=None, min_length=8) create_handoff: bool = False @model_validator(mode="after") def validate_credentials_bundle(self) -> "MobileRegisterVerify": fields = (self.email, self.username, self.password) if any(fields) and not all(fields): raise ValueError("ایمیل، نام کاربری و رمز عبور باید با هم ارسال شوند") return self class HandoffRedeemRequest(BaseModel): handoff_code: str = Field(..., min_length=16) class OtpRequestResponse(BaseModel): message: str expires_in: int skipped: bool = False class RegisterCompleteResponse(BaseModel): message: str mobile_verified: bool user_id: str | None = None class AuthSessionResponse(TokenResponse): """پاسخ ورود/ثبت‌نام موفق — شامل توکن SSO برای redirect مستقیم به داشبورد.""" message: str intent: Literal["login", "register"] user_id: str | None = None mobile_verified: bool = True redirect_url: str | None = None class MobileAuthStartResponse(OtpRequestResponse): """شروع ورود/ثبت‌نام یکپارچه با موبایل — بک‌اند نوع عملیات را تشخیص می‌دهد.""" intent: Literal["login", "register"] class VerifyMobileResponse(BaseModel): message: str mobile_verified: bool mobile_verified_at: datetime | None = None