Files
grateful-journal/backend/routers/users.py

179 lines
6.5 KiB
Python

"""User management routes"""
import logging
from fastapi import APIRouter, HTTPException, Depends
from db import get_database
from models import UserCreate, UserUpdate
from datetime import datetime
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, token: dict = Depends(get_current_user)):
"""
Register or get user (idempotent).
Uses upsert pattern to ensure one user per email.
If user already exists, returns existing user.
Called after Firebase Google Auth on frontend.
"""
if user_data.email != token.get("email"):
raise HTTPException(status_code=403, detail="Access denied")
db = get_database()
try:
result = db.users.update_one(
{"email": user_data.email},
{
"$setOnInsert": {
"email": user_data.email,
"displayName": user_data.displayName or user_data.email.split("@")[0],
"photoURL": user_data.photoURL,
"theme": "light",
"createdAt": datetime.utcnow()
},
"$set": {
"updatedAt": datetime.utcnow()
}
},
upsert=True
)
user = db.users.find_one({"email": user_data.email})
if not user:
raise HTTPException(status_code=500, detail="Failed to retrieve user after upsert")
return {
"id": str(user["_id"]),
"email": user["email"],
"displayName": user["displayName"],
"photoURL": user.get("photoURL"),
"theme": user.get("theme", "light"),
"backgroundImage": user.get("backgroundImage"),
"backgroundImages": user.get("backgroundImages", []),
"reminder": user.get("reminder"),
"createdAt": user["createdAt"].isoformat(),
"updatedAt": user["updatedAt"].isoformat(),
"message": "User registered successfully" if result.upserted_id else "User already exists"
}
except HTTPException:
raise
except Exception as 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, token: dict = Depends(get_current_user)):
"""Get user profile by email (called after Firebase Auth)."""
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:
raise HTTPException(status_code=404, detail="User not found")
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", []),
"reminder": user.get("reminder"),
"tutorial": user.get("tutorial"),
"createdAt": user["createdAt"].isoformat(),
"updatedAt": user["updatedAt"].isoformat()
}
except HTTPException:
raise
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, token: dict = Depends(get_current_user)):
"""Get user profile by ID."""
db = get_database()
try:
user = verify_user_access(user_id, db, token)
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", []),
"createdAt": user["createdAt"].isoformat(),
"updatedAt": user["updatedAt"].isoformat()
}
except HTTPException:
raise
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, token: dict = Depends(get_current_user)):
"""Update user profile."""
db = get_database()
try:
user = verify_user_access(user_id, db, token)
user_oid = user["_id"]
update_data = user_data.model_dump(exclude_unset=True)
update_data["updatedAt"] = datetime.utcnow()
db.users.update_one({"_id": user_oid}, {"$set": update_data})
updated = db.users.find_one({"_id": user_oid})
return {
"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:
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, token: dict = Depends(get_current_user)):
"""Delete user account and all associated data."""
db = get_database()
try:
user = verify_user_access(user_id, db, token)
user_oid = user["_id"]
user_result = db.users.delete_one({"_id": user_oid})
entry_result = db.entries.delete_many({"userId": user_oid})
return {
"message": "User deleted successfully",
"user_deleted": user_result.deleted_count,
"entries_deleted": entry_result.deleted_count
}
except HTTPException:
raise
except Exception:
log.exception("User deletion failed")
raise HTTPException(status_code=500, detail="Internal server error")