Files
2025-09-28 20:41:57 +09:00

334 lines
9.8 KiB
Python

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
)