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