feat(ui): /orderlist 미인증 시 dbx-main 로그인으로 위임, 프로필 헤더 개선
- 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 별도 커밋).
This commit is contained in:
+59
-9
@@ -1,7 +1,9 @@
|
|||||||
from fastapi import FastAPI, Depends, Request
|
from fastapi import FastAPI, Depends, Request
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
from html import escape
|
||||||
|
from urllib.parse import quote
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -68,18 +70,66 @@ app.include_router(settings.router, prefix="/api/settings", tags=["settings"])
|
|||||||
os.makedirs("static", exist_ok=True)
|
os.makedirs("static", exist_ok=True)
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def read_root():
|
async def read_root(request: Request):
|
||||||
# Return the main index.html file
|
# 미인증 사용자는 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"):
|
if os.path.exists("static/index.html"):
|
||||||
with open("static/index.html", "r", encoding="utf-8") as f:
|
with open("static/index.html", "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
content = content.replace("__APP_BASE_PATH__", APP_ROOT_PATH)
|
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(
|
||||||
content = content.replace('href="/static/', f'href="{APP_ROOT_PATH}/static/' if APP_ROOT_PATH else 'href="/static/')
|
'src="/static/',
|
||||||
content = content.replace("window.location.href='/login", f"window.location.href='{APP_ROOT_PATH}/login")
|
f'src="{APP_ROOT_PATH}/static/' if APP_ROOT_PATH else 'src="/static/',
|
||||||
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(
|
||||||
content = content.replace('window.location.href="/logout', f'window.location.href="{APP_ROOT_PATH}/logout')
|
'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=content)
|
||||||
return HTMLResponse(content="<h1>Welcome. static/index.html is missing.</h1>")
|
return HTMLResponse(content="<h1>Welcome. static/index.html is missing.</h1>")
|
||||||
|
|||||||
@@ -1769,4 +1769,82 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 우측 상단 프로필 영역 (메인 페이지 스타일) ───────────── */
|
||||||
|
.nav-profile-area .profile-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .profile-name-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .profile-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .profile-email {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(120, 120, 130, 0.75);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .profile-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .profile-avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .profile-avatar-icon {
|
||||||
|
color: rgba(80, 80, 90, 0.7);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .btn-logout-text {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-profile-area .btn-logout-text:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모바일에서 이메일은 숨겨 공간 절약 */
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.nav-profile-area .profile-email { display: none; }
|
||||||
|
.nav-profile-area .btn-logout-text span { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Force UI Update */
|
/* Force UI Update */
|
||||||
|
|||||||
+14
-26
@@ -20,26 +20,9 @@
|
|||||||
<!-- MAIN APP CONTAINER -->
|
<!-- MAIN APP CONTAINER -->
|
||||||
<div class="app-container" id="app">
|
<div class="app-container" id="app">
|
||||||
|
|
||||||
<!-- LOGIN SCREEN (Visible when unauthenticated) -->
|
<!-- DASHBOARD SCREEN — 미인증 사용자는 서버 단에서 dbx-main 로그인으로 리다이렉트되므로
|
||||||
<div id="login-screen" class="screen active glass-panel">
|
OMS 자체 로그인 UI 는 더 이상 존재하지 않는다 (SSO 원칙). -->
|
||||||
<div class="login-box">
|
<div id="dashboard-screen" class="screen active">
|
||||||
<div class="logo">
|
|
||||||
<i class="fa-solid fa-boxes-packing logo-icon"></i>
|
|
||||||
<h2>OMS system</h2>
|
|
||||||
</div>
|
|
||||||
<p class="subtitle">Enter the corporate order management center.</p>
|
|
||||||
<div class="oauth-buttons">
|
|
||||||
<button class="btn btn-primary" style="width: 100%;"
|
|
||||||
onclick="window.location.href='/login'">
|
|
||||||
<i class="fa-solid fa-right-to-bracket"></i> 로그인
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="auth-hint">Access restricted to approved users.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DASHBOARD SCREEN (Visible when authenticated) -->
|
|
||||||
<div id="dashboard-screen" class="screen hidden">
|
|
||||||
<!-- Unified Top Header Bar -->
|
<!-- Unified Top Header Bar -->
|
||||||
<header class="unified-header glass-panel fade-in">
|
<header class="unified-header glass-panel fade-in">
|
||||||
<!-- 1. Logo Section -->
|
<!-- 1. Logo Section -->
|
||||||
@@ -111,16 +94,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. Profile Section -->
|
<!-- 3. Profile Section — 메인 페이지 스타일 (이름 + 이메일 + 아바타 + 로그아웃 버튼) -->
|
||||||
<div class="nav-profile-area">
|
<div class="nav-profile-area">
|
||||||
<button id="btn-data-management" class="icon-btn mini-icon-btn nav-tool-btn" title="데이터 관리"><i
|
<button id="btn-data-management" class="icon-btn mini-icon-btn nav-tool-btn" title="데이터 관리"><i
|
||||||
class="fa-solid fa-database"></i></button>
|
class="fa-solid fa-database"></i></button>
|
||||||
<div class="user-profile mini-profile">
|
<div class="profile-info">
|
||||||
<span id="user-name">Loading...</span>
|
<div class="profile-name-block">
|
||||||
<div class="avatar mini-avatar"><i class="fa-solid fa-user"></i></div>
|
<span class="profile-name" id="user-name">__USER_NAME__</span>
|
||||||
|
<span class="profile-email">__USER_EMAIL__</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="icon-btn mini-icon-btn" onclick="window.location.href='/logout'" title="Logout"><i
|
<div class="profile-avatar">__USER_AVATAR__</div>
|
||||||
class="fa-solid fa-right-from-bracket"></i></button>
|
</div>
|
||||||
|
<button class="btn-logout-text" onclick="window.location.href='/logout'" title="로그아웃">
|
||||||
|
<i class="fa-solid fa-right-from-bracket"></i>
|
||||||
|
<span>로그아웃</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
+9
-13
@@ -3,8 +3,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const appUrl = (path) => `${APP_BASE_PATH}${path.startsWith('/') ? path : `/${path}`}`;
|
const appUrl = (path) => `${APP_BASE_PATH}${path.startsWith('/') ? path : `/${path}`}`;
|
||||||
|
|
||||||
// --- Elements ---
|
// --- Elements ---
|
||||||
const loginScreen = document.getElementById('login-screen');
|
// loginScreen 은 더 이상 존재하지 않음 (서버에서 미인증 사용자를 dbx-main 으로 리다이렉트)
|
||||||
const dashboardScreen = document.getElementById('dashboard-screen');
|
|
||||||
const userNameEl = document.getElementById('user-name');
|
const userNameEl = document.getElementById('user-name');
|
||||||
|
|
||||||
const searchInputs = {
|
const searchInputs = {
|
||||||
@@ -185,26 +184,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Authentication ---
|
// --- Authentication ---
|
||||||
|
// 미인증 사용자는 서버(read_root) 가 이미 dbx-main 로그인으로 리다이렉트하므로
|
||||||
|
// 여기서는 is_admin 확인 + 세션 만료 시 다시 dbx-main 으로 보내는 역할만 한다.
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(appUrl('/api/users/me'));
|
const res = await fetch(appUrl('/api/users/me'));
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
currentUser = await res.json();
|
currentUser = await res.json();
|
||||||
|
if (userNameEl && currentUser.name) {
|
||||||
userNameEl.textContent = currentUser.name;
|
userNameEl.textContent = currentUser.name;
|
||||||
|
}
|
||||||
loginScreen.classList.remove('active');
|
|
||||||
loginScreen.classList.add('hidden');
|
|
||||||
dashboardScreen.classList.remove('hidden');
|
|
||||||
dashboardScreen.classList.add('active');
|
|
||||||
|
|
||||||
if (btnDataManagement) {
|
if (btnDataManagement) {
|
||||||
btnDataManagement.style.display = currentUser.is_admin ? 'inline-flex' : 'none';
|
btnDataManagement.style.display = currentUser.is_admin ? 'inline-flex' : 'none';
|
||||||
}
|
}
|
||||||
} else {
|
} else if (res.status === 401) {
|
||||||
loginScreen.classList.remove('hidden');
|
// 세션 만료 — dbx-main 로그인으로 위임 (next 로 현재 위치 보존)
|
||||||
loginScreen.classList.add('active');
|
const next = encodeURIComponent((APP_BASE_PATH || '') + '/');
|
||||||
dashboardScreen.classList.add('hidden');
|
window.location.href = appUrl('/login') + '?next=' + next;
|
||||||
dashboardScreen.classList.remove('active');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth check error', error);
|
console.error('Auth check error', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user