from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Optional from datetime import datetime import uvicorn import os import sys import logging from contextlib import asynccontextmanager from database import init_db from models import User from beanie import PydanticObjectId sys.path.append('/app') from shared.kafka import KafkaProducer, Event, EventType logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Pydantic models for requests class UserCreate(BaseModel): username: str email: str full_name: Optional[str] = None profile_picture: Optional[str] = None bio: Optional[str] = None location: Optional[str] = None website: Optional[str] = None class UserUpdate(BaseModel): username: Optional[str] = None email: Optional[str] = None full_name: Optional[str] = None profile_picture: Optional[str] = None profile_picture_thumbnail: Optional[str] = None bio: Optional[str] = None location: Optional[str] = None website: Optional[str] = None is_email_verified: Optional[bool] = None is_active: Optional[bool] = None class UserResponse(BaseModel): id: str username: str email: str full_name: Optional[str] = None profile_picture: Optional[str] = None profile_picture_thumbnail: Optional[str] = None bio: Optional[str] = None location: Optional[str] = None website: Optional[str] = None is_email_verified: bool is_active: bool created_at: datetime updated_at: datetime class UserPublicResponse(BaseModel): """공개 프로필용 응답 (민감한 정보 제외)""" id: str username: str full_name: Optional[str] = None profile_picture: Optional[str] = None profile_picture_thumbnail: Optional[str] = None bio: Optional[str] = None location: Optional[str] = None website: Optional[str] = None created_at: datetime # Global Kafka producer kafka_producer: Optional[KafkaProducer] = None @asynccontextmanager async def lifespan(app: FastAPI): # Startup global kafka_producer await init_db() # Initialize Kafka producer try: kafka_producer = KafkaProducer( bootstrap_servers=os.getenv('KAFKA_BOOTSTRAP_SERVERS', 'kafka:9092') ) await kafka_producer.start() logger.info("Kafka producer initialized") except Exception as e: logger.warning(f"Failed to initialize Kafka producer: {e}") kafka_producer = None yield # Shutdown if kafka_producer: await kafka_producer.stop() app = FastAPI( title="Users Service", description="User management microservice with MongoDB", version="0.2.0", lifespan=lifespan ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Health check @app.get("/health") async def health_check(): return { "status": "healthy", "service": "users", "timestamp": datetime.now().isoformat() } # CRUD Operations @app.get("/users", response_model=List[UserResponse]) async def get_users(): users = await User.find_all().to_list() return [UserResponse( id=str(user.id), username=user.username, email=user.email, full_name=user.full_name, profile_picture=user.profile_picture, profile_picture_thumbnail=user.profile_picture_thumbnail, bio=user.bio, location=user.location, website=user.website, is_email_verified=user.is_email_verified, is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at ) for user in users] @app.get("/users/{user_id}", response_model=UserResponse) async def get_user(user_id: str): try: user = await User.get(PydanticObjectId(user_id)) if not user: raise HTTPException(status_code=404, detail="User not found") return UserResponse( id=str(user.id), username=user.username, email=user.email, full_name=user.full_name, profile_picture=user.profile_picture, profile_picture_thumbnail=user.profile_picture_thumbnail, bio=user.bio, location=user.location, website=user.website, is_email_verified=user.is_email_verified, is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at ) except Exception: raise HTTPException(status_code=404, detail="User not found") @app.post("/users", response_model=UserResponse, status_code=201) async def create_user(user_data: UserCreate): # Check if username already exists existing_user = await User.find_one(User.username == user_data.username) if existing_user: raise HTTPException(status_code=400, detail="Username already exists") # Create new user user = User( username=user_data.username, email=user_data.email, full_name=user_data.full_name, profile_picture=user_data.profile_picture, bio=user_data.bio, location=user_data.location, website=user_data.website ) await user.create() # Publish event if kafka_producer: event = Event( event_type=EventType.USER_CREATED, service="users", data={ "user_id": str(user.id), "username": user.username, "email": user.email }, user_id=str(user.id) ) await kafka_producer.send_event("user-events", event) return UserResponse( id=str(user.id), username=user.username, email=user.email, full_name=user.full_name, profile_picture=user.profile_picture, profile_picture_thumbnail=user.profile_picture_thumbnail, bio=user.bio, location=user.location, website=user.website, is_email_verified=user.is_email_verified, is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at ) @app.put("/users/{user_id}", response_model=UserResponse) async def update_user(user_id: str, user_update: UserUpdate): try: user = await User.get(PydanticObjectId(user_id)) if not user: raise HTTPException(status_code=404, detail="User not found") except Exception: raise HTTPException(status_code=404, detail="User not found") if user_update.username is not None: # Check if new username already exists existing_user = await User.find_one( User.username == user_update.username, User.id != user.id ) if existing_user: raise HTTPException(status_code=400, detail="Username already exists") user.username = user_update.username if user_update.email is not None: user.email = user_update.email if user_update.full_name is not None: user.full_name = user_update.full_name if user_update.profile_picture is not None: user.profile_picture = user_update.profile_picture if user_update.profile_picture_thumbnail is not None: user.profile_picture_thumbnail = user_update.profile_picture_thumbnail if user_update.bio is not None: user.bio = user_update.bio if user_update.location is not None: user.location = user_update.location if user_update.website is not None: user.website = user_update.website if user_update.is_email_verified is not None: user.is_email_verified = user_update.is_email_verified if user_update.is_active is not None: user.is_active = user_update.is_active user.updated_at = datetime.now() await user.save() # Publish event if kafka_producer: event = Event( event_type=EventType.USER_UPDATED, service="users", data={ "user_id": str(user.id), "username": user.username, "email": user.email, "updated_fields": list(user_update.dict(exclude_unset=True).keys()) }, user_id=str(user.id) ) await kafka_producer.send_event("user-events", event) return UserResponse( id=str(user.id), username=user.username, email=user.email, full_name=user.full_name, profile_picture=user.profile_picture, profile_picture_thumbnail=user.profile_picture_thumbnail, bio=user.bio, location=user.location, website=user.website, is_email_verified=user.is_email_verified, is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at ) @app.delete("/users/{user_id}") async def delete_user(user_id: str): try: user = await User.get(PydanticObjectId(user_id)) if not user: raise HTTPException(status_code=404, detail="User not found") user_id_str = str(user.id) username = user.username await user.delete() # Publish event if kafka_producer: event = Event( event_type=EventType.USER_DELETED, service="users", data={ "user_id": user_id_str, "username": username }, user_id=user_id_str ) await kafka_producer.send_event("user-events", event) return {"message": "User deleted successfully"} except Exception: raise HTTPException(status_code=404, detail="User not found") if __name__ == "__main__": uvicorn.run( "main:app", host="0.0.0.0", port=8000, reload=True )