from pydantic import BaseModel, Field # type: ignore from datetime import datetime from typing import Optional, List from enum import Enum from bson import ObjectId # ========== Helper for ObjectId handling ========== class PyObjectId(ObjectId): """Custom type for ObjectId serialization""" @classmethod def __get_validators__(cls): yield cls.validate @classmethod def validate(cls, v): if isinstance(v, ObjectId): return v if isinstance(v, str): return ObjectId(v) raise ValueError(f"Invalid ObjectId: {v}") def __repr__(self): return f"ObjectId('{self}')" # ========== User Models ========== class UserCreate(BaseModel): email: str displayName: Optional[str] = None photoURL: Optional[str] = None class UserUpdate(BaseModel): displayName: Optional[str] = None photoURL: Optional[str] = None theme: Optional[str] = None tutorial: Optional[bool] = None class Config: json_schema_extra = { "example": { "displayName": "John Doe", "theme": "dark" } } class User(BaseModel): id: str = Field(alias="_id") email: str displayName: Optional[str] = None photoURL: Optional[str] = None createdAt: datetime updatedAt: datetime theme: str = "light" tutorial: Optional[bool] = None class Config: from_attributes = True populate_by_name = True json_schema_extra = { "example": { "_id": "507f1f77bcf86cd799439011", "email": "user@example.com", "displayName": "John Doe", "photoURL": "https://example.com/photo.jpg", "createdAt": "2026-03-05T00:00:00Z", "updatedAt": "2026-03-05T00:00:00Z", "theme": "light" } } # ========== Journal Entry Models ========== class MoodEnum(str, Enum): happy = "happy" sad = "sad" neutral = "neutral" anxious = "anxious" grateful = "grateful" class EncryptionMetadata(BaseModel): """Encryption metadata for entries - zero-knowledge privacy""" encrypted: bool = True ciphertext: str # Base64-encoded encrypted content nonce: str # Base64-encoded nonce used for encryption algorithm: str = "XSalsa20-Poly1305" # crypto_secretbox algorithm class Config: json_schema_extra = { "example": { "encrypted": True, "ciphertext": "base64_encoded_ciphertext...", "nonce": "base64_encoded_nonce...", "algorithm": "XSalsa20-Poly1305" } } class JournalEntryCreate(BaseModel): title: Optional[str] = None # Optional if encrypted content: Optional[str] = None # Optional if encrypted mood: Optional[MoodEnum] = None tags: Optional[List[str]] = None isPublic: Optional[bool] = False # Logical journal date; defaults to today entryDate: Optional[datetime] = None # Encryption metadata - present if entry is encrypted encryption: Optional[EncryptionMetadata] = None class Config: json_schema_extra = { "example": { "encryption": { "encrypted": True, "ciphertext": "base64_ciphertext...", "nonce": "base64_nonce...", "algorithm": "XSalsa20-Poly1305" }, "mood": "grateful", "tags": ["work", "family"], "isPublic": False, "entryDate": "2026-03-05T00:00:00Z" } } class JournalEntryUpdate(BaseModel): title: Optional[str] = None content: Optional[str] = None mood: Optional[MoodEnum] = None tags: Optional[List[str]] = None isPublic: Optional[bool] = None encryption: Optional[EncryptionMetadata] = None class Config: json_schema_extra = { "example": { "title": "Updated Title", "mood": "happy" } } class JournalEntry(BaseModel): id: str = Field(alias="_id") userId: str # ObjectId as string title: Optional[str] = None # None if encrypted content: Optional[str] = None # None if encrypted mood: Optional[MoodEnum] = None tags: Optional[List[str]] = [] isPublic: bool = False entryDate: datetime # Logical journal date createdAt: datetime updatedAt: datetime encryption: Optional[EncryptionMetadata] = None # Present if encrypted class Config: from_attributes = True populate_by_name = True json_schema_extra = { "example": { "_id": "507f1f77bcf86cd799439011", "userId": "507f1f77bcf86cd799439012", "encryption": { "encrypted": True, "ciphertext": "base64_ciphertext...", "nonce": "base64_nonce...", "algorithm": "XSalsa20-Poly1305" }, "mood": "grateful", "tags": ["work", "family"], "isPublic": False, "entryDate": "2026-03-05T00:00:00Z", "createdAt": "2026-03-05T12:00:00Z", "updatedAt": "2026-03-05T12:00:00Z" } } # ========== Pagination Models ========== class PaginationMeta(BaseModel): """Pagination metadata for list responses""" total: int limit: int skip: int hasMore: bool class Config: json_schema_extra = { "example": { "total": 42, "limit": 20, "skip": 0, "hasMore": True } } class EntriesListResponse(BaseModel): """Response model for paginated entries""" entries: List[JournalEntry] pagination: PaginationMeta class Config: json_schema_extra = { "example": { "entries": [], "pagination": { "total": 42, "limit": 20, "skip": 0, "hasMore": True } } }