random-access/src/random_access/main.py

159 lines
5.2 KiB
Python

"""Random Access API main entry point."""
import asyncio
import logging
from collections import namedtuple
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from random_access.database import (
airtable_write_worker,
get_airtable_base,
get_table,
validate_all_schemas,
)
from random_access.routes.auth import create_auth_router
from random_access.routes.items import create_items_router, create_user_items_router
from random_access.routes.system import create_system_router
from random_access.routes.users import create_users_router
from random_access.security import SecurityHeaders, get_client_ip
from random_access.settings import settings
from random_access.slack_integration import create_slack_app, setup_slack_handlers
Result = namedtuple("Result", "content, status")
logger = logging.getLogger("uvicorn.error")
# Initialize rate limiter
limiter = Limiter(key_func=get_client_ip)
# Initialize Airtable
at_base = get_airtable_base()
SUBMISSIONS = get_table(at_base, "submissions")
USERS = get_table(at_base, "users")
SESSIONS = get_table(at_base, "sessions")
ITEMS = get_table(at_base, "items")
ITEM_ADDONS = get_table(at_base, "item_addons")
ITEM_INSTANCES = get_table(at_base, "item_instances")
slack = create_slack_app()
setup_slack_handlers(slack)
slack_handler = AsyncSlackRequestHandler(slack)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager."""
# Validate table schemas on startup
logger.info("Validating Airtable schemas...")
schema_valid = await validate_all_schemas()
if not schema_valid:
if settings.is_production:
logger.error("Schema validation failed in production - startup aborted")
raise RuntimeError("Table schema validation failed")
else:
logger.warning(
"Schema validation failed in development - continuing anyway"
)
# Start the write worker
asyncio.create_task(airtable_write_worker())
yield
app = FastAPI(
title="Random Access API",
version="0.1.0",
lifespan=lifespan,
description="API for Random Access game integration",
)
# Security middleware
app.state.limiter = limiter
# Custom rate limit exception handler
@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
"""Custom rate limit exceeded handler."""
return JSONResponse(
status_code=429,
content={"detail": f"Rate limit exceeded: {exc.detail}"},
headers={
"X-RateLimit-Limit": str(
getattr(exc, "limit", settings.rate_limit_requests)
),
"Retry-After": "60", # Default retry after 60 seconds
},
)
app.add_middleware(SlowAPIMiddleware)
# CORS middleware - allows all origins for game compatibility but with secure settings
app.add_middleware(
CORSMiddleware,
allow_origins=settings.origins_list, # ["*"] for development, specific domains for production
allow_credentials=False, # Don't allow credentials with wildcards for security
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"],
max_age=3600, # Cache preflight requests for 1 hour
)
# Security headers middleware
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
"""Add security headers to all responses."""
response = await call_next(request)
# Add security headers
security_headers = SecurityHeaders.get_security_headers()
for header, value in security_headers.items():
response.headers[header] = value
# Add rate limit headers
response.headers["X-Content-Security-Policy"] = "default-src 'self'"
return response
# Request size limit middleware
@app.middleware("http")
async def limit_request_size(request: Request, call_next):
"""Limit request body size to prevent large payload attacks."""
content_length = request.headers.get("content-length")
if content_length:
content_length = int(content_length)
if content_length > settings.max_request_size:
from fastapi import HTTPException, status
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Request too large. Maximum size: {settings.max_request_size} bytes",
)
response = await call_next(request)
return response
# Include routers
routers = [
create_auth_router(SESSIONS, USERS, SUBMISSIONS, slack),
create_users_router(SESSIONS, USERS),
create_items_router(SESSIONS, USERS, ITEMS, ITEM_ADDONS, ITEM_INSTANCES),
create_user_items_router(SESSIONS, USERS, ITEMS, ITEM_ADDONS, ITEM_INSTANCES),
create_system_router(slack_handler),
]
for router in routers:
app.include_router(router)