Files
chat/services/api_gateway/main.py
2025-09-25 08:05:25 +09:00

295 lines
9.6 KiB
Python

from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import httpx
import time
from typing import Dict
from shared.config import settings
import asyncio
app = FastAPI(title="API Gateway", version="1.0.0")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Service registry
SERVICES = {
"users": "http://localhost:8001",
"emergency": "http://localhost:8002",
"location": "http://localhost:8003",
"calendar": "http://localhost:8004",
"notifications": "http://localhost:8005"
}
# Rate limiting (simple in-memory implementation)
request_counts: Dict[str, Dict[str, int]] = {}
RATE_LIMIT_REQUESTS = 100 # requests per minute
RATE_LIMIT_WINDOW = 60 # seconds
def get_client_ip(request: Request) -> str:
"""Get client IP address"""
x_forwarded_for = request.headers.get("X-Forwarded-For")
if x_forwarded_for:
return x_forwarded_for.split(",")[0].strip()
return request.client.host
def is_rate_limited(client_ip: str) -> bool:
"""Check if client is rate limited"""
current_time = int(time.time())
window_start = current_time - RATE_LIMIT_WINDOW
if client_ip not in request_counts:
request_counts[client_ip] = {}
# Clean old entries
request_counts[client_ip] = {
timestamp: count for timestamp, count in request_counts[client_ip].items()
if int(timestamp) > window_start
}
# Count requests in current window
total_requests = sum(request_counts[client_ip].values())
if total_requests >= RATE_LIMIT_REQUESTS:
return True
# Add current request
timestamp_key = str(current_time)
request_counts[client_ip][timestamp_key] = request_counts[client_ip].get(timestamp_key, 0) + 1
return False
async def proxy_request(service_url: str, path: str, method: str, headers: dict, body: bytes = None, params: dict = None):
"""Proxy request to microservice"""
url = f"{service_url}{path}"
# Remove hop-by-hop headers
filtered_headers = {
k: v for k, v in headers.items()
if k.lower() not in ["host", "connection", "upgrade", "proxy-connection",
"proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding"]
}
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.request(
method=method,
url=url,
headers=filtered_headers,
content=body,
params=params
)
return response
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Service timeout")
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="Service unavailable")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Proxy error: {str(e)}")
@app.middleware("http")
async def rate_limiting_middleware(request: Request, call_next):
"""Rate limiting middleware"""
client_ip = get_client_ip(request)
# Skip rate limiting for health checks
if request.url.path.endswith("/health"):
return await call_next(request)
if is_rate_limited(client_ip):
return JSONResponse(
status_code=429,
content={"detail": "Rate limit exceeded"}
)
return await call_next(request)
# User Service routes
@app.api_route("/api/v1/register", methods=["POST"])
@app.api_route("/api/v1/login", methods=["POST"])
@app.api_route("/api/v1/profile", methods=["GET", "PUT"])
async def user_service_proxy(request: Request):
"""Proxy requests to User Service"""
body = await request.body()
response = await proxy_request(
SERVICES["users"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params)
)
return JSONResponse(
status_code=response.status_code,
content=response.json(),
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
)
# Emergency Service routes
@app.api_route("/api/v1/alert", methods=["POST"])
@app.api_route("/api/v1/alert/{alert_id}/respond", methods=["POST"])
@app.api_route("/api/v1/alert/{alert_id}/resolve", methods=["PUT"])
@app.api_route("/api/v1/alerts/my", methods=["GET"])
@app.api_route("/api/v1/alerts/active", methods=["GET"])
async def emergency_service_proxy(request: Request):
"""Proxy requests to Emergency Service"""
body = await request.body()
response = await proxy_request(
SERVICES["emergency"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params)
)
return JSONResponse(
status_code=response.status_code,
content=response.json(),
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
)
# Location Service routes
@app.api_route("/api/v1/update-location", methods=["POST"])
@app.api_route("/api/v1/user-location/{user_id}", methods=["GET"])
@app.api_route("/api/v1/nearby-users", methods=["GET"])
@app.api_route("/api/v1/location-history", methods=["GET"])
@app.api_route("/api/v1/location", methods=["DELETE"])
async def location_service_proxy(request: Request):
"""Proxy requests to Location Service"""
body = await request.body()
response = await proxy_request(
SERVICES["location"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params)
)
return JSONResponse(
status_code=response.status_code,
content=response.json(),
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
)
# Calendar Service routes
@app.api_route("/api/v1/entries", methods=["GET", "POST"])
@app.api_route("/api/v1/entries/{entry_id}", methods=["DELETE"])
@app.api_route("/api/v1/cycle-overview", methods=["GET"])
@app.api_route("/api/v1/insights", methods=["GET"])
async def calendar_service_proxy(request: Request):
"""Proxy requests to Calendar Service"""
body = await request.body()
response = await proxy_request(
SERVICES["calendar"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params)
)
return JSONResponse(
status_code=response.status_code,
content=response.json(),
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
)
# Notification Service routes
@app.api_route("/api/v1/register-device", methods=["POST"])
@app.api_route("/api/v1/send-notification", methods=["POST"])
@app.api_route("/api/v1/device-token", methods=["DELETE"])
@app.api_route("/api/v1/my-devices", methods=["GET"])
async def notification_service_proxy(request: Request):
"""Proxy requests to Notification Service"""
body = await request.body()
response = await proxy_request(
SERVICES["notifications"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params)
)
return JSONResponse(
status_code=response.status_code,
content=response.json(),
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
)
@app.get("/api/v1/health")
async def gateway_health_check():
"""Gateway health check"""
return {"status": "healthy", "service": "api-gateway"}
@app.get("/api/v1/services-status")
async def check_services_status():
"""Check status of all microservices"""
service_status = {}
async def check_service(name: str, url: str):
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"{url}/api/v1/health")
service_status[name] = {
"status": "healthy" if response.status_code == 200 else "unhealthy",
"response_time_ms": response.elapsed.total_seconds() * 1000,
"url": url
}
except Exception as e:
service_status[name] = {
"status": "unhealthy",
"error": str(e),
"url": url
}
# Check all services concurrently
tasks = [check_service(name, url) for name, url in SERVICES.items()]
await asyncio.gather(*tasks)
all_healthy = all(status["status"] == "healthy" for status in service_status.values())
return {
"gateway_status": "healthy",
"all_services_healthy": all_healthy,
"services": service_status
}
@app.get("/")
async def root():
"""Root endpoint with API information"""
return {
"service": "Women Safety App API Gateway",
"version": "1.0.0",
"status": "running",
"endpoints": {
"auth": "/api/v1/register, /api/v1/login",
"profile": "/api/v1/profile",
"emergency": "/api/v1/alert, /api/v1/alerts/*",
"location": "/api/v1/update-location, /api/v1/nearby-users",
"calendar": "/api/v1/entries, /api/v1/cycle-overview",
"notifications": "/api/v1/register-device, /api/v1/send-notification"
},
"docs": "/docs"
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)