diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2c2a900..0b1b8a4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,8 @@ "Bash(curl:*)", "Bash(npx lighthouse:*)", "Bash(echo \"exit:$?\")", - "Bash(python -c \"from config import get_settings; s = get_settings\\(\\); print\\('SA JSON set:', bool\\(s.firebase_service_account_json\\)\\)\")" + "Bash(python -c \"from config import get_settings; s = get_settings\\(\\); print\\('SA JSON set:', bool\\(s.firebase_service_account_json\\)\\)\")", + "Bash(python3 -c ' *)" ] } } diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..eea6f46 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,47 @@ +"""Firebase token verification and ownership helpers.""" +import logging +from fastapi import HTTPException, Header +from firebase_admin import auth as firebase_auth +import firebase_admin +from bson import ObjectId +from bson.errors import InvalidId + +log = logging.getLogger(__name__) + + +async def get_current_user(authorization: str = Header(..., alias="Authorization")) -> dict: + """FastAPI dependency: verifies Firebase ID token and returns decoded payload.""" + if not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Invalid authorization header") + + token = authorization[len("Bearer "):] + + if not firebase_admin._apps: + raise HTTPException(status_code=503, detail="Authentication service unavailable") + + try: + return firebase_auth.verify_id_token(token) + except firebase_auth.ExpiredIdTokenError: + raise HTTPException(status_code=401, detail="Token expired") + except Exception: + raise HTTPException(status_code=401, detail="Invalid token") + + +def verify_user_access(user_id: str, db, token: dict) -> dict: + """ + Fetch user by ObjectId and confirm the token owner matches. + Returns the user document. Raises 400/404/403 on failure. + """ + try: + user_oid = ObjectId(user_id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid user ID format") + + user = db.users.find_one({"_id": user_oid}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + if user.get("email") != token.get("email"): + raise HTTPException(status_code=403, detail="Access denied") + + return user diff --git a/backend/main.py b/backend/main.py index 40cef81..92c576f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -52,7 +52,7 @@ app.add_middleware( allow_origins=cors_origins, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allow_headers=["*"], + allow_headers=["Authorization", "Content-Type"], ) # Include routers diff --git a/backend/routers/entries.py b/backend/routers/entries.py index 0e5978d..651794e 100644 --- a/backend/routers/entries.py +++ b/backend/routers/entries.py @@ -1,13 +1,13 @@ """Journal entry routes""" -from fastapi import APIRouter, HTTPException, Query +import logging +from fastapi import APIRouter, HTTPException, Query, Depends from db import get_database -from models import JournalEntryCreate, JournalEntryUpdate, JournalEntry, EntriesListResponse, PaginationMeta +from models import JournalEntryCreate, JournalEntryUpdate from datetime import datetime, timedelta -from typing import List, Optional -from bson import ObjectId -from bson.errors import InvalidId +from auth import get_current_user, verify_user_access from utils import format_ist_timestamp +log = logging.getLogger(__name__) router = APIRouter() @@ -16,21 +16,20 @@ def _format_entry(entry: dict) -> dict: return { "id": str(entry["_id"]), "userId": str(entry["userId"]), - "title": entry.get("title"), # None if encrypted - "content": entry.get("content"), # None if encrypted + "title": entry.get("title"), + "content": entry.get("content"), "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(), - # Full encryption metadata including ciphertext and nonce "encryption": entry.get("encryption") } @router.post("/{user_id}", response_model=dict) -async def create_entry(user_id: str, entry_data: JournalEntryCreate): +async def create_entry(user_id: str, entry_data: JournalEntryCreate, token: dict = Depends(get_current_user)): """ Create a new journal entry. @@ -38,33 +37,18 @@ async def create_entry(user_id: str, entry_data: JournalEntryCreate): - 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) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") - - try: - # Verify user exists - user = db.users.find_one({"_id": user_oid}) - if not user: - raise HTTPException(status_code=404, detail="User not found") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] 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( @@ -74,12 +58,12 @@ async def create_entry(user_id: str, entry_data: JournalEntryCreate): entry_doc = { "userId": user_oid, - "title": entry_data.title, # None if encrypted - "content": entry_data.content, # None if encrypted + "title": entry_data.title, + "content": entry_data.content, "mood": entry_data.mood, "tags": entry_data.tags or [], "isPublic": entry_data.isPublic or False, - "entryDate": entry_date, # Logical journal date + "entryDate": entry_date, "createdAt": now, "updatedAt": now, "encryption": entry_data.encryption.model_dump() if entry_data.encryption else None @@ -94,48 +78,29 @@ async def create_entry(user_id: str, entry_data: JournalEntryCreate): } except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to create entry: {str(e)}") + except Exception: + log.exception("Failed to create entry") + raise HTTPException(status_code=500, detail="Internal server error") @router.get("/{user_id}") async def get_user_entries( user_id: str, limit: int = Query(50, ge=1, le=100), - skip: int = Query(0, ge=0) + skip: int = Query(0, ge=0), + token: dict = Depends(get_current_user) ): - """ - Get paginated entries for a user (most recent first). - - Supports pagination via skip and limit. - """ + """Get paginated entries for a user (most recent first).""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] - try: - # Verify user exists - user = db.users.find_one({"_id": user_oid}) - if not user: - raise HTTPException(status_code=404, detail="User not found") - - # Get entries entries = list( - db.entries.find( - {"userId": user_oid} - ).sort("createdAt", -1).skip(skip).limit(limit) + db.entries.find({"userId": user_oid}).sort("createdAt", -1).skip(skip).limit(limit) ) - - # 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 return { "entries": formatted_entries, @@ -143,101 +108,95 @@ async def get_user_entries( "total": total, "limit": limit, "skip": skip, - "hasMore": has_more + "hasMore": (skip + limit) < total } } except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to fetch entries: {str(e)}") + except Exception: + log.exception("Failed to fetch entries") + raise HTTPException(status_code=500, detail="Internal server error") @router.get("/{user_id}/{entry_id}") -async def get_entry(user_id: str, entry_id: str): +async def get_entry(user_id: str, entry_id: str, token: dict = Depends(get_current_user)): """Get a specific entry by ID.""" + from bson import ObjectId + from bson.errors import InvalidId db = get_database() - try: - user_oid = ObjectId(user_id) - entry_oid = ObjectId(entry_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid ID format") - - try: - entry = db.entries.find_one({ - "_id": entry_oid, - "userId": user_oid - }) + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] + try: + entry_oid = ObjectId(entry_id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid entry ID format") + entry = db.entries.find_one({"_id": entry_oid, "userId": user_oid}) if not entry: raise HTTPException(status_code=404, detail="Entry not found") return _format_entry(entry) except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to fetch entry: {str(e)}") + except Exception: + log.exception("Failed to fetch entry") + raise HTTPException(status_code=500, detail="Internal server error") @router.put("/{user_id}/{entry_id}") -async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpdate): +async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpdate, token: dict = Depends(get_current_user)): """Update a journal entry.""" + from bson import ObjectId + from bson.errors import InvalidId db = get_database() - try: - user_oid = ObjectId(user_id) - entry_oid = ObjectId(entry_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] + try: + entry_oid = ObjectId(entry_id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid entry ID format") - try: update_data = entry_data.model_dump(exclude_unset=True) update_data["updatedAt"] = datetime.utcnow() - # 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")) result = db.entries.update_one( - { - "_id": entry_oid, - "userId": user_oid - }, + {"_id": entry_oid, "userId": user_oid}, {"$set": update_data} ) if result.matched_count == 0: raise HTTPException(status_code=404, detail="Entry not found") - # Fetch and return updated entry entry = db.entries.find_one({"_id": entry_oid}) return _format_entry(entry) except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to update entry: {str(e)}") + except Exception: + log.exception("Failed to update entry") + raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/{user_id}/{entry_id}") -async def delete_entry(user_id: str, entry_id: str): +async def delete_entry(user_id: str, entry_id: str, token: dict = Depends(get_current_user)): """Delete a journal entry.""" + from bson import ObjectId + from bson.errors import InvalidId db = get_database() - try: - user_oid = ObjectId(user_id) - entry_oid = ObjectId(entry_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] + try: + entry_oid = ObjectId(entry_id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid entry ID format") - try: - result = db.entries.delete_one({ - "_id": entry_oid, - "userId": user_oid - }) + result = db.entries.delete_one({"_id": entry_oid, "userId": user_oid}) if result.deleted_count == 0: raise HTTPException(status_code=404, detail="Entry not found") @@ -245,108 +204,83 @@ async def delete_entry(user_id: str, entry_id: str): return {"message": "Entry deleted successfully"} except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to delete entry: {str(e)}") + except Exception: + log.exception("Failed to delete entry") + raise HTTPException(status_code=500, detail="Internal server error") @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. - """ +async def get_entries_by_date(user_id: str, date_str: str, token: dict = Depends(get_current_user)): + """Get entries for a specific date (format: YYYY-MM-DD).""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] + + try: + target_date = datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD") - try: - # Parse date - target_date = datetime.strptime(date_str, "%Y-%m-%d") next_date = target_date + timedelta(days=1) entries = list( db.entries.find({ "userId": user_oid, - "entryDate": { - "$gte": target_date, - "$lt": next_date - } + "entryDate": {"$gte": target_date, "$lt": next_date} }).sort("createdAt", -1) ) - formatted_entries = [_format_entry(entry) for entry in entries] - return { - "entries": formatted_entries, + "entries": [_format_entry(e) for e in entries], "date": date_str, - "count": len(formatted_entries) + "count": len(entries) } - except ValueError: - raise HTTPException( - status_code=400, detail="Invalid date format. Use YYYY-MM-DD") except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to fetch entries: {str(e)}") + except Exception: + log.exception("Failed to fetch entries by date") + raise HTTPException(status_code=500, detail="Internal server error") @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 - """ +async def get_entries_by_month( + user_id: str, + year: int, + month: int, + limit: int = Query(100, ge=1, le=500), + token: dict = Depends(get_current_user) +): + """Get entries for a specific month (for calendar view).""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] - try: 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: - end_date = datetime(year + 1, 1, 1) - else: - end_date = datetime(year, month + 1, 1) + end_date = datetime(year + 1, 1, 1) if month == 12 else datetime(year, month + 1, 1) entries = list( db.entries.find({ "userId": user_oid, - "entryDate": { - "$gte": start_date, - "$lt": end_date - } + "entryDate": {"$gte": start_date, "$lt": end_date} }).sort("entryDate", -1).limit(limit) ) - formatted_entries = [_format_entry(entry) for entry in entries] - return { - "entries": formatted_entries, + "entries": [_format_entry(e) for e in entries], "year": year, "month": month, - "count": len(formatted_entries) + "count": len(entries) } - except ValueError: - raise HTTPException(status_code=400, detail="Invalid year or month") except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to fetch entries: {str(e)}") + except Exception: + log.exception("Failed to fetch entries by month") + raise HTTPException(status_code=500, detail="Internal server error") @router.post("/convert-timestamp/utc-to-ist") @@ -355,18 +289,14 @@ async def convert_utc_to_ist(data: dict): try: utc_timestamp = data.get("timestamp") if not utc_timestamp: - raise HTTPException( - status_code=400, detail="Missing 'timestamp' field") + raise HTTPException(status_code=400, detail="Missing 'timestamp' field") ist_timestamp = format_ist_timestamp(utc_timestamp) - return { - "utc": utc_timestamp, - "ist": ist_timestamp - } + return {"utc": utc_timestamp, "ist": ist_timestamp} except HTTPException: raise 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)}") + except Exception: + log.exception("Timestamp conversion failed") + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/backend/routers/notifications.py b/backend/routers/notifications.py index 9b2a2c3..1b26168 100644 --- a/backend/routers/notifications.py +++ b/backend/routers/notifications.py @@ -1,12 +1,13 @@ """Notification routes — FCM token registration and reminder settings.""" -from fastapi import APIRouter, HTTPException +import logging +from fastapi import APIRouter, HTTPException, Depends from db import get_database from pydantic import BaseModel from typing import Optional -from bson import ObjectId -from bson.errors import InvalidId from datetime import datetime +from auth import get_current_user, verify_user_access +log = logging.getLogger(__name__) router = APIRouter() @@ -22,57 +23,52 @@ class ReminderSettingsRequest(BaseModel): @router.post("/fcm-token", response_model=dict) -async def register_fcm_token(body: FcmTokenRequest): +async def register_fcm_token(body: FcmTokenRequest, token: dict = Depends(get_current_user)): """ Register (or refresh) an FCM device token for a user. Stores unique tokens per user — duplicate tokens are ignored. """ db = get_database() - try: - user_oid = ObjectId(body.userId) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID") + user = verify_user_access(body.userId, db, token) + user_oid = user["_id"] - user = db.users.find_one({"_id": user_oid}) - if not user: - raise HTTPException(status_code=404, detail="User not found") - - # Add token to set (avoid duplicates) - db.users.update_one( - {"_id": user_oid}, - { - "$addToSet": {"fcmTokens": body.fcmToken}, - "$set": {"updatedAt": datetime.utcnow()}, - } - ) - return {"message": "FCM token registered"} + db.users.update_one( + {"_id": user_oid}, + { + "$addToSet": {"fcmTokens": body.fcmToken}, + "$set": {"updatedAt": datetime.utcnow()}, + } + ) + return {"message": "FCM token registered"} + except HTTPException: + raise + except Exception: + log.exception("Failed to register FCM token") + raise HTTPException(status_code=500, detail="Internal server error") @router.put("/reminder/{user_id}", response_model=dict) -async def update_reminder(user_id: str, settings: ReminderSettingsRequest): - """ - Save or update daily reminder settings for a user. - """ +async def update_reminder(user_id: str, settings: ReminderSettingsRequest, token: dict = Depends(get_current_user)): + """Save or update daily reminder settings for a user.""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] - user = db.users.find_one({"_id": user_oid}) - if not user: - raise HTTPException(status_code=404, detail="User not found") + reminder_update: dict = {"reminder.enabled": settings.enabled} + if settings.time is not None: + reminder_update["reminder.time"] = settings.time + if settings.timezone is not None: + reminder_update["reminder.timezone"] = settings.timezone - reminder_update: dict = {"reminder.enabled": settings.enabled} - if settings.time is not None: - reminder_update["reminder.time"] = settings.time - if settings.timezone is not None: - reminder_update["reminder.timezone"] = settings.timezone - - db.users.update_one( - {"_id": user_oid}, - {"$set": {**reminder_update, "updatedAt": datetime.utcnow()}} - ) - return {"message": "Reminder settings updated"} + db.users.update_one( + {"_id": user_oid}, + {"$set": {**reminder_update, "updatedAt": datetime.utcnow()}} + ) + return {"message": "Reminder settings updated"} + except HTTPException: + raise + except Exception: + log.exception("Failed to update reminder settings") + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/backend/routers/users.py b/backend/routers/users.py index c4a77ab..f432684 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -1,17 +1,17 @@ """User management routes""" -from fastapi import APIRouter, HTTPException +import logging +from fastapi import APIRouter, HTTPException, Depends from db import get_database -from models import UserCreate, UserUpdate, User +from models import UserCreate, UserUpdate from datetime import datetime -from typing import Optional -from bson import ObjectId -from bson.errors import InvalidId +from auth import get_current_user, verify_user_access +log = logging.getLogger(__name__) router = APIRouter() @router.post("/register", response_model=dict) -async def register_user(user_data: UserCreate): +async def register_user(user_data: UserCreate, token: dict = Depends(get_current_user)): """ Register or get user (idempotent). @@ -19,10 +19,11 @@ async def register_user(user_data: UserCreate): If user already exists, returns existing user. Called after Firebase Google Auth on frontend. """ - db = get_database() + if user_data.email != token.get("email"): + raise HTTPException(status_code=403, detail="Access denied") + db = get_database() try: - # Upsert: Update if exists, insert if not result = db.users.update_one( {"email": user_data.email}, { @@ -40,11 +41,9 @@ async def register_user(user_data: UserCreate): upsert=True ) - # Fetch the user (either newly created or existing) user = db.users.find_one({"email": user_data.email}) if not user: - raise HTTPException( - status_code=500, detail="Failed to retrieve user after upsert") + raise HTTPException(status_code=500, detail="Failed to retrieve user after upsert") return { "id": str(user["_id"]), @@ -62,15 +61,17 @@ async def register_user(user_data: UserCreate): except HTTPException: raise except Exception as e: - raise HTTPException( - status_code=500, detail=f"Registration failed: {str(e)}") + log.exception("Registration failed") + raise HTTPException(status_code=500, detail="Internal server error") @router.get("/by-email/{email}", response_model=dict) -async def get_user_by_email(email: str): +async def get_user_by_email(email: str, token: dict = Depends(get_current_user)): """Get user profile by email (called after Firebase Auth).""" - db = get_database() + if email != token.get("email"): + raise HTTPException(status_code=403, detail="Access denied") + db = get_database() try: user = db.users.find_one({"email": email}) if not user: @@ -91,26 +92,17 @@ async def get_user_by_email(email: str): } except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to fetch user: {str(e)}") + except Exception: + log.exception("Failed to fetch user by email") + raise HTTPException(status_code=500, detail="Internal server error") @router.get("/{user_id}", response_model=dict) -async def get_user_by_id(user_id: str): +async def get_user_by_id(user_id: str, token: dict = Depends(get_current_user)): """Get user profile by ID.""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") - - try: - user = db.users.find_one({"_id": user_oid}) - if not user: - raise HTTPException(status_code=404, detail="User not found") - + user = verify_user_access(user_id, db, token) return { "id": str(user["_id"]), "email": user["email"], @@ -124,72 +116,54 @@ async def get_user_by_id(user_id: str): } except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to fetch user: {str(e)}") + except Exception: + log.exception("Failed to fetch user by ID") + raise HTTPException(status_code=500, detail="Internal server error") @router.put("/{user_id}", response_model=dict) -async def update_user(user_id: str, user_data: UserUpdate): +async def update_user(user_id: str, user_data: UserUpdate, token: dict = Depends(get_current_user)): """Update user profile.""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] - try: - # Prepare update data (exclude None values) update_data = user_data.model_dump(exclude_unset=True) update_data["updatedAt"] = datetime.utcnow() - result = db.users.update_one( - {"_id": user_oid}, - {"$set": update_data} - ) + db.users.update_one({"_id": user_oid}, {"$set": update_data}) - if result.matched_count == 0: - raise HTTPException(status_code=404, detail="User not found") - - # Fetch and return updated user - user = db.users.find_one({"_id": user_oid}) + updated = db.users.find_one({"_id": user_oid}) return { - "id": str(user["_id"]), - "email": user["email"], - "displayName": user.get("displayName"), - "photoURL": user.get("photoURL"), - "theme": user.get("theme", "light"), - "backgroundImage": user.get("backgroundImage"), - "backgroundImages": user.get("backgroundImages", []), - "tutorial": user.get("tutorial"), - "createdAt": user["createdAt"].isoformat(), - "updatedAt": user["updatedAt"].isoformat(), + "id": str(updated["_id"]), + "email": updated["email"], + "displayName": updated.get("displayName"), + "photoURL": updated.get("photoURL"), + "theme": updated.get("theme", "light"), + "backgroundImage": updated.get("backgroundImage"), + "backgroundImages": updated.get("backgroundImages", []), + "tutorial": updated.get("tutorial"), + "createdAt": updated["createdAt"].isoformat(), + "updatedAt": updated["updatedAt"].isoformat(), "message": "User updated successfully" } except HTTPException: raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Update failed: {str(e)}") + except Exception: + log.exception("User update failed") + raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/{user_id}") -async def delete_user(user_id: str): +async def delete_user(user_id: str, token: dict = Depends(get_current_user)): """Delete user account and all associated data.""" db = get_database() - try: - user_oid = ObjectId(user_id) - except InvalidId: - raise HTTPException(status_code=400, detail="Invalid user ID format") + user = verify_user_access(user_id, db, token) + user_oid = user["_id"] - try: - # Delete user user_result = db.users.delete_one({"_id": user_oid}) - if user_result.deleted_count == 0: - raise HTTPException(status_code=404, detail="User not found") - - # Delete all user's entries entry_result = db.entries.delete_many({"userId": user_oid}) return { @@ -199,6 +173,6 @@ async def delete_user(user_id: str): } except HTTPException: raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Deletion failed: {str(e)}") + except Exception: + log.exception("User deletion failed") + raise HTTPException(status_code=500, detail="Internal server error")