from fastapi import FastAPI, Depends, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, JSONResponse from starlette.middleware.sessions import SessionMiddleware import logging import os from app.database import engine, Base, check_db_connection, log_db_startup_status from app.database_maintenance import ensure_search_optimizations from app.routers import orders, settings from app.auth import router as auth_router logging.basicConfig(level=logging.INFO) APP_ROOT_PATH = os.getenv("APP_ROOT_PATH", "").rstrip("/") # Verify DB connectivity at startup (log only — do not crash so the # container can stay up and report status via /health/db). log_db_startup_status() # Create all DB tables Base.metadata.create_all(bind=engine) ensure_search_optimizations(engine) # root_path 는 FastAPI 생성자에 넘기지 않는다. # Starlette 의 Mount 가 root_path 를 흡수해 mount 경로에 prefix 가 붙어버려서 # @app.get 라우트와 mount 가 비대칭으로 등록되는 문제 때문이다. # (예: /health/db 는 그대로지만 /static/* 는 /orderlist/static/* 로 등록됨) # # 대신 uvicorn --root-path 로 ASGI scope.root_path 만 세팅한다. # 그러면 모든 라우트/마운트가 prefix 없이 등록되고, URL 생성 시에만 prefix 가 붙는다. # Dockerfile 의 CMD 가 ${APP_ROOT_PATH} 를 --root-path 로 넘긴다. app = FastAPI(title="Order Management System") @app.get("/health/db") async def health_db(): ok, msg = check_db_connection() status_code = 200 if ok else 503 return JSONResponse({"ok": ok, "detail": msg}, status_code=status_code) # ---- Session middleware ---- # dbx-main 과 SSO 하려면 SESSION_SECRET_KEY 와 SESSION_COOKIE_NAME 을 # 두 앱이 동일하게 맞춰야 한다. 그래야 dbx-main 이 발급한 세션 쿠키를 # OMS 가 그대로 읽을 수 있다. _session_secret = os.getenv("SESSION_SECRET_KEY", "").strip() if not _session_secret: raise RuntimeError( "SESSION_SECRET_KEY 환경변수가 설정되지 않았습니다. " "openssl rand -hex 32 로 새 값을 만들어 .env 에 넣고 컨테이너를 재기동하세요. " "dbx-main 과 SSO 하려면 양쪽 .env 에 같은 값이어야 합니다." ) app.add_middleware( SessionMiddleware, secret_key=_session_secret, session_cookie=os.getenv("SESSION_COOKIE_NAME", "session"), same_site=os.getenv("SESSION_SAME_SITE", "lax"), https_only=os.getenv("SESSION_HTTPS_ONLY", "0") == "1", max_age=int(os.getenv("SESSION_MAX_AGE", "28800")), # 8h — dbx-main 과 통일 ) # Include routers app.include_router(auth_router) app.include_router(orders.router, prefix="/api/orders", tags=["orders"]) app.include_router(settings.router, prefix="/api/settings", tags=["settings"]) # Mount static files for frontend os.makedirs("static", exist_ok=True) app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/", response_class=HTMLResponse) async def read_root(): # Return the main index.html file if os.path.exists("static/index.html"): with open("static/index.html", "r", encoding="utf-8") as f: content = f.read() content = content.replace("__APP_BASE_PATH__", APP_ROOT_PATH) content = content.replace('src="/static/', f'src="{APP_ROOT_PATH}/static/' if APP_ROOT_PATH else 'src="/static/') content = content.replace('href="/static/', f'href="{APP_ROOT_PATH}/static/' if APP_ROOT_PATH else 'href="/static/') content = content.replace("window.location.href='/login", f"window.location.href='{APP_ROOT_PATH}/login") content = content.replace('window.location.href="/login', f'window.location.href="{APP_ROOT_PATH}/login') content = content.replace("window.location.href='/logout", f"window.location.href='{APP_ROOT_PATH}/logout") content = content.replace('window.location.href="/logout', f'window.location.href="{APP_ROOT_PATH}/logout') return HTMLResponse(content=content) return HTMLResponse(content="