From 315eeea2aee18984c1f461a3fff8b2cb72b4ccfa Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Wed, 10 Sep 2025 16:36:47 +0900 Subject: [PATCH] Step 5: Authentication System & Environment Variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.example | 37 +++++++ console/backend/auth.py | 65 +++++++++++ console/backend/main.py | 76 ++++++++++++- console/backend/requirements.txt | 5 +- docker-compose.yml | 33 +++--- docs/ARCHITECTURE.md | 182 +++++++++++++++++++++++++++++++ 6 files changed, 378 insertions(+), 20 deletions(-) create mode 100644 .env.example create mode 100644 console/backend/auth.py create mode 100644 docs/ARCHITECTURE.md 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