from datetime import datetime from typing import Optional, List, Annotated from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict from pydantic_core import core_schema from bson import ObjectId class PyObjectId(str): """Custom ObjectId type for Pydantic v2""" @classmethod def __get_pydantic_core_schema__(cls, source_type, handler): return core_schema.union_schema([ core_schema.is_instance_schema(ObjectId), core_schema.chain_schema([ core_schema.str_schema(), core_schema.no_info_plain_validator_function(cls.validate), ]) ], serialization=core_schema.plain_serializer_function_ser_schema( lambda x: str(x) )) @classmethod def validate(cls, v): if isinstance(v, ObjectId): return v if isinstance(v, str) and ObjectId.is_valid(v): return ObjectId(v) raise ValueError("Invalid ObjectId") class UserRole(str): """User roles""" ADMIN = "admin" EDITOR = "editor" VIEWER = "viewer" class OAuthProvider(BaseModel): """OAuth provider information""" provider: str = Field(..., description="OAuth provider name (google, github, azure)") provider_user_id: str = Field(..., description="User ID from the provider") access_token: Optional[str] = Field(None, description="Access token (encrypted)") refresh_token: Optional[str] = Field(None, description="Refresh token (encrypted)") class UserProfile(BaseModel): """User profile information""" avatar_url: Optional[str] = None department: Optional[str] = None timezone: str = "Asia/Seoul" class User(BaseModel): """User model""" id: Optional[PyObjectId] = Field(alias="_id", default=None) email: EmailStr = Field(..., description="User email") username: str = Field(..., min_length=3, max_length=50, description="Username") hashed_password: str = Field(..., description="Hashed password") full_name: Optional[str] = Field(None, description="Full name") role: str = Field(default=UserRole.VIEWER, description="User role") permissions: List[str] = Field(default_factory=list, description="User permissions") oauth_providers: List[OAuthProvider] = Field(default_factory=list) profile: UserProfile = Field(default_factory=UserProfile) status: str = Field(default="active", description="User status") is_active: bool = Field(default=True) created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) last_login_at: Optional[datetime] = None model_config = ConfigDict( populate_by_name=True, arbitrary_types_allowed=True, json_encoders={ObjectId: str}, json_schema_extra={ "example": { "email": "user@example.com", "username": "johndoe", "full_name": "John Doe", "role": "viewer" } } ) class UserInDB(User): """User model with password hash""" pass