931ad173ab
- read_root() 가 세션 미인증 시 dbx-main /login?next=/orderlist/ 로 302. OMS 는 더 이상 자체 로그인 UI 를 가지지 않는다. - index.html: #login-screen 블록 제거, dashboard 만 렌더. - 우측 상단 프로필 영역을 메인 페이지 스타일로 개편: 이름 + 이메일 + 아바타(Google picture URL, 폴백 아이콘) + 로그아웃 텍스트 버튼. - main.py 가 세션의 user_name/user_email/user_picture 를 HTML placeholder 로 치환. - app.js checkAuth() 단순화: 화면 토글 제거, 401 시 dbx-main 로그인으로 자동 이동. - style.css 에 새 프로필 영역 CSS 추가. Google 아바타가 보이려면 dbx-main 도 같이 user_picture 를 세션 top-level 에 저장하도록 수정됨 (dbx-main 별도 커밋).
136 lines
5.8 KiB
Python
136 lines
5.8 KiB
Python
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 이 있으면 <img>, 없으면 폴백 아이콘
|
|
if user_picture:
|
|
avatar_html = (
|
|
f'<img src="{escape(user_picture, quote=True)}" '
|
|
f'alt="" class="profile-avatar-img" referrerpolicy="no-referrer" '
|
|
f'onerror="this.style.display=\'none\';'
|
|
f'this.nextElementSibling.style.display=\'flex\'">'
|
|
f'<i class="fa-solid fa-user profile-avatar-icon" style="display:none"></i>'
|
|
)
|
|
else:
|
|
avatar_html = '<i class="fa-solid fa-user profile-avatar-icon"></i>'
|
|
|
|
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="<h1>Welcome. static/index.html is missing.</h1>")
|