로그인 설정 UI 제거 및 .env 기반 설정
- 로그인 설정 모달(UI) 제거 - 허용 이메일 라벨을 관리자 이메일로 변경 - Auth0/관리자 이메일을 서버 .env에서 제공하고 클라이언트가 자동 로드 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
161
script.js
161
script.js
@@ -10,7 +10,6 @@
|
||||
const THEME_KEY = "links_home_theme_v1";
|
||||
const AUTH_TOAST_ONCE_KEY = "links_home_auth_toast_once_v1";
|
||||
const AUTH_OVERRIDE_KEY = "links_home_auth_override_v1";
|
||||
const AUTH_SETUP_SHOWN_KEY = "links_home_auth_setup_shown_v1";
|
||||
const AUTH_PKCE_KEY = "links_home_auth_pkce_v1";
|
||||
const AUTH_TOKEN_KEY = "links_home_auth_tokens_v1";
|
||||
|
||||
@@ -31,14 +30,6 @@
|
||||
btnLogout: document.getElementById("btnLogout"),
|
||||
snsLogin: document.getElementById("snsLogin"),
|
||||
btnGoogle: document.getElementById("btnGoogle"),
|
||||
authModal: document.getElementById("authModal"),
|
||||
btnAuthClose: document.getElementById("btnAuthClose"),
|
||||
authForm: document.getElementById("authForm"),
|
||||
authDomain: document.getElementById("authDomain"),
|
||||
authClientId: document.getElementById("authClientId"),
|
||||
authAllowedEmails: document.getElementById("authAllowedEmails"),
|
||||
authConnGoogle: document.getElementById("authConnGoogle"),
|
||||
btnAuthReset: document.getElementById("btnAuthReset"),
|
||||
modal: document.getElementById("modal"),
|
||||
btnClose: document.getElementById("btnClose"),
|
||||
btnCancel: document.getElementById("btnCancel"),
|
||||
@@ -79,7 +70,7 @@
|
||||
"link-ncue-net",
|
||||
"dreamgirl-ncue-net",
|
||||
]);
|
||||
const ACCESS_ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]);
|
||||
const DEFAULT_ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]);
|
||||
|
||||
function getUserEmail() {
|
||||
const e = auth && auth.user && auth.user.email ? String(auth.user.email) : "";
|
||||
@@ -88,7 +79,10 @@
|
||||
|
||||
function isAdminEmail(email) {
|
||||
const e = String(email || "").trim().toLowerCase();
|
||||
return ACCESS_ADMIN_EMAILS.has(e);
|
||||
const cfg = getAuthConfig();
|
||||
const admins = Array.isArray(cfg.adminEmails) ? cfg.adminEmails : [];
|
||||
if (admins.length) return admins.includes(e);
|
||||
return DEFAULT_ADMIN_EMAILS.has(e);
|
||||
}
|
||||
|
||||
function canAccessLink(link) {
|
||||
@@ -416,7 +410,12 @@
|
||||
const data = raw ? safeJsonParse(raw, null) : null;
|
||||
if (!data || typeof data !== "object") return null;
|
||||
const auth0 = data.auth0 && typeof data.auth0 === "object" ? data.auth0 : {};
|
||||
const allowedEmails = Array.isArray(data.allowedEmails) ? data.allowedEmails : [];
|
||||
// legacy: allowedEmails -> adminEmails
|
||||
const adminEmails = Array.isArray(data.adminEmails)
|
||||
? data.adminEmails
|
||||
: Array.isArray(data.allowedEmails)
|
||||
? data.allowedEmails
|
||||
: [];
|
||||
return {
|
||||
auth0: {
|
||||
domain: String(auth0.domain || "").trim(),
|
||||
@@ -431,7 +430,7 @@
|
||||
naver: String(data.connections.naver || "").trim(),
|
||||
}
|
||||
: { google: "", kakao: "", naver: "" },
|
||||
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||
adminEmails: adminEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -445,12 +444,15 @@
|
||||
|
||||
function getAuthConfig() {
|
||||
const cfg = globalThis.AUTH_CONFIG && typeof globalThis.AUTH_CONFIG === "object" ? globalThis.AUTH_CONFIG : {};
|
||||
const allowEndUserConfig = Boolean(cfg.allowEndUserConfig);
|
||||
const apiBase = String(cfg.apiBase || "").trim(); // optional, e.g. https://api.ncue.net
|
||||
const auth0 = cfg.auth0 && typeof cfg.auth0 === "object" ? cfg.auth0 : {};
|
||||
const allowedEmails = Array.isArray(cfg.allowedEmails) ? cfg.allowedEmails : [];
|
||||
// legacy: allowedEmails -> adminEmails
|
||||
const adminEmails = Array.isArray(cfg.adminEmails)
|
||||
? cfg.adminEmails
|
||||
: Array.isArray(cfg.allowedEmails)
|
||||
? cfg.allowedEmails
|
||||
: [];
|
||||
const base = {
|
||||
allowEndUserConfig,
|
||||
apiBase,
|
||||
auth0: {
|
||||
domain: String(auth0.domain || "").trim(),
|
||||
@@ -461,13 +463,12 @@
|
||||
kakao: "",
|
||||
naver: "",
|
||||
},
|
||||
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||
adminEmails: adminEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||
};
|
||||
const override = loadAuthOverride();
|
||||
if (!override) return base;
|
||||
// override가 있으면 우선 적용 (서버 재배포 없이 테스트 가능)
|
||||
return {
|
||||
allowEndUserConfig,
|
||||
apiBase,
|
||||
auth0: {
|
||||
domain: override.auth0.domain || base.auth0.domain,
|
||||
@@ -478,7 +479,7 @@
|
||||
kakao: override.connections?.kakao || "",
|
||||
naver: override.connections?.naver || "",
|
||||
},
|
||||
allowedEmails: override.allowedEmails.length ? override.allowedEmails : base.allowedEmails,
|
||||
adminEmails: override.adminEmails.length ? override.adminEmails : base.adminEmails,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -505,7 +506,12 @@
|
||||
const v = data.value;
|
||||
const auth0 = v.auth0 || {};
|
||||
const connections = v.connections || {};
|
||||
const allowedEmails = Array.isArray(v.allowedEmails) ? v.allowedEmails : [];
|
||||
// legacy: allowedEmails -> adminEmails
|
||||
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();
|
||||
@@ -513,7 +519,7 @@
|
||||
saveAuthOverride({
|
||||
auth0: { domain, clientId },
|
||||
connections: { google },
|
||||
allowedEmails,
|
||||
adminEmails,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
@@ -597,7 +603,7 @@
|
||||
async function manualAuthorize(connection) {
|
||||
const cfg = getAuthConfig();
|
||||
if (!cfg.auth0.domain || !cfg.auth0.clientId) {
|
||||
openAuthModal();
|
||||
toast("로그인 설정이 서버(.env)에 없습니다. 관리자에게 문의하세요.");
|
||||
return;
|
||||
}
|
||||
if (!globalThis.crypto || !crypto.subtle) {
|
||||
@@ -745,11 +751,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isAllowedEmail(email) {
|
||||
const { allowedEmails } = getAuthConfig();
|
||||
if (!allowedEmails.length) return true; // 설정이 비어있으면 로그인만으로 허용
|
||||
function isManageAdminEmail(email) {
|
||||
const cfg = getAuthConfig();
|
||||
const admins = Array.isArray(cfg.adminEmails) ? cfg.adminEmails : [];
|
||||
const e = String(email || "").trim().toLowerCase();
|
||||
return allowedEmails.includes(e);
|
||||
if (admins.length) return admins.includes(e);
|
||||
// 안전한 기본값: 설정이 비어있으면 기본 관리자만 관리 가능
|
||||
return DEFAULT_ADMIN_EMAILS.has(e);
|
||||
}
|
||||
|
||||
function updateAuthUi() {
|
||||
@@ -787,7 +795,7 @@
|
||||
// 로그인 기능이 "enabled"일 때만 관리 잠금을 적용합니다.
|
||||
state.canManage = auth.mode === "enabled" ? Boolean(auth.user && auth.authorized) : true;
|
||||
|
||||
const lockMsg = "관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.";
|
||||
const lockMsg = "관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.";
|
||||
el.btnAdd.disabled = !state.canManage;
|
||||
el.btnImport.disabled = !state.canManage;
|
||||
|
||||
@@ -814,11 +822,7 @@
|
||||
auth.mode = "misconfigured";
|
||||
updateAuthUi();
|
||||
applyManageLock();
|
||||
// 브라우저당 1회: 설정 모달 자동 오픈
|
||||
if (!localStorage.getItem(AUTH_SETUP_SHOWN_KEY)) {
|
||||
localStorage.setItem(AUTH_SETUP_SHOWN_KEY, "1");
|
||||
setTimeout(() => openAuthModal(), 80);
|
||||
}
|
||||
toastOnce("misconf", "로그인 설정이 서버(.env)에 없습니다. 관리자에게 문의하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -829,7 +833,7 @@
|
||||
await manualHandleCallbackIfNeeded().catch(() => {});
|
||||
auth.user = await manualLoadUser();
|
||||
const email = auth.user && auth.user.email ? String(auth.user.email) : "";
|
||||
auth.authorized = Boolean(auth.user) && isAllowedEmail(email);
|
||||
auth.authorized = Boolean(auth.user) && isManageAdminEmail(email);
|
||||
auth.serverCanManage = null;
|
||||
const t = loadTokens();
|
||||
if (auth.user && t && t.id_token) {
|
||||
@@ -883,7 +887,7 @@
|
||||
const isAuthed = await auth.client.isAuthenticated();
|
||||
auth.user = isAuthed ? await auth.client.getUser() : null;
|
||||
const email = auth.user && auth.user.email ? auth.user.email : "";
|
||||
auth.authorized = Boolean(auth.user) && isAllowedEmail(email);
|
||||
auth.authorized = Boolean(auth.user) && isManageAdminEmail(email);
|
||||
auth.serverCanManage = null;
|
||||
|
||||
// 로그인되었으면 서버에 사용자 upsert 및 can_manage 동기화(서버가 있을 때만)
|
||||
@@ -893,7 +897,7 @@
|
||||
const raw = claims && claims.__raw ? String(claims.__raw) : "";
|
||||
if (raw) {
|
||||
const cfg = getAuthConfig();
|
||||
const r = await fetch("/api/auth/sync", {
|
||||
const r = await fetch(apiUrl("/api/auth/sync"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${raw}`,
|
||||
@@ -915,9 +919,7 @@
|
||||
}
|
||||
|
||||
if (auth.user && !auth.authorized) {
|
||||
toastOnce("deny", "로그인은 되었지만 허용 이메일이 아니라서 관리 기능이 잠금 상태입니다.");
|
||||
} else if (auth.user && getAuthConfig().allowedEmails.length === 0) {
|
||||
toastOnce("allowall", "주의: 허용 이메일 목록이 비어있어서 로그인한 모든 계정이 관리 가능 상태입니다.");
|
||||
toastOnce("deny", "로그인은 되었지만 관리자 이메일이 아니라서 관리 기능이 잠금 상태입니다.");
|
||||
}
|
||||
|
||||
updateAuthUi();
|
||||
@@ -926,7 +928,7 @@
|
||||
|
||||
async function login() {
|
||||
if (auth.mode !== "enabled" || !auth.client) {
|
||||
openAuthModal();
|
||||
toast("로그인 설정이 서버(.env)에 필요합니다.");
|
||||
return;
|
||||
}
|
||||
await auth.client.loginWithRedirect();
|
||||
@@ -934,7 +936,7 @@
|
||||
|
||||
async function loginWithConnection(connection) {
|
||||
if (auth.mode !== "enabled") {
|
||||
openAuthModal();
|
||||
toast("로그인 설정이 서버(.env)에 필요합니다.");
|
||||
return;
|
||||
}
|
||||
if (auth.client) {
|
||||
@@ -983,29 +985,6 @@
|
||||
location.assign(u.toString());
|
||||
}
|
||||
|
||||
function openAuthModal() {
|
||||
if (!el.authModal || !el.authForm) {
|
||||
toast("로그인 설정 UI를 찾지 못했습니다. 새로고침 후 다시 시도하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = getAuthConfig();
|
||||
el.authDomain.value = cfg.auth0.domain || "";
|
||||
el.authClientId.value = cfg.auth0.clientId || "";
|
||||
el.authAllowedEmails.value = (cfg.allowedEmails || []).join(", ");
|
||||
if (el.authConnGoogle) el.authConnGoogle.value = cfg.connections.google || "";
|
||||
|
||||
el.authModal.hidden = false;
|
||||
document.body.style.overflow = "hidden";
|
||||
setTimeout(() => el.authDomain.focus(), 0);
|
||||
}
|
||||
|
||||
function closeAuthModal() {
|
||||
if (!el.authModal) return;
|
||||
el.authModal.hidden = true;
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
@@ -1173,7 +1152,7 @@
|
||||
return;
|
||||
}
|
||||
if (auth.mode === "enabled" && !state.canManage && (act === "fav" || act === "edit" || act === "del")) {
|
||||
toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
|
||||
toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
|
||||
return;
|
||||
}
|
||||
if (act === "fav") {
|
||||
@@ -1281,7 +1260,7 @@
|
||||
|
||||
el.btnAdd.addEventListener("click", () => {
|
||||
if (auth.mode === "enabled" && !state.canManage)
|
||||
return toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
|
||||
return toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
|
||||
openModal("add", null);
|
||||
});
|
||||
el.btnClose.addEventListener("click", closeModal);
|
||||
@@ -1299,7 +1278,7 @@
|
||||
el.btnExport.addEventListener("click", exportJson);
|
||||
el.btnImport.addEventListener("click", () => {
|
||||
if (auth.mode === "enabled" && !state.canManage)
|
||||
return toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
|
||||
return toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
|
||||
el.file.click();
|
||||
});
|
||||
el.file.addEventListener("change", async () => {
|
||||
@@ -1324,57 +1303,9 @@
|
||||
if (el.btnGoogle)
|
||||
el.btnGoogle.addEventListener("click", () => {
|
||||
const c = getAuthConfig().connections.google;
|
||||
if (!c) return openAuthModal();
|
||||
if (!c) return toast("서버(.env)에 AUTH0_GOOGLE_CONNECTION 설정이 필요합니다.");
|
||||
return loginWithConnection(c).catch(() => toast("로그인에 실패했습니다."));
|
||||
});
|
||||
|
||||
if (el.btnAuthClose) el.btnAuthClose.addEventListener("click", closeAuthModal);
|
||||
if (el.authModal) {
|
||||
el.authModal.addEventListener("click", (e) => {
|
||||
const close = e.target && e.target.getAttribute && e.target.getAttribute("data-auth-close");
|
||||
if (close) closeAuthModal();
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && el.authModal && !el.authModal.hidden) closeAuthModal();
|
||||
});
|
||||
|
||||
if (el.btnAuthReset) {
|
||||
el.btnAuthReset.addEventListener("click", () => {
|
||||
clearAuthOverride();
|
||||
toast("로그인 설정을 초기화했습니다.");
|
||||
closeAuthModal();
|
||||
// 초기화 후 재로딩(상태 정리)
|
||||
setTimeout(() => location.reload(), 200);
|
||||
});
|
||||
}
|
||||
|
||||
if (el.authForm) {
|
||||
el.authForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const domain = String(el.authDomain.value || "").trim();
|
||||
const clientId = String(el.authClientId.value || "").trim();
|
||||
const emails = String(el.authAllowedEmails.value || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const connGoogle = el.authConnGoogle ? String(el.authConnGoogle.value || "").trim() : "";
|
||||
|
||||
if (!domain || !clientId) {
|
||||
toast("Domain과 Client ID를 입력하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
saveAuthOverride({
|
||||
auth0: { domain, clientId },
|
||||
connections: { google: connGoogle },
|
||||
allowedEmails: emails,
|
||||
});
|
||||
toast("저장했습니다. 페이지를 새로고침합니다.");
|
||||
closeAuthModal();
|
||||
setTimeout(() => location.reload(), 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
Reference in New Issue
Block a user