Spaces:
Sleeping
Sleeping
| """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 | |
| 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) | |
| 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"] | |