관리자 이메일 확장 및 폴백 동작 정리

- 기본 관리자 이메일 목록에 추가 계정 반영
- script.js 로드 실패 시 폴백에서도 /api/config/auth hydrate 및 /api/auth/sync 호출
- 폴백에서 관리자 전용 기능 잠금 및 로그인 전 내보내기 비활성화 적용

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-08 09:56:58 +09:00
parent 62768602c2
commit d161b61783
3 changed files with 92 additions and 4 deletions

View File

@@ -14,7 +14,7 @@ AUTH0_CLIENT_ID=g5RDfax7FZkKzXYvXOgku3Ll8CxuA4IM
# Google connection name (usually google-oauth2) # Google connection name (usually google-oauth2)
AUTH0_GOOGLE_CONNECTION=google-oauth2 AUTH0_GOOGLE_CONNECTION=google-oauth2
# Admin emails (comma-separated) # Admin emails (comma-separated)
ADMIN_EMAILS=dosangyoon@gmail.com,dsyoon@ncue.net ADMIN_EMAILS=dsyoon@ncue.net,dosangyoon@gmail.com,dosangyoon2@gmail.com,dosangyoon3@gmail.com
## Optional ## Optional
# Server port # Server port

View File

@@ -433,11 +433,15 @@
"link-ncue-net", "link-ncue-net",
"dreamgirl-ncue-net", "dreamgirl-ncue-net",
]); ]);
const ACCESS_ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]);
let sessionEmail = ""; let sessionEmail = "";
function isAdminEmail(email) { function isAdminEmail(email) {
return ACCESS_ADMIN_EMAILS.has(String(email || "").trim().toLowerCase()); const cfg = getAuthConfig();
const admins = cfg && Array.isArray(cfg.adminEmails) ? cfg.adminEmails : [];
const e = String(email || "").trim().toLowerCase();
if (admins.length) return admins.includes(e);
// fallback default
return ["dsyoon@ncue.net", "dosangyoon@gmail.com", "dosangyoon2@gmail.com", "dosangyoon3@gmail.com"].includes(e);
} }
function canAccessLink(link) { function canAccessLink(link) {
const id = String(link && link.id ? link.id : ""); const id = String(link && link.id ? link.id : "");
@@ -663,6 +667,50 @@
ready: false, ready: false,
}; };
function apiUrl(pathname) {
// same-origin only in fallback
return pathname;
}
async function hydrateAuthConfigFromServerIfNeeded() {
const cfg = getAuthConfig();
const hasLocal = Boolean(cfg.auth0.domain && cfg.auth0.clientId && cfg.connections.google);
if (hasLocal) return true;
try {
const r = await fetch(apiUrl("/api/config/auth"), { cache: "no-store" });
if (!r.ok) return false;
const data = await r.json();
if (!data || !data.ok || !data.value) return false;
const v = data.value;
const auth0 = v.auth0 || {};
const connections = v.connections || {};
const adminEmails = Array.isArray(v.adminEmails) ? v.adminEmails : Array.isArray(v.allowedEmails) ? v.allowedEmails : [];
const domain = String(auth0.domain || "").trim();
const clientId = String(auth0.clientId || "").trim();
const google = String(connections.google || "").trim();
if (!domain || !clientId || !google) return false;
localStorage.setItem(
AUTH_OVERRIDE_KEY,
JSON.stringify({
auth0: { domain, clientId },
connections: { google },
adminEmails,
})
);
return true;
} catch {
return false;
}
}
function applyManageLock() {
const canManage = Boolean(auth.user) && isAdminEmail(sessionEmail);
if (el.btnAdd) el.btnAdd.disabled = !canManage;
if (el.btnImport) el.btnImport.disabled = !canManage;
// 요청: 로그인 전 내보내기 비활성화
if (el.btnExport) el.btnExport.disabled = !auth.user;
}
function loadAuthOverride() { function loadAuthOverride() {
const raw = localStorage.getItem(AUTH_OVERRIDE_KEY); const raw = localStorage.getItem(AUTH_OVERRIDE_KEY);
const data = raw ? safeJsonParse(raw, null) : null; const data = raw ? safeJsonParse(raw, null) : null;
@@ -736,6 +784,7 @@
async function ensureAuthClient() { async function ensureAuthClient() {
if (auth.client) return auth.client; if (auth.client) return auth.client;
await hydrateAuthConfigFromServerIfNeeded();
const cfg = getAuthConfig(); const cfg = getAuthConfig();
if (!cfg.auth0.domain || !cfg.auth0.clientId) return null; if (!cfg.auth0.domain || !cfg.auth0.clientId) return null;
if (typeof window.createAuth0Client !== "function") return null; if (typeof window.createAuth0Client !== "function") return null;
@@ -756,6 +805,7 @@
const client = await ensureAuthClient(); const client = await ensureAuthClient();
if (!client) { if (!client) {
// No config: keep buttons visible but disabled style // No config: keep buttons visible but disabled style
applyManageLock();
return; return;
} }
const u = new URL(location.href); const u = new URL(location.href);
@@ -774,6 +824,29 @@
if (el.snsLogin) el.snsLogin.hidden = Boolean(auth.user); if (el.snsLogin) el.snsLogin.hidden = Boolean(auth.user);
if (el.user) el.user.hidden = !auth.user; if (el.user) el.user.hidden = !auth.user;
if (el.userText && auth.user) el.userText.textContent = auth.user.email || auth.user.name || "로그인됨"; if (el.userText && auth.user) el.userText.textContent = auth.user.email || auth.user.name || "로그인됨";
// sync user to server (upsert ncue_user)
if (auth.user) {
try {
const claims = await client.getIdTokenClaims();
const raw = claims && claims.__raw ? String(claims.__raw) : "";
if (raw) {
const cfg = getAuthConfig();
await fetch(apiUrl("/api/auth/sync"), {
method: "POST",
headers: {
Authorization: `Bearer ${raw}`,
"X-Auth0-Issuer": `https://${cfg.auth0.domain}/`,
"X-Auth0-ClientId": cfg.auth0.clientId,
},
}).catch(() => {});
}
} catch {
// ignore
}
}
applyManageLock();
render(); render();
} }
@@ -876,6 +949,13 @@
e.preventDefault(); e.preventDefault();
return; return;
} }
// block manage actions unless admin
const act0 = btn.getAttribute("data-act");
const canManage = Boolean(auth.user) && isAdminEmail(sessionEmail);
if (!canManage && (act0 === "fav" || act0 === "edit" || act0 === "del")) {
toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
return;
}
const act = btn.getAttribute("data-act"); const act = btn.getAttribute("data-act");
const link = id ? getById(id) : null; const link = id ? getById(id) : null;
if (!link) return; if (!link) return;
@@ -899,6 +979,8 @@
if (el.btnExport) el.btnExport.addEventListener("click", exportJson); if (el.btnExport) el.btnExport.addEventListener("click", exportJson);
if (el.btnImport) if (el.btnImport)
el.btnImport.addEventListener("click", () => { el.btnImport.addEventListener("click", () => {
const canManage = Boolean(auth.user) && isAdminEmail(sessionEmail);
if (!canManage) return toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
if (el.file) el.file.click(); if (el.file) el.file.click();
}); });
if (el.file) if (el.file)
@@ -926,6 +1008,7 @@
render(); render();
initAuth().catch(() => {}); initAuth().catch(() => {});
applyManageLock();
toast("스크립트 로딩 문제로 폴백 모드로 실행 중입니다."); toast("스크립트 로딩 문제로 폴백 모드로 실행 중입니다.");
}, 200); }, 200);
})(); })();

View File

@@ -70,7 +70,12 @@
"link-ncue-net", "link-ncue-net",
"dreamgirl-ncue-net", "dreamgirl-ncue-net",
]); ]);
const DEFAULT_ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]); const DEFAULT_ADMIN_EMAILS = new Set([
"dsyoon@ncue.net",
"dosangyoon@gmail.com",
"dosangyoon2@gmail.com",
"dosangyoon3@gmail.com",
]);
function getUserEmail() { function getUserEmail() {
const e = auth && auth.user && auth.user.email ? String(auth.user.email) : ""; const e = auth && auth.user && auth.user.email ? String(auth.user.email) : "";