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