diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f8a163f --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/console/backend/auth.py b/console/backend/auth.py new file mode 100644 index 0000000..2225944 --- /dev/null +++ b/console/backend/auth.py @@ -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 \ No newline at end of file diff --git a/console/backend/main.py b/console/backend/main.py index 8bc3c12..95244c0 100644 --- a/console/backend/main.py +++ b/console/backend/main.py @@ -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 diff --git a/console/backend/requirements.txt b/console/backend/requirements.txt index 098afdc..2d35a3a 100644 --- a/console/backend/requirements.txt +++ b/console/backend/requirements.txt @@ -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 \ No newline at end of file +httpx==0.26.0 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 14ee06f..581b71b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..e152c63 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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**: 메트릭 수집/시각화 \ No newline at end of file