diff --git a/.env.example b/.env.example index 1631bf9..f5f11af 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,7 @@ AUTH0_CLIENT_ID=g5RDfax7FZkKzXYvXOgku3Ll8CxuA4IM # Google connection name (usually google-oauth2) AUTH0_GOOGLE_CONNECTION=google-oauth2 # 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 # Server port diff --git a/index.html b/index.html index 491d8c5..f215902 100644 --- a/index.html +++ b/index.html @@ -433,11 +433,15 @@ "link-ncue-net", "dreamgirl-ncue-net", ]); - const ACCESS_ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]); let sessionEmail = ""; 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) { const id = String(link && link.id ? link.id : ""); @@ -663,6 +667,50 @@ 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() { const raw = localStorage.getItem(AUTH_OVERRIDE_KEY); const data = raw ? safeJsonParse(raw, null) : null; @@ -736,6 +784,7 @@ async function ensureAuthClient() { if (auth.client) return auth.client; + await hydrateAuthConfigFromServerIfNeeded(); const cfg = getAuthConfig(); if (!cfg.auth0.domain || !cfg.auth0.clientId) return null; if (typeof window.createAuth0Client !== "function") return null; @@ -756,6 +805,7 @@ const client = await ensureAuthClient(); if (!client) { // No config: keep buttons visible but disabled style + applyManageLock(); return; } const u = new URL(location.href); @@ -774,6 +824,29 @@ if (el.snsLogin) el.snsLogin.hidden = Boolean(auth.user); if (el.user) el.user.hidden = !auth.user; 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(); } @@ -876,6 +949,13 @@ e.preventDefault(); 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 link = id ? getById(id) : null; if (!link) return; @@ -899,6 +979,8 @@ if (el.btnExport) el.btnExport.addEventListener("click", exportJson); if (el.btnImport) el.btnImport.addEventListener("click", () => { + const canManage = Boolean(auth.user) && isAdminEmail(sessionEmail); + if (!canManage) return toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다."); if (el.file) el.file.click(); }); if (el.file) @@ -926,6 +1008,7 @@ render(); initAuth().catch(() => {}); + applyManageLock(); toast("스크립트 로딩 문제로 폴백 모드로 실행 중입니다."); }, 200); })(); diff --git a/script.js b/script.js index f6c5e61..7d3098b 100644 --- a/script.js +++ b/script.js @@ -70,7 +70,12 @@ "link-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() { const e = auth && auth.user && auth.user.email ? String(auth.user.email) : "";