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