Files
site11/services/users/backend/main.py
jungwoo choi 1467766f3d Step 8: OAuth 2.0 인증 시스템 및 프로필 기능 구현
- OAuth 2.0 서비스 구현
  * Authorization Code, Client Credentials, Refresh Token 플로우 지원
  * 애플리케이션 등록 및 관리 기능
  * 토큰 introspection 및 revocation
  * SSO 설정 지원 (Google, GitHub, SAML)
  * 실용적인 스코프 시스템 (user, app, org, api 관리)

- 사용자 프로필 기능 확장
  * 프로필 사진 및 썸네일 필드 추가
  * bio, location, website 등 추가 프로필 정보
  * 이메일 인증 및 계정 활성화 상태 관리
  * UserPublicResponse 모델 추가

- OAuth 스코프 관리
  * picture 스코프 추가 (프로필 사진 접근 제어)
  * 카테고리별 스코프 정리 (기본 인증, 사용자 데이터, 앱 관리, 조직, API)
  * 스코프별 승인 필요 여부 설정

- 인프라 개선
  * Users 서비스 포트 매핑 추가 (8001)
  * OAuth 서비스 Docker 구성 (포트 8003)
  * Kafka 이벤트 통합 (USER_CREATED, USER_UPDATED, USER_DELETED)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 17:37:16 +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
)