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()