From 5d258bf67fa88d4006543ea801aeb1bc9dc78ee2 Mon Sep 17 00:00:00 2001 From: Bruce Lin Date: Sat, 12 Jul 2025 17:53:01 -0400 Subject: [PATCH] created dummy fastapi --- api/web/Dockerfile | 33 +++++++++++++ api/web/app.py | 71 ++++++++++++++++++++++++++++ api/web/auth_utils.py | 87 ++++++++++++++++++++++++++++++++++ api/web/requirements.txt | 5 ++ api/web/start_webserver.sh | 52 +++++++++++++++++++++ api/web/test_api.sh | 96 ++++++++++++++++++++++++++++++++++++++ api/web/users.py | 34 ++++++++++++++ 7 files changed, 378 insertions(+) create mode 100644 api/web/Dockerfile create mode 100644 api/web/app.py create mode 100644 api/web/auth_utils.py create mode 100644 api/web/requirements.txt create mode 100755 api/web/start_webserver.sh create mode 100755 api/web/test_api.sh create mode 100644 api/web/users.py diff --git a/api/web/Dockerfile b/api/web/Dockerfile new file mode 100644 index 00000000..4f5d6870 --- /dev/null +++ b/api/web/Dockerfile @@ -0,0 +1,33 @@ +# Use Python 3.12 slim image +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create non-root user +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/api/web/app.py b/api/web/app.py new file mode 100644 index 00000000..5f02ddff --- /dev/null +++ b/api/web/app.py @@ -0,0 +1,71 @@ +from datetime import timedelta +from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from fastapi.middleware.cors import CORSMiddleware +from users import Token, User, fake_users_db +from auth_utils import ( + authenticate_user, + create_access_token, + get_current_active_user, + ACCESS_TOKEN_EXPIRE_MINUTES +) + + +app = FastAPI(title="Trading Agents API", version="1.0.0") + +# Configure CORS +origins = [ + "http://localhost", + "http://localhost:3000", + "http://localhost:8000", + "http://localhost:8080", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.post("/token", response_model=Token) +async def login(form_data: OAuth2PasswordRequestForm = Depends()): + """Login endpoint to get access token.""" + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + 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("/users/me", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + """Get current user information.""" + return current_user + + +@app.get("/protected") +async def protected_route(current_user: User = Depends(get_current_active_user)): + """Example protected route.""" + return {"message": f"Hello {current_user.username}, this is a protected route!"} + + +@app.get("/") +async def root(): + """Root endpoint.""" + return {"message": "Trading Agents API with OAuth2"} + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy"} \ No newline at end of file diff --git a/api/web/auth_utils.py b/api/web/auth_utils.py new file mode 100644 index 00000000..b875a46f --- /dev/null +++ b/api/web/auth_utils.py @@ -0,0 +1,87 @@ +from datetime import datetime, timedelta +from typing import Optional +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from passlib.context import CryptContext +from users import UserInDB, TokenData, fake_users_db + + +# Configuration +SECRET_KEY = "your-secret-key-here-change-in-production" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# OAuth2 scheme +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a plain password against its hash.""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Hash a password.""" + return pwd_context.hash(password) + + +def get_user(db: dict, username: str) -> Optional[UserInDB]: + """Get a user from the database.""" + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + return None + + +def authenticate_user(fake_db: dict, username: str, password: str) -> Optional[UserInDB]: + """Authenticate a user.""" + user = get_user(fake_db, username) + if not user: + return None + if not verify_password(password, user.hashed_password): + return None + return user + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create a JWT access token.""" + 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)) -> UserInDB: + """Get the current authenticated user from JWT token.""" + 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 + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user(current_user: UserInDB = Depends(get_current_user)) -> UserInDB: + """Get the current active user.""" + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user \ No newline at end of file diff --git a/api/web/requirements.txt b/api/web/requirements.txt new file mode 100644 index 00000000..fbc5346a --- /dev/null +++ b/api/web/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.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/api/web/start_webserver.sh b/api/web/start_webserver.sh new file mode 100755 index 00000000..2aebd35a --- /dev/null +++ b/api/web/start_webserver.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Script to build and run the FastAPI web server using Docker + +# Set variables +IMAGE_NAME="tradingagents-api-web" +CONTAINER_NAME="tradingagents-api-web-container" +PORT=8000 + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Building Docker image...${NC}" +docker build -t ${IMAGE_NAME} . + +if [ $? -eq 0 ]; then + echo -e "${GREEN}Docker image built successfully!${NC}" +else + echo -e "${RED}Failed to build Docker image${NC}" + exit 1 +fi + +# Stop and remove existing container if it exists +echo -e "${YELLOW}Checking for existing container...${NC}" +if [ "$(docker ps -aq -f name=${CONTAINER_NAME})" ]; then + echo -e "${YELLOW}Stopping and removing existing container...${NC}" + docker stop ${CONTAINER_NAME} + docker rm ${CONTAINER_NAME} +fi + +# Run the container +echo -e "${YELLOW}Starting container...${NC}" +docker run -d \ + --name ${CONTAINER_NAME} \ + -p ${PORT}:8000 \ + -e SECRET_KEY="${SECRET_KEY:-your-secret-key-here-change-in-production}" \ + ${IMAGE_NAME} + +if [ $? -eq 0 ]; then + echo -e "${GREEN}Container started successfully!${NC}" + echo -e "${GREEN}API is available at: http://localhost:${PORT}${NC}" + echo -e "${GREEN}API documentation: http://localhost:${PORT}/docs${NC}" + echo "" + echo -e "${YELLOW}To view logs:${NC} docker logs -f ${CONTAINER_NAME}" + echo -e "${YELLOW}To stop:${NC} docker stop ${CONTAINER_NAME}" +else + echo -e "${RED}Failed to start container${NC}" + exit 1 +fi \ No newline at end of file diff --git a/api/web/test_api.sh b/api/web/test_api.sh new file mode 100755 index 00000000..e2c22a96 --- /dev/null +++ b/api/web/test_api.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Test commands for the Trading Agents API + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +API_URL="http://localhost:8000" + +echo -e "${BLUE}=== Trading Agents API Test Commands ===${NC}\n" + +# 1. Health check +echo -e "${YELLOW}1. Health Check:${NC}" +echo "curl -X GET ${API_URL}/health" +echo -e "${GREEN}Expected: {\"status\":\"healthy\"}${NC}\n" + +# 2. Root endpoint +echo -e "${YELLOW}2. Root Endpoint:${NC}" +echo "curl -X GET ${API_URL}/" +echo -e "${GREEN}Expected: {\"message\":\"Trading Agents API with OAuth2\"}${NC}\n" + +# 3. Get token (login) +echo -e "${YELLOW}3. Login (Get Access Token):${NC}" +echo "curl -X POST ${API_URL}/token \\" +echo " -H \"Content-Type: application/x-www-form-urlencoded\" \\" +echo " -d \"username=testuser&password=secret\"" +echo -e "${GREEN}Expected: {\"access_token\":\"\",\"token_type\":\"bearer\"}${NC}\n" + +# 4. Access protected endpoint without token (should fail) +echo -e "${YELLOW}4. Access Protected Route (No Token - Should Fail):${NC}" +echo "curl -X GET ${API_URL}/protected" +echo -e "${RED}Expected: 401 Unauthorized${NC}\n" + +# 5. Access protected endpoint with token +echo -e "${YELLOW}5. Access Protected Route (With Token):${NC}" +echo "# First get the token:" +echo "TOKEN=\$(curl -s -X POST ${API_URL}/token \\" +echo " -H \"Content-Type: application/x-www-form-urlencoded\" \\" +echo " -d \"username=testuser&password=secret\" | jq -r '.access_token')" +echo "" +echo "# Then use it:" +echo "curl -X GET ${API_URL}/protected \\" +echo " -H \"Authorization: Bearer \$TOKEN\"" +echo -e "${GREEN}Expected: {\"message\":\"Hello testuser, this is a protected route!\"}${NC}\n" + +# 6. Get current user info +echo -e "${YELLOW}6. Get Current User Info:${NC}" +echo "curl -X GET ${API_URL}/users/me \\" +echo " -H \"Authorization: Bearer \$TOKEN\"" +echo -e "${GREEN}Expected: User information JSON${NC}\n" + +# 7. View API documentation +echo -e "${YELLOW}7. API Documentation:${NC}" +echo "Open in browser: ${API_URL}/docs" +echo "or" +echo "curl -X GET ${API_URL}/openapi.json | jq ." +echo "" + +# One-liner test script +echo -e "${BLUE}=== Quick Test Script ===${NC}" +echo -e "${YELLOW}Run this to test all endpoints:${NC}\n" +cat << 'EOF' +#!/bin/bash +API_URL="http://localhost:8000" + +# Test health +echo "Testing health endpoint..." +curl -s ${API_URL}/health | jq . + +# Get token +echo -e "\nGetting access token..." +TOKEN=$(curl -s -X POST ${API_URL}/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=testuser&password=secret" | jq -r '.access_token') + +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + echo "Failed to get token!" + exit 1 +fi + +echo "Token obtained successfully!" + +# Test protected endpoint +echo -e "\nTesting protected endpoint..." +curl -s -X GET ${API_URL}/protected \ + -H "Authorization: Bearer $TOKEN" | jq . + +# Get user info +echo -e "\nGetting user info..." +curl -s -X GET ${API_URL}/users/me \ + -H "Authorization: Bearer $TOKEN" | jq . +EOF \ No newline at end of file diff --git a/api/web/users.py b/api/web/users.py new file mode 100644 index 00000000..9407fc4e --- /dev/null +++ b/api/web/users.py @@ -0,0 +1,34 @@ +from typing import Optional +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Optional[str] = None + + +class User(BaseModel): + username: str + email: Optional[str] = None + full_name: Optional[str] = None + disabled: Optional[bool] = None + + +class UserInDB(User): + hashed_password: str + + +# Fake database for demonstration +fake_users_db = { + "testuser": { + "username": "testuser", + "full_name": "Test User", + "email": "test@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", # secret + "disabled": False, + } +} \ No newline at end of file