159 lines
5.2 KiB
Python
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)
|