from fastapi import FastAPI, Depends, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from html import escape
from urllib.parse import quote
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")
MAIN_LOGIN_URL = os.getenv("MAIN_LOGIN_URL", "https://dbx.no1king.freeddns.org/login")
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
# 미인증 사용자는 dbx-main 의 로그인 페이지로 위임 (SSO 원칙).
# OMS 는 자체 로그인 UI 를 보여주지 않는다.
user_email = request.session.get("user_email")
if not user_email:
next_path = f"{APP_ROOT_PATH}/" if APP_ROOT_PATH else "/"
sep = "&" if "?" in MAIN_LOGIN_URL else "?"
target = f"{MAIN_LOGIN_URL}{sep}next={quote(next_path, safe='/')}"
return RedirectResponse(url=target, status_code=302)
user_name = request.session.get("user_name") or user_email
user_picture = request.session.get("user_picture", "") or ""
# 아바타: Google picture URL 이 있으면 , 없으면 폴백 아이콘
if user_picture:
avatar_html = (
f'
'
f''
)
else:
avatar_html = ''
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',
)
# 사용자 정보 주입 (HTML 안의 placeholder 치환)
content = content.replace("__USER_NAME__", escape(user_name))
content = content.replace("__USER_EMAIL__", escape(user_email))
content = content.replace("__USER_AVATAR__", avatar_html)
return HTMLResponse(content=content)
return HTMLResponse(content="