backend / app /main.py
bastienp's picture
feat(security): add an API-Key Mechanism
d60934b
raw
history blame
4.88 kB
from fastapi import FastAPI, Request, HTTPException, Security, Depends
from fastapi.middleware.cors import CORSMiddleware
from app.routes import health, wagons, chat, players, generate
from app.core.logging import get_logger, setup_logging
from dotenv import load_dotenv
from datetime import datetime
import time
from pathlib import Path
from fastapi.security.api_key import APIKeyHeader
import os
# Load environment variables
load_dotenv()
# Setup logging
logger = get_logger("main")
API_KEY = os.getenv("API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=True)
async def get_api_key(api_key_header: str = Security(api_key_header)):
if not API_KEY:
raise HTTPException(status_code=500, detail="API key not configured on server")
if api_key_header != API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
return api_key_header
app = FastAPI(
title="Game Jam API",
description="API for Game Jam Hackathon",
version="1.0.0",
dependencies=[Depends(get_api_key)],
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"],
allow_headers=["*"],
expose_headers=[
"Location",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Credentials",
"Access-Control-Expose-Headers",
],
max_age=3600,
)
@app.middleware("http")
async def handle_redirects(request: Request, call_next):
"""Ensure CORS headers are in redirect responses and force https in the 'Location' header."""
response = await call_next(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = (
"GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH"
)
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Max-Age"] = "3600"
if response.status_code in [301, 302, 307, 308]:
response.headers["Access-Control-Expose-Headers"] = "Location"
if "Location" in response.headers:
location = response.headers["Location"]
if location.startswith("http://"):
response.headers["Location"] = location.replace(
"http://", "https://", 1
)
return response
@app.middleware("http")
async def security_headers(request: Request, call_next):
"""Add security-related headers to all responses."""
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN" # More permissive than DENY
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Content-Security-Policy"] = (
"default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src *"
)
response.headers["Referrer-Policy"] = "no-referrer-when-downgrade"
return response
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Middleware to log all requests and responses."""
start_time = time.time()
logger.info(
"Incoming request",
extra={
"method": request.method,
"url": str(request.url),
"client_host": request.client.host if request.client else None,
"timestamp": datetime.utcnow().isoformat(),
},
)
try:
response = await call_next(request)
process_time = time.time() - start_time
logger.info(
"Request completed",
extra={
"method": request.method,
"url": str(request.url),
"status_code": response.status_code,
"process_time_ms": round(process_time * 1000, 2),
},
)
return response
except Exception as e:
logger.error(
"Request failed",
extra={"method": request.method, "url": str(request.url), "error": str(e)},
)
raise
app.include_router(health.router)
app.include_router(wagons.router)
app.include_router(chat.router)
app.include_router(players.router)
app.include_router(generate.router)
@app.get("/")
async def root():
logger.info("Root endpoint accessed")
return {
"message": "Welcome to Game Jam API",
"docs_url": "/docs",
"health_check": "/health",
"wagons_endpoint": "/api/wagons",
"chat_endpoint": "/api/chat",
"players_endpoint": "/api/players",
}
@app.on_event("startup")
async def startup_event():
logs_dir = Path("logs")
logs_dir.mkdir(exist_ok=True)
setup_logging()