Step 5: Authentication System & Environment Variables

- Implemented JWT authentication in Console backend
- Added .env file for environment variable management
- Updated docker-compose to use .env variables
- Created authentication endpoints (login/logout/me)
- Added protected route middleware
- Created ARCHITECTURE.md with Kafka as main messaging platform
- Defined Kafka for both events and task queues
- Redis dedicated for caching and session management

Test credentials:
- admin/admin123
- user/user123

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2025-09-10 16:36:47 +09:00
parent 4b2be7ccaf
commit 315eeea2ae
6 changed files with 378 additions and 20 deletions

37
.env.example Normal file
View File

@ -0,0 +1,37 @@
# Environment Configuration Example
# Copy this file to .env and update with your values
ENV=development
# Port Configuration
CONSOLE_BACKEND_PORT=8011
CONSOLE_FRONTEND_PORT=3000
USERS_SERVICE_PORT=8001
MONGODB_PORT=27017
REDIS_PORT=6379
# Database Configuration
MONGODB_URL=mongodb://mongodb:27017
MONGODB_DATABASE=site11_db
USERS_DB_NAME=users_db
# Redis Configuration
REDIS_URL=redis://redis:6379
# JWT Configuration
JWT_SECRET_KEY=change-this-secret-key-in-production
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# Service URLs (Internal)
USERS_SERVICE_URL=http://users-backend:8000
# Frontend Configuration
VITE_API_URL=http://localhost:8011
# Kafka Configuration (Future)
# KAFKA_BOOTSTRAP_SERVERS=kafka:9092
# KAFKA_GROUP_ID=site11-group
# Docker Configuration
COMPOSE_PROJECT_NAME=site11

65
console/backend/auth.py Normal file
View File

@ -0,0 +1,65 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
import os
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
class UserLogin(BaseModel):
username: str
password: str
class UserInDB(BaseModel):
username: str
hashed_password: str
email: str
full_name: Optional[str] = None
is_active: bool = True
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
return token_data

View File

@ -1,10 +1,17 @@
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi import FastAPI, HTTPException, Request, Response, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordRequestForm
import uvicorn
from datetime import datetime
from datetime import datetime, timedelta
import httpx
import os
from typing import Any
from auth import (
Token, UserLogin, UserInDB,
verify_password, get_password_hash,
create_access_token, get_current_user,
ACCESS_TOKEN_EXPIRE_MINUTES
)
app = FastAPI(
title="Console API Gateway",
@ -40,6 +47,58 @@ async def health_check():
"timestamp": datetime.now().isoformat()
}
# Authentication endpoints
@app.post("/api/auth/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""Login endpoint for authentication"""
# For demo purposes - in production, check against database
# This is temporary until we integrate with Users service
demo_users = {
"admin": {
"username": "admin",
"hashed_password": get_password_hash("admin123"),
"email": "admin@site11.com",
"full_name": "Administrator",
"is_active": True
},
"user": {
"username": "user",
"hashed_password": get_password_hash("user123"),
"email": "user@site11.com",
"full_name": "Test User",
"is_active": True
}
}
user = demo_users.get(form_data.username)
if not user or not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/api/auth/me")
async def get_me(current_user = Depends(get_current_user)):
"""Get current user information"""
return {
"username": current_user.username,
"email": f"{current_user.username}@site11.com",
"is_active": True
}
@app.post("/api/auth/logout")
async def logout(current_user = Depends(get_current_user)):
"""Logout endpoint"""
# In a real application, you might want to blacklist the token
return {"message": "Successfully logged out"}
@app.get("/api/status")
async def system_status():
services_status = {}
@ -65,10 +124,19 @@ async def system_status():
"timestamp": datetime.now().isoformat()
}
# Protected endpoint example
@app.get("/api/protected")
async def protected_route(current_user = Depends(get_current_user)):
"""Example of a protected route"""
return {
"message": "This is a protected route",
"user": current_user.username
}
# API Gateway - Route to Users service
@app.api_route("/api/users/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def proxy_to_users(path: str, request: Request):
"""Proxy requests to Users service"""
async def proxy_to_users(path: str, request: Request, current_user = Depends(get_current_user)):
"""Proxy requests to Users service (protected)"""
try:
async with httpx.AsyncClient() as client:
# Build the target URL

View File

@ -2,4 +2,7 @@ fastapi==0.109.0
uvicorn[standard]==0.27.0
python-dotenv==1.0.0
pydantic==2.5.3
httpx==0.26.0
httpx==0.26.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6

View File

@ -5,9 +5,9 @@ services:
build:
context: ./console/frontend
dockerfile: Dockerfile
container_name: site11_console_frontend
container_name: ${COMPOSE_PROJECT_NAME}_console_frontend
ports:
- "3000:80"
- "${CONSOLE_FRONTEND_PORT}:80"
networks:
- site11_network
restart: unless-stopped
@ -18,13 +18,16 @@ services:
build:
context: ./console/backend
dockerfile: Dockerfile
container_name: site11_console_backend
container_name: ${COMPOSE_PROJECT_NAME}_console_backend
ports:
- "8011:8000"
- "${CONSOLE_BACKEND_PORT}:8000"
environment:
- ENV=development
- ENV=${ENV}
- PORT=8000
- USERS_SERVICE_URL=http://users-backend:8000
- USERS_SERVICE_URL=${USERS_SERVICE_URL}
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
- JWT_ALGORITHM=${JWT_ALGORITHM}
- ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES}
volumes:
- ./console/backend:/app
networks:
@ -37,12 +40,12 @@ services:
build:
context: ./services/users/backend
dockerfile: Dockerfile
container_name: site11_users_backend
container_name: ${COMPOSE_PROJECT_NAME}_users_backend
environment:
- ENV=development
- ENV=${ENV}
- PORT=8000
- MONGODB_URL=mongodb://mongodb:27017
- DB_NAME=users_db
- MONGODB_URL=${MONGODB_URL}
- DB_NAME=${USERS_DB_NAME}
volumes:
- ./services/users/backend:/app
networks:
@ -53,11 +56,11 @@ services:
mongodb:
image: mongo:7.0
container_name: site11_mongodb
container_name: ${COMPOSE_PROJECT_NAME}_mongodb
environment:
- MONGO_INITDB_DATABASE=site11_db
- MONGO_INITDB_DATABASE=${MONGODB_DATABASE}
ports:
- "27017:27017"
- "${MONGODB_PORT}:27017"
volumes:
- mongodb_data:/data/db
- mongodb_config:/data/configdb
@ -72,9 +75,9 @@ services:
redis:
image: redis:7-alpine
container_name: site11_redis
container_name: ${COMPOSE_PROJECT_NAME}_redis
ports:
- "6379:6379"
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
networks:

182
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,182 @@
# Site11 Microservices Architecture
## 시스템 아키텍처 개요
### 메시징 및 데이터 처리 시스템
#### 1. **Apache Kafka** - 통합 메시징 플랫폼
- **역할**: 이벤트 스트리밍 + 작업 큐 + 메시지 버스
- **사용 사례**:
- 서비스 간 이벤트 발행/구독
- 비동기 작업 큐 (Celery 대체)
- 사용자 활동 로그 스트리밍
- 실시간 데이터 파이프라인
- 이벤트 소싱 패턴 구현
- CQRS (Command Query Responsibility Segregation)
- 백그라운드 작업 처리
#### 2. **Redis** - 인메모리 데이터 스토어
- **역할**: 캐싱 및 세션 관리 전용
- **사용 사례**:
- API 응답 캐싱
- 사용자 세션 저장
- Rate limiting
- 실시간 리더보드/카운터
- 임시 데이터 저장
#### 3. **MongoDB** - Document Database
- **역할**: 주요 데이터 영속성
- **사용 사례**:
- 서비스별 도메인 데이터
- 유연한 스키마 관리
- 이벤트 저장소
## 서비스 통신 패턴
### 동기 통신 (REST API)
```
Client → Nginx → Console (API Gateway) → Microservice
```
- 즉각적인 응답이 필요한 경우
- CRUD 작업
- 실시간 데이터 조회
### 비동기 통신 (Kafka Events)
```
Service A → Kafka Topic → Service B, C, D
```
- 서비스 간 느슨한 결합
- 이벤트 기반 아키텍처
- 확장 가능한 처리
### 캐싱 전략 (Redis)
```
Request → Check Redis Cache → Hit? Return : Fetch from DB → Store in Redis → Return
```
- 응답 시간 개선
- 데이터베이스 부하 감소
- 세션 관리
## 이벤트 플로우 예시
### 사용자 등록 플로우
1. **API Request**: Client → Console → Users Service
2. **User Created Event**: Users Service → Kafka
3. **Event Consumers**:
- Statistics Service: 사용자 통계 업데이트
- Email Service: 환영 이메일 발송
- Analytics Service: 가입 분석
4. **Cache Update**: Redis에 사용자 정보 캐싱
### 이미지 업로드 플로우
1. **Upload Request**: Client → Console → Images Service
2. **Image Uploaded Event**: Images Service → Kafka
3. **Event Processing**:
- Thumbnail Service: 썸네일 생성
- ML Service: 이미지 분석
- Statistics Service: 업로드 통계
4. **Job Queue**: Redis/Celery로 백그라운드 처리
## Kafka Topics 구조 (예정)
### Event Topics (이벤트 스트리밍)
```
# User Domain
user.created
user.updated
user.deleted
user.login
# Image Domain
image.uploaded
image.processed
image.deleted
# Application Domain
app.registered
app.updated
app.deployed
# System Events
service.health
service.error
audit.log
```
### Task Queue Topics (작업 큐)
```
# Background Jobs
tasks.email.send
tasks.image.resize
tasks.report.generate
tasks.data.export
tasks.notification.push
# Scheduled Jobs
tasks.cleanup.expired
tasks.backup.database
tasks.analytics.aggregate
```
## Redis 사용 패턴
### 1. 캐싱 계층
- Key: `cache:users:{user_id}`
- TTL: 3600초
- 패턴: Cache-Aside
### 2. 세션 관리
- Key: `session:{token}`
- TTL: 1800초
- 데이터: 사용자 정보, 권한
### 3. Rate Limiting
- Key: `rate_limit:{user_id}:{endpoint}`
- Window: Sliding window
- Limit: 100 requests/minute
### 4. 작업 큐 (Celery)
- Queue: `celery:tasks`
- Priority Queue 지원
- Dead Letter Queue
## 구현 로드맵
### Phase 1 (현재)
- ✅ 기본 서비스 구조
- ✅ MongoDB 연동
- ✅ Redis 설치
- 🔄 JWT 인증
### Phase 2 (Step 6-7)
- Kafka 클러스터 설정
- 기본 Producer/Consumer 구현
- Event Schema 정의
- Redis 캐싱 전략 구현
### Phase 3 (Step 8+)
- Event Sourcing 패턴
- CQRS 구현
- Saga 패턴 (분산 트랜잭션)
- 모니터링 대시보드
## 기술 스택
### 메시징 & 스트리밍
- **Kafka**: Event streaming
- **Redis**: Caching, Queue, Pub/Sub
- **Confluent Schema Registry**: Schema 관리 (향후)
### 백엔드
- **FastAPI**: REST API
- **Celery**: 비동기 작업 처리
- **kafka-python**: Kafka 클라이언트
### 데이터베이스
- **MongoDB**: Document store
- **Redis**: In-memory cache
### 모니터링 (향후)
- **Kafka Manager**: Kafka 클러스터 관리
- **RedisInsight**: Redis 모니터링
- **Prometheus + Grafana**: 메트릭 수집/시각화