From 7559c4c5a8dede28c6dd6c5373caa30c38a26a8c Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Wed, 10 Sep 2025 16:09:36 +0900 Subject: [PATCH] Step 2: Add Users microservice with API Gateway routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created Users service with full CRUD operations - Updated Console to act as API Gateway for Users service - Implemented service-to-service communication - Added service health monitoring in Console - Docker Compose now manages both services Services running: - Console (API Gateway): http://localhost:8011 - Users service: Internal network only Test endpoints: - Status: curl http://localhost:8011/api/status - Users: curl http://localhost:8011/api/users/users 🤖 Generated with Claude Code Co-Authored-By: Claude --- console/backend/main.py | 71 +++++++++++-- docker-compose.yml | 17 ++++ docs/PROGRESS.md | 9 +- services/users/backend/Dockerfile | 21 ++++ services/users/backend/main.py | 127 ++++++++++++++++++++++++ services/users/backend/requirements.txt | 3 + 6 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 services/users/backend/Dockerfile create mode 100644 services/users/backend/main.py create mode 100644 services/users/backend/requirements.txt diff --git a/console/backend/main.py b/console/backend/main.py index f832901..8bc3c12 100644 --- a/console/backend/main.py +++ b/console/backend/main.py @@ -1,7 +1,10 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware import uvicorn from datetime import datetime +import httpx +import os +from typing import Any app = FastAPI( title="Console API Gateway", @@ -9,6 +12,9 @@ app = FastAPI( version="0.1.0" ) +# Service URLs from environment +USERS_SERVICE_URL = os.getenv("USERS_SERVICE_URL", "http://users-backend:8000") + # CORS middleware app.add_middleware( CORSMiddleware, @@ -36,19 +42,66 @@ async def health_check(): @app.get("/api/status") async def system_status(): + services_status = {} + + # Check Users service + try: + async with httpx.AsyncClient() as client: + response = await client.get(f"{USERS_SERVICE_URL}/health", timeout=2.0) + services_status["users"] = "online" if response.status_code == 200 else "error" + except: + services_status["users"] = "offline" + + # Other services (not yet implemented) + services_status["oauth"] = "pending" + services_status["images"] = "pending" + services_status["applications"] = "pending" + services_status["data"] = "pending" + services_status["statistics"] = "pending" + return { "console": "online", - "services": { - "users": "pending", - "oauth": "pending", - "images": "pending", - "applications": "pending", - "data": "pending", - "statistics": "pending" - }, + "services": services_status, "timestamp": datetime.now().isoformat() } +# 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""" + try: + async with httpx.AsyncClient() as client: + # Build the target URL + url = f"{USERS_SERVICE_URL}/{path}" + + # Get request body if exists + body = None + if request.method in ["POST", "PUT", "PATCH"]: + body = await request.body() + + # Forward the request + response = await client.request( + method=request.method, + url=url, + headers={ + key: value for key, value in request.headers.items() + if key.lower() not in ["host", "content-length"] + }, + content=body, + params=request.query_params + ) + + # Return the response + return Response( + content=response.content, + status_code=response.status_code, + headers=dict(response.headers) + ) + except httpx.ConnectError: + raise HTTPException(status_code=503, detail="Users service unavailable") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + if __name__ == "__main__": uvicorn.run( "main:app", diff --git a/docker-compose.yml b/docker-compose.yml index b1e4bf5..9b47d71 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,11 +11,28 @@ services: environment: - ENV=development - PORT=8000 + - USERS_SERVICE_URL=http://users-backend:8000 volumes: - ./console/backend:/app networks: - site11_network restart: unless-stopped + depends_on: + - users-backend + + users-backend: + build: + context: ./services/users/backend + dockerfile: Dockerfile + container_name: site11_users_backend + environment: + - ENV=development + - PORT=8000 + volumes: + - ./services/users/backend:/app + networks: + - site11_network + restart: unless-stopped networks: site11_network: diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 3d35873..0e49369 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -5,8 +5,8 @@ ## Current Status - **Date Started**: 2025-09-09 -- **Current Phase**: Step 1 Complete ✅ -- **Next Action**: Step 2 - Add First Service (Users) +- **Current Phase**: Step 2 Complete ✅ +- **Next Action**: Step 3 - Database Integration (MongoDB) ## Completed Checkpoints ✅ Project structure planning (CLAUDE.md) @@ -16,6 +16,11 @@ - docker-compose.yml created - console/backend with FastAPI - Running on port 8011 +✅ Step 2: Add First Service (Users) + - Users service with CRUD operations + - Console API Gateway routing to Users + - Service communication verified + - Test: curl http://localhost:8011/api/users/users ## Active Working Files ``` diff --git a/services/users/backend/Dockerfile b/services/users/backend/Dockerfile new file mode 100644 index 0000000..2515968 --- /dev/null +++ b/services/users/backend/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["python", "main.py"] \ No newline at end of file diff --git a/services/users/backend/main.py b/services/users/backend/main.py new file mode 100644 index 0000000..652d2ef --- /dev/null +++ b/services/users/backend/main.py @@ -0,0 +1,127 @@ +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 + +app = FastAPI( + title="Users Service", + description="User management microservice", + version="0.1.0" +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# In-memory storage for now (will be replaced with MongoDB later) +users_db = {} +user_id_counter = 1 + +# Pydantic models +class UserCreate(BaseModel): + username: str + email: str + full_name: Optional[str] = None + +class UserUpdate(BaseModel): + username: Optional[str] = None + email: Optional[str] = None + full_name: Optional[str] = None + +class User(BaseModel): + id: int + username: str + email: str + full_name: Optional[str] = None + created_at: datetime + updated_at: datetime + +# 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[User]) +async def get_users(): + return list(users_db.values()) + +@app.get("/users/{user_id}", response_model=User) +async def get_user(user_id: int): + if user_id not in users_db: + raise HTTPException(status_code=404, detail="User not found") + return users_db[user_id] + +@app.post("/users", response_model=User, status_code=201) +async def create_user(user: UserCreate): + global user_id_counter + + # Check if username already exists + for existing_user in users_db.values(): + if existing_user["username"] == user.username: + raise HTTPException(status_code=400, detail="Username already exists") + + new_user = { + "id": user_id_counter, + "username": user.username, + "email": user.email, + "full_name": user.full_name, + "created_at": datetime.now(), + "updated_at": datetime.now() + } + + users_db[user_id_counter] = new_user + user_id_counter += 1 + + return new_user + +@app.put("/users/{user_id}", response_model=User) +async def update_user(user_id: int, user_update: UserUpdate): + if user_id not in users_db: + raise HTTPException(status_code=404, detail="User not found") + + user = users_db[user_id] + + if user_update.username is not None: + # Check if new username already exists + for uid, existing_user in users_db.items(): + if uid != user_id and existing_user["username"] == user_update.username: + 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 + + user["updated_at"] = datetime.now() + + return user + +@app.delete("/users/{user_id}") +async def delete_user(user_id: int): + if user_id not in users_db: + raise HTTPException(status_code=404, detail="User not found") + + del users_db[user_id] + return {"message": "User deleted successfully"} + +if __name__ == "__main__": + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=True + ) \ No newline at end of file diff --git a/services/users/backend/requirements.txt b/services/users/backend/requirements.txt new file mode 100644 index 0000000..7da3bfd --- /dev/null +++ b/services/users/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +pydantic==2.5.3 \ No newline at end of file