Initial microservices setup from artifacts

This commit is contained in:
Urtzi Alfaro
2025-07-17 13:09:24 +02:00
commit 347ff51bd7
200 changed files with 9559 additions and 0 deletions

View File

View File

@@ -0,0 +1,101 @@
"""
Authentication middleware for gateway
"""
import logging
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
import httpx
from typing import Optional
from app.core.config import settings
from shared.auth.jwt_handler import JWTHandler
logger = logging.getLogger(__name__)
# JWT handler
jwt_handler = JWTHandler(settings.JWT_SECRET_KEY, settings.JWT_ALGORITHM)
# Routes that don't require authentication
PUBLIC_ROUTES = [
"/health",
"/metrics",
"/docs",
"/redoc",
"/openapi.json",
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/refresh"
]
async def auth_middleware(request: Request, call_next):
"""Authentication middleware"""
# Check if route requires authentication
if _is_public_route(request.url.path):
return await call_next(request)
# Get token from header
token = _extract_token(request)
if not token:
return JSONResponse(
status_code=401,
content={"detail": "Authentication required"}
)
# Verify token
try:
# First try to verify token locally
payload = jwt_handler.verify_token(token)
if payload:
# Add user info to request state
request.state.user = payload
return await call_next(request)
else:
# Token invalid or expired, verify with auth service
user_info = await _verify_with_auth_service(token)
if user_info:
request.state.user = user_info
return await call_next(request)
else:
return JSONResponse(
status_code=401,
content={"detail": "Invalid or expired token"}
)
except Exception as e:
logger.error(f"Authentication error: {e}")
return JSONResponse(
status_code=401,
content={"detail": "Authentication failed"}
)
def _is_public_route(path: str) -> bool:
"""Check if route is public"""
return any(path.startswith(route) for route in PUBLIC_ROUTES)
def _extract_token(request: Request) -> Optional[str]:
"""Extract JWT token from request"""
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
return auth_header.split(" ")[1]
return None
async def _verify_with_auth_service(token: str) -> Optional[dict]:
"""Verify token with auth service"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.post(
f"{settings.AUTH_SERVICE_URL}/verify",
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code == 200:
return response.json()
else:
return None
except Exception as e:
logger.error(f"Auth service verification failed: {e}")
return None

View File

@@ -0,0 +1,48 @@
"""
Logging middleware for gateway
"""
import logging
import time
from fastapi import Request
import json
logger = logging.getLogger(__name__)
async def logging_middleware(request: Request, call_next):
"""Logging middleware"""
start_time = time.time()
# Log request
logger.info(
f"Request: {request.method} {request.url.path}",
extra={
"method": request.method,
"url": request.url.path,
"query_params": str(request.query_params),
"client_host": request.client.host,
"user_agent": request.headers.get("user-agent", ""),
"request_id": getattr(request.state, 'request_id', None)
}
)
# Process request
response = await call_next(request)
# Calculate duration
duration = time.time() - start_time
# Log response
logger.info(
f"Response: {response.status_code} in {duration:.3f}s",
extra={
"status_code": response.status_code,
"duration": duration,
"method": request.method,
"url": request.url.path,
"request_id": getattr(request.state, 'request_id', None)
}
)
return response

View File

@@ -0,0 +1,85 @@
"""
Rate limiting middleware for gateway
"""
import logging
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
import redis.asyncio as redis
from datetime import datetime, timedelta
import hashlib
from app.core.config import settings
logger = logging.getLogger(__name__)
# Redis client for rate limiting
redis_client = redis.from_url(settings.REDIS_URL)
async def rate_limit_middleware(request: Request, call_next):
"""Rate limiting middleware"""
# Skip rate limiting for health checks
if request.url.path in ["/health", "/metrics"]:
return await call_next(request)
# Get client identifier (IP address or user ID)
client_id = _get_client_id(request)
# Check rate limit
if await _is_rate_limited(client_id):
return JSONResponse(
status_code=429,
content={
"detail": "Rate limit exceeded",
"retry_after": settings.RATE_LIMIT_WINDOW
}
)
# Process request
response = await call_next(request)
# Update rate limit counter
await _update_rate_limit(client_id)
return response
def _get_client_id(request: Request) -> str:
"""Get client identifier for rate limiting"""
# Use user ID if authenticated, otherwise use IP
if hasattr(request.state, 'user') and request.state.user:
return f"user:{request.state.user.get('user_id', 'unknown')}"
else:
# Hash IP address for privacy
ip = request.client.host
return f"ip:{hashlib.md5(ip.encode()).hexdigest()}"
async def _is_rate_limited(client_id: str) -> bool:
"""Check if client is rate limited"""
try:
key = f"rate_limit:{client_id}"
current_count = await redis_client.get(key)
if current_count is None:
return False
return int(current_count) >= settings.RATE_LIMIT_REQUESTS
except Exception as e:
logger.error(f"Rate limit check failed: {e}")
return False
async def _update_rate_limit(client_id: str):
"""Update rate limit counter"""
try:
key = f"rate_limit:{client_id}"
# Increment counter
current_count = await redis_client.incr(key)
# Set TTL on first request
if current_count == 1:
await redis_client.expire(key, settings.RATE_LIMIT_WINDOW)
except Exception as e:
logger.error(f"Rate limit update failed: {e}")