226 lines
6.1 KiB
Python
226 lines
6.1 KiB
Python
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
|
|
}
|
|
}
|
|
}
|