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.auto_login import router as auto_login_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.auto_login_service import start_auto_login_scheduler 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) try: start_auto_login_scheduler() except Exception as exc: app.state.background_warnings["auto_login_scheduler"] = str(exc) print(f"[STARTUP] auto-login scheduler 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(auto_login_router) 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()