CyberArena / app /main.py
Hussien Haider
H
05a9e8e
Raw
History Blame Contribute Delete
5.62 kB
"""FastAPI application entry point.
This module builds the ``app`` object that :mod:`main` (the 5-line
top-level entry) imports and feeds to uvicorn. All real logic lives
in the subpackages — this file is the wiring diagram.
"""
# IMPORTANT: load .env BEFORE anything else in the package. This way
# `python main.py`, `uvicorn app.main:app`, `uvicorn main:app`, or
# any script that imports `app.main` will all pick up the same
# environment, regardless of cwd.
from app._env import load_app_env, assert_critical_env # noqa: E402
load_app_env()
import asyncio
import os
import sys
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from app.generators import REGISTRY as GENERATOR_REGISTRY
# Force UTF-8 in stdout (Windows Arabic)
try:
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except Exception:
pass
# --------------------------------------------------------------------------- #
# App + middleware #
# --------------------------------------------------------------------------- #
# Rate limiter - 100 requests per minute per IP for general endpoints
# Stricter limits for sensitive endpoints
limiter = Limiter(key_func=get_remote_address)
app = FastAPI(title="CyberArena Backend")
# Add rate limiter to app
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"https://alpha-team-cyberarena.hf.space",
],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
# Security headers middleware
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
# --------------------------------------------------------------------------- #
# Routers #
# --------------------------------------------------------------------------- #
from app.api import ( # noqa: E402 (import after middleware so order is right)
auth as _auth,
xp as _xp,
leaderboard as _leaderboard,
certificates as _certificates,
training as _training,
terminal as _terminal,
onevone as _onevone,
profile as _profile,
mentor as _mentor,
chatbot as _chatbot,
daily_challenge as _daily_challenge,
ai_recommend as _ai_recommend,
)
def _include_all_routers(app: FastAPI) -> None:
app.include_router(_auth.router)
app.include_router(_xp.router)
app.include_router(_leaderboard.router)
app.include_router(_certificates.router)
app.include_router(_training.router)
app.include_router(_terminal.router)
app.include_router(_onevone.router)
app.include_router(_profile.router)
app.include_router(_mentor.router)
app.include_router(_chatbot.router)
app.include_router(_daily_challenge.router)
app.include_router(_ai_recommend.router)
_include_all_routers(app)
# --------------------------------------------------------------------------- #
# Background pool watcher #
# --------------------------------------------------------------------------- #
async def populate_pool_background() -> None:
"""Spawn one ``start_pool_watcher(team)`` task per (type, team).
See AGENTS.md "Pool Architecture" for the per-team registration
model. The five registered generators come from
:data:`app.generators.REGISTRY`.
"""
await asyncio.sleep(8) # Gentle wait on startup
print("Background pool watcher orchestrator started.")
print(f"[main] Active generators: {[name for name, _, _ in GENERATOR_REGISTRY]}")
for name, gen, teams in GENERATOR_REGISTRY:
for team in teams:
asyncio.create_task(gen.start_pool_watcher(team))
# Keep the orchestrator alive
while True:
await asyncio.sleep(3600)
@app.on_event("startup")
async def startup_event():
# Make sure the backend dir is on sys.path so generators (which are
# self-contained) and their relative imports keep working.
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
asyncio.create_task(populate_pool_background())
# --------------------------------------------------------------------------- #
# Environment validation #
# --------------------------------------------------------------------------- #
# Run this as soon as a worker imports the module. It only fires once
# (see app/_env.py::_is_loaded_marker_set) and gives a clear, single-line
# error instead of a 401 from Supabase ten seconds later.
assert_critical_env("SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_JWT_SECRET")
__all__ = ["app", "populate_pool_background"]