File size: 6,112 Bytes
d60934b 5873878 d60934b a5399cd d60934b 5873878 d60934b a5399cd d60934b a5399cd d60934b 5873878 d60934b 5873878 d60934b 5873878 d60934b 5873878 d60934b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
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
import secrets
# 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)
# Add this near the top with other environment variables
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000") # Add default for local development
async def get_api_key(api_key_header: str = Security(api_key_header)):
if not API_KEY:
logger.error("API key not configured on server")
raise HTTPException(
status_code=500,
detail="Server configuration error" # Don't expose specific details
)
if not api_key_header:
raise HTTPException(
status_code=401,
detail="Missing API key",
headers={"WWW-Authenticate": "ApiKey"},
)
if not secrets.compare_digest(api_key_header, API_KEY): # Constant-time comparison
logger.warning(f"Invalid API key attempt from {request.client.host}")
raise HTTPException(
status_code=403,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "ApiKey"},
)
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=[FRONTEND_URL], # Replace "*" with specific frontend URL
allow_credentials=True, # Changed to True since we're restricting origins
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"] = FRONTEND_URL
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"] = "DENY" # Stricter than SAMEORIGIN
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"connect-src 'self' "
)
response.headers["Permissions-Policy"] = (
"accelerometer=(), camera=(), geolocation=(), gyroscope=(), "
"magnetometer=(), microphone=(), payment=(), usb=()"
)
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
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()
|