added encryption
This commit is contained in:
@@ -15,19 +15,16 @@ def _format_entry(entry: dict) -> dict:
|
||||
return {
|
||||
"id": str(entry["_id"]),
|
||||
"userId": str(entry["userId"]),
|
||||
"title": entry.get("title", ""),
|
||||
"content": entry.get("content", ""),
|
||||
"title": entry.get("title"), # None if encrypted
|
||||
"content": entry.get("content"), # None if encrypted
|
||||
"mood": entry.get("mood"),
|
||||
"tags": entry.get("tags", []),
|
||||
"isPublic": entry.get("isPublic", False),
|
||||
"entryDate": entry.get("entryDate", entry.get("createdAt")).isoformat() if entry.get("entryDate") or entry.get("createdAt") else None,
|
||||
"createdAt": entry["createdAt"].isoformat(),
|
||||
"updatedAt": entry["updatedAt"].isoformat(),
|
||||
"encryption": entry.get("encryption", {
|
||||
"encrypted": False,
|
||||
"iv": None,
|
||||
"algorithm": None
|
||||
})
|
||||
# Full encryption metadata including ciphertext and nonce
|
||||
"encryption": entry.get("encryption")
|
||||
}
|
||||
|
||||
|
||||
@@ -35,51 +32,70 @@ def _format_entry(entry: dict) -> dict:
|
||||
async def create_entry(user_id: str, entry_data: JournalEntryCreate):
|
||||
"""
|
||||
Create a new journal entry.
|
||||
|
||||
|
||||
For encrypted entries:
|
||||
- Send encryption metadata with ciphertext and nonce
|
||||
- Omit title and content (they're encrypted in ciphertext)
|
||||
|
||||
For unencrypted entries (deprecated):
|
||||
- Send title and content directly
|
||||
|
||||
entryDate: The logical journal date for this entry (defaults to today UTC).
|
||||
createdAt: Database write timestamp.
|
||||
|
||||
Server stores only: encrypted ciphertext, nonce, and metadata.
|
||||
Server never sees plaintext.
|
||||
"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
user_oid = ObjectId(user_id)
|
||||
|
||||
|
||||
# Verify user exists
|
||||
user = db.users.find_one({"_id": user_oid})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
now = datetime.utcnow()
|
||||
entry_date = entry_data.entryDate or now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
entry_date = entry_data.entryDate or now.replace(
|
||||
hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Validate encryption metadata if present
|
||||
if entry_data.encryption:
|
||||
if not entry_data.encryption.ciphertext or not entry_data.encryption.nonce:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Encryption metadata must include ciphertext and nonce"
|
||||
)
|
||||
|
||||
entry_doc = {
|
||||
"userId": user_oid,
|
||||
"title": entry_data.title,
|
||||
"content": entry_data.content,
|
||||
"title": entry_data.title, # None if encrypted
|
||||
"content": entry_data.content, # None if encrypted
|
||||
"mood": entry_data.mood,
|
||||
"tags": entry_data.tags or [],
|
||||
"isPublic": entry_data.isPublic or False,
|
||||
"entryDate": entry_date, # Logical journal date
|
||||
"createdAt": now,
|
||||
"updatedAt": now,
|
||||
"encryption": entry_data.encryption.model_dump() if entry_data.encryption else {
|
||||
"encrypted": False,
|
||||
"iv": None,
|
||||
"algorithm": None
|
||||
}
|
||||
"encryption": entry_data.encryption.model_dump() if entry_data.encryption else None
|
||||
}
|
||||
|
||||
result = db.entries.insert_one(entry_doc)
|
||||
|
||||
|
||||
return {
|
||||
"id": str(result.inserted_id),
|
||||
"userId": user_id,
|
||||
"message": "Entry created successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create entry: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create entry: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
@@ -90,14 +106,14 @@ async def get_user_entries(
|
||||
):
|
||||
"""
|
||||
Get paginated entries for a user (most recent first).
|
||||
|
||||
|
||||
Supports pagination via skip and limit.
|
||||
"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
user_oid = ObjectId(user_id)
|
||||
|
||||
|
||||
# Verify user exists
|
||||
user = db.users.find_one({"_id": user_oid})
|
||||
if not user:
|
||||
@@ -112,7 +128,7 @@ async def get_user_entries(
|
||||
|
||||
# Format entries
|
||||
formatted_entries = [_format_entry(entry) for entry in entries]
|
||||
|
||||
|
||||
# Get total count
|
||||
total = db.entries.count_documents({"userId": user_oid})
|
||||
has_more = (skip + limit) < total
|
||||
@@ -128,8 +144,10 @@ async def get_user_entries(
|
||||
}
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch entries: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch entries: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{user_id}/{entry_id}")
|
||||
@@ -153,7 +171,8 @@ async def get_entry(user_id: str, entry_id: str):
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch entry: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch entry: {str(e)}")
|
||||
|
||||
|
||||
@router.put("/{user_id}/{entry_id}")
|
||||
@@ -170,7 +189,8 @@ async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpda
|
||||
|
||||
# If entryDate provided in update data, ensure it's a datetime
|
||||
if "entryDate" in update_data and isinstance(update_data["entryDate"], str):
|
||||
update_data["entryDate"] = datetime.fromisoformat(update_data["entryDate"].replace("Z", "+00:00"))
|
||||
update_data["entryDate"] = datetime.fromisoformat(
|
||||
update_data["entryDate"].replace("Z", "+00:00"))
|
||||
|
||||
result = db.entries.update_one(
|
||||
{
|
||||
@@ -189,7 +209,8 @@ async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpda
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update entry: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update entry: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/{user_id}/{entry_id}")
|
||||
@@ -213,21 +234,22 @@ async def delete_entry(user_id: str, entry_id: str):
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete entry: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete entry: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{user_id}/by-date/{date_str}")
|
||||
async def get_entries_by_date(user_id: str, date_str: str):
|
||||
"""
|
||||
Get entries for a specific date (format: YYYY-MM-DD).
|
||||
|
||||
|
||||
Matches entries by entryDate field.
|
||||
"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
user_oid = ObjectId(user_id)
|
||||
|
||||
|
||||
# Parse date
|
||||
target_date = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
next_date = target_date + timedelta(days=1)
|
||||
@@ -254,25 +276,28 @@ async def get_entries_by_date(user_id: str, date_str: str):
|
||||
status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch entries: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch entries: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{user_id}/by-month/{year}/{month}")
|
||||
async def get_entries_by_month(user_id: str, year: int, month: int, limit: int = Query(100, ge=1, le=500)):
|
||||
"""
|
||||
Get entries for a specific month (for calendar view).
|
||||
|
||||
|
||||
Query format: GET /api/entries/{user_id}/by-month/{year}/{month}?limit=100
|
||||
"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
user_oid = ObjectId(user_id)
|
||||
|
||||
|
||||
if not (1 <= month <= 12):
|
||||
raise HTTPException(status_code=400, detail="Month must be between 1 and 12")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Month must be between 1 and 12")
|
||||
|
||||
# Calculate date range
|
||||
start_date = datetime(year, month, 1)
|
||||
if month == 12:
|
||||
@@ -302,8 +327,10 @@ async def get_entries_by_month(user_id: str, year: int, month: int, limit: int =
|
||||
raise HTTPException(status_code=400, detail="Invalid year or month")
|
||||
except Exception as e:
|
||||
if "invalid ObjectId" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch entries: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid user ID format")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch entries: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/convert-timestamp/utc-to-ist")
|
||||
@@ -323,4 +350,5 @@ async def convert_utc_to_ist(data: dict):
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Conversion failed: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Conversion failed: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user