2026-04-10 00:06:30 +00:00

175 lines
5.8 KiB
Python

import os
from contextlib import asynccontextmanager
from urllib.parse import urlparse
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.admin_role_service import bootstrap_super_admin
from app.admin_router import router as admin_router
from app.routers.auth import router as auth_router
from app.routers.broker import router as broker_router
from app.routers.health import router as health_router
from app.routers.paper import router as paper_router
from app.routers.password_reset import router as password_reset_router
from app.routers.strategy import router as strategy_router
from app.routers.support_ticket import router as support_ticket_router
from app.routers.system import router as system_router
from app.routers.zerodha import router as zerodha_router, public_router as zerodha_public_router
from app.services.db import _db_config as _validate_db_config
from app.services.live_equity_service import start_live_equity_snapshot_daemon
from app.services.strategy_service import init_log_state, resume_running_runs
from market import router as market_router
from paper_mtm import router as paper_mtm_router
DEFAULT_PRODUCTION_ORIGINS = {"https://app.quantfortune.com"}
DEFAULT_DEV_ORIGINS = {
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:5173",
"http://127.0.0.1:5173",
"https://app.quantfortune.com"
}
PRODUCTION_ENV_NAMES = {"prod", "production"}
def _environment_name() -> str:
return (
os.getenv("APP_ENV")
or os.getenv("ENVIRONMENT")
or os.getenv("FASTAPI_ENV")
or "development"
).strip().lower()
def _normalize_origin(origin: str) -> str:
return origin.strip().rstrip("/")
def _is_dev_origin(origin: str) -> bool:
parsed = urlparse(origin)
return parsed.scheme == "http" and parsed.hostname in {"localhost", "127.0.0.1"}
def _validate_cors_origin(origin: str) -> str:
normalized = _normalize_origin(origin)
if not normalized:
raise RuntimeError("Empty CORS origin is not allowed")
if normalized in DEFAULT_PRODUCTION_ORIGINS or _is_dev_origin(normalized):
return normalized
raise RuntimeError(
f"Unsupported CORS origin '{normalized}'. Only app.quantfortune.com and localhost dev origins are allowed."
)
def _build_cors_origins() -> list[str]:
configured = [
_normalize_origin(origin)
for origin in os.getenv("CORS_ORIGINS", "").split(",")
if origin.strip()
]
env_name = _environment_name()
if env_name in PRODUCTION_ENV_NAMES:
if not configured:
raise RuntimeError("CORS_ORIGINS must be configured explicitly in production")
origins = configured
else:
origins = configured or sorted(DEFAULT_DEV_ORIGINS)
deduped: list[str] = []
seen: set[str] = set()
for origin in origins:
validated = _validate_cors_origin(origin)
if validated not in seen:
seen.add(validated)
deduped.append(validated)
return deduped
def _validate_runtime_secrets():
env_name = _environment_name()
if env_name not in PRODUCTION_ENV_NAMES:
return
broker_token_key = (os.getenv("BROKER_TOKEN_KEY") or "").strip()
if not broker_token_key:
raise RuntimeError("BROKER_TOKEN_KEY must be configured in production")
if (os.getenv("ENABLE_SUPER_ADMIN_BOOTSTRAP") or "").strip() in {"1", "true", "yes"}:
if not (os.getenv("SUPER_ADMIN_EMAIL") or "").strip():
raise RuntimeError("SUPER_ADMIN_EMAIL must be configured when bootstrap is enabled")
if not (os.getenv("SUPER_ADMIN_PASSWORD") or "").strip():
raise RuntimeError("SUPER_ADMIN_PASSWORD must be configured when bootstrap is enabled")
def _initialize_app_state(app: FastAPI):
app.state.startup_complete = False
app.state.startup_error = None
app.state.startup_started = False
app.state.background_warnings = {}
def _run_startup_tasks(app: FastAPI):
if os.getenv("DISABLE_STARTUP_TASKS", "0") == "1":
app.state.startup_started = True
app.state.startup_complete = True
app.state.startup_error = None
return
app.state.startup_started = True
app.state.startup_complete = False
app.state.startup_error = None
try:
init_log_state()
bootstrap_super_admin()
resume_running_runs()
app.state.startup_complete = True
except Exception as exc:
app.state.startup_error = str(exc)
print(f"[STARTUP] critical startup task failed: {exc}", flush=True)
try:
start_live_equity_snapshot_daemon()
except Exception as exc:
app.state.background_warnings["live_equity_snapshot_daemon"] = str(exc)
print(f"[STARTUP] live equity snapshot daemon failed to start: {exc}", flush=True)
@asynccontextmanager
async def _lifespan(app: FastAPI):
_initialize_app_state(app)
_run_startup_tasks(app)
yield
def create_app() -> FastAPI:
_validate_db_config()
_validate_runtime_secrets()
app = FastAPI(title="QuantFortune Backend", version="1.0", lifespan=_lifespan)
cors_origins = _build_cors_origins()
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(strategy_router)
app.include_router(auth_router)
app.include_router(broker_router)
app.include_router(zerodha_router)
app.include_router(zerodha_public_router)
app.include_router(paper_router)
app.include_router(market_router)
app.include_router(paper_mtm_router)
app.include_router(health_router)
app.include_router(system_router)
app.include_router(admin_router)
app.include_router(support_ticket_router)
app.include_router(password_reset_router)
return app
app = create_app()