179 lines
6.2 KiB
Python
179 lines
6.2 KiB
Python
"""User management routes"""
|
|
from fastapi import APIRouter, HTTPException
|
|
from pymongo.errors import DuplicateKeyError, WriteError
|
|
from db import get_database
|
|
from models import UserCreate, UserUpdate, User
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from bson import ObjectId
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/register", response_model=dict)
|
|
async def register_user(user_data: UserCreate):
|
|
"""
|
|
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.
|
|
"""
|
|
db = get_database()
|
|
|
|
try:
|
|
# Upsert: Update if exists, insert if not
|
|
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
|
|
)
|
|
|
|
# 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")
|
|
|
|
return {
|
|
"id": str(user["_id"]),
|
|
"email": user["email"],
|
|
"displayName": user["displayName"],
|
|
"photoURL": user.get("photoURL"),
|
|
"theme": user.get("theme", "light"),
|
|
"createdAt": user["createdAt"].isoformat(),
|
|
"updatedAt": user["updatedAt"].isoformat(),
|
|
"message": "User registered successfully" if result.upserted_id else "User already exists"
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Registration failed: {str(e)}")
|
|
|
|
|
|
@router.get("/by-email/{email}", response_model=dict)
|
|
async def get_user_by_email(email: str):
|
|
"""Get user profile by email (called after Firebase Auth)."""
|
|
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"),
|
|
"createdAt": user["createdAt"].isoformat(),
|
|
"updatedAt": user["updatedAt"].isoformat()
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch user: {str(e)}")
|
|
|
|
|
|
@router.get("/{user_id}", response_model=dict)
|
|
async def get_user_by_id(user_id: str):
|
|
"""Get user profile by ID."""
|
|
db = get_database()
|
|
|
|
try:
|
|
user = db.users.find_one({"_id": ObjectId(user_id)})
|
|
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"),
|
|
"createdAt": user["createdAt"].isoformat(),
|
|
"updatedAt": user["updatedAt"].isoformat()
|
|
}
|
|
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 user: {str(e)}")
|
|
|
|
|
|
@router.put("/{user_id}", response_model=dict)
|
|
async def update_user(user_id: str, user_data: UserUpdate):
|
|
"""Update user profile."""
|
|
db = get_database()
|
|
|
|
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": ObjectId(user_id)},
|
|
{"$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": ObjectId(user_id)})
|
|
return {
|
|
"id": str(user["_id"]),
|
|
"email": user["email"],
|
|
"displayName": user.get("displayName"),
|
|
"photoURL": user.get("photoURL"),
|
|
"theme": user.get("theme", "light"),
|
|
"createdAt": user["createdAt"].isoformat(),
|
|
"updatedAt": user["updatedAt"].isoformat(),
|
|
"message": "User updated successfully"
|
|
}
|
|
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"Update failed: {str(e)}")
|
|
|
|
|
|
@router.delete("/{user_id}")
|
|
async def delete_user(user_id: str):
|
|
"""Delete user account and all associated data."""
|
|
db = get_database()
|
|
|
|
try:
|
|
# Delete user
|
|
user_result = db.users.delete_one({"_id": ObjectId(user_id)})
|
|
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": ObjectId(user_id)})
|
|
|
|
return {
|
|
"message": "User deleted successfully",
|
|
"user_deleted": user_result.deleted_count,
|
|
"entries_deleted": entry_result.deleted_count
|
|
}
|
|
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"Deletion failed: {str(e)}")
|
|
|
|
# Delete all entries by user
|
|
db.entries.delete_many({"userId": user_id})
|
|
|
|
# Delete user settings
|
|
db.settings.delete_one({"userId": user_id})
|
|
|
|
return {"message": "User and associated data deleted"}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|