Spaces:
Sleeping
Sleeping
File size: 5,624 Bytes
80a4a65 3c7b4e4 80a4a65 3c7b4e4 80a4a65 3c7b4e4 80a4a65 3c7b4e4 80a4a65 3c7b4e4 80a4a65 3c7b4e4 80a4a65 3c7b4e4 80a4a65 7b6762c a6e0de1 338517d 0b3bc37 05a9e8e 80a4a65 7b6762c a6e0de1 338517d 0b3bc37 05a9e8e 80a4a65 3c7b4e4 80a4a65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | """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"]
|