로그인 설정 UI 제거 및 .env 기반 설정
- 로그인 설정 모달(UI) 제거 - 허용 이메일 라벨을 관리자 이메일로 변경 - Auth0/관리자 이메일을 서버 .env에서 제공하고 클라이언트가 자동 로드 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24
.env.example
Normal file
24
.env.example
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## Database
|
||||||
|
DB_HOST=ncue.net
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=ncue
|
||||||
|
DB_USER=ncue
|
||||||
|
DB_PASSWORD=REPLACE_ME
|
||||||
|
TABLE=ncue_user
|
||||||
|
|
||||||
|
## Auth0 (server-side)
|
||||||
|
# Auth0 Domain (without https://)
|
||||||
|
AUTH0_DOMAIN=ncue.net
|
||||||
|
# Auth0 SPA Application Client ID
|
||||||
|
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
|
||||||
|
|
||||||
|
## Optional
|
||||||
|
# Server port
|
||||||
|
PORT=8000
|
||||||
|
# Optional: allow writing config via API (not required if using env)
|
||||||
|
CONFIG_TOKEN=
|
||||||
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,4 +2,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
|||||||
68
index.html
68
index.html
@@ -22,8 +22,6 @@
|
|||||||
// 2) Allowed Callback URLs / Allowed Logout URLs에 현재 사이트 주소를 등록하세요.
|
// 2) Allowed Callback URLs / Allowed Logout URLs에 현재 사이트 주소를 등록하세요.
|
||||||
// 예: https://drive.daewoongai.com/apps/dashboard/
|
// 예: https://drive.daewoongai.com/apps/dashboard/
|
||||||
window.AUTH_CONFIG = {
|
window.AUTH_CONFIG = {
|
||||||
// end-user가 설정 모달을 사용하는지 여부(기본: false)
|
|
||||||
allowEndUserConfig: false,
|
|
||||||
// (선택) API 서버가 다른 도메인이면 지정. 예: https://api.ncue.net
|
// (선택) API 서버가 다른 도메인이면 지정. 예: https://api.ncue.net
|
||||||
apiBase: "",
|
apiBase: "",
|
||||||
auth0: {
|
auth0: {
|
||||||
@@ -34,8 +32,8 @@
|
|||||||
connections: {
|
connections: {
|
||||||
google: "",
|
google: "",
|
||||||
},
|
},
|
||||||
// 관리 허용 이메일(대소문자 무시)
|
// 관리자 이메일(대소문자 무시)
|
||||||
allowedEmails: [],
|
adminEmails: [],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script defer src="./script.js"></script>
|
<script defer src="./script.js"></script>
|
||||||
@@ -194,48 +192,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth Config Modal -->
|
|
||||||
<div class="modal" id="authModal" role="dialog" aria-modal="true" aria-labelledby="authModalTitle" hidden>
|
|
||||||
<div class="modal-backdrop" data-auth-close="1"></div>
|
|
||||||
<div class="modal-card" role="document">
|
|
||||||
<div class="modal-head">
|
|
||||||
<div class="modal-title" id="authModalTitle">로그인 설정</div>
|
|
||||||
<button class="icon-btn" type="button" id="btnAuthClose" title="닫기" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="authForm" class="modal-body">
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Auth0 Domain</span>
|
|
||||||
<input id="authDomain" class="input" type="text" placeholder="예: your-tenant.us.auth0.com" />
|
|
||||||
<div class="hint">Auth0 테넌트 도메인입니다. (비밀값 아님)</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Auth0 Client ID</span>
|
|
||||||
<input id="authClientId" class="input" type="text" placeholder="예: AbCdEf..." />
|
|
||||||
<div class="hint">Auth0 SPA Application의 Client ID입니다. (비밀값 아님)</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">허용 이메일</span>
|
|
||||||
<input id="authAllowedEmails" class="input" type="text" placeholder="예: me@example.com, admin@example.com" />
|
|
||||||
<div class="hint">쉼표로 구분합니다. 비워두면 “로그인한 모든 계정”이 관리 가능해집니다.</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Connection 이름(선택)</span>
|
|
||||||
<input id="authConnGoogle" class="input" type="text" placeholder="Google 예: google-oauth2" />
|
|
||||||
<div class="hint">Auth0에서 설정한 Google connection 이름입니다. (보통 `google-oauth2`)</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="modal-foot">
|
|
||||||
<button class="btn btn-ghost" type="button" id="btnAuthReset">초기화</button>
|
|
||||||
<button class="btn btn-primary" type="submit" id="btnAuthSave">저장</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden file input for import -->
|
<!-- Hidden file input for import -->
|
||||||
<input id="file" type="file" accept="application/json" hidden />
|
<input id="file" type="file" accept="application/json" hidden />
|
||||||
|
|
||||||
@@ -713,7 +669,12 @@
|
|||||||
if (!data || typeof data !== "object") return null;
|
if (!data || typeof data !== "object") return null;
|
||||||
const auth0 = data.auth0 && typeof data.auth0 === "object" ? data.auth0 : {};
|
const auth0 = data.auth0 && typeof data.auth0 === "object" ? data.auth0 : {};
|
||||||
const connections = data.connections && typeof data.connections === "object" ? data.connections : {};
|
const connections = data.connections && typeof data.connections === "object" ? data.connections : {};
|
||||||
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 {
|
return {
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: String(auth0.domain || "").trim(),
|
domain: String(auth0.domain || "").trim(),
|
||||||
@@ -724,7 +685,7 @@
|
|||||||
kakao: String(connections.kakao || "").trim(),
|
kakao: String(connections.kakao || "").trim(),
|
||||||
naver: String(connections.naver || "").trim(),
|
naver: String(connections.naver || "").trim(),
|
||||||
},
|
},
|
||||||
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
adminEmails: adminEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +693,12 @@
|
|||||||
const cfg = window.AUTH_CONFIG && typeof window.AUTH_CONFIG === "object" ? window.AUTH_CONFIG : {};
|
const cfg = window.AUTH_CONFIG && typeof window.AUTH_CONFIG === "object" ? window.AUTH_CONFIG : {};
|
||||||
const auth0 = cfg.auth0 && typeof cfg.auth0 === "object" ? cfg.auth0 : {};
|
const auth0 = cfg.auth0 && typeof cfg.auth0 === "object" ? cfg.auth0 : {};
|
||||||
const connections = cfg.connections && typeof cfg.connections === "object" ? cfg.connections : {};
|
const connections = cfg.connections && typeof cfg.connections === "object" ? cfg.connections : {};
|
||||||
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 = {
|
const base = {
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: String(auth0.domain || "").trim(),
|
domain: String(auth0.domain || "").trim(),
|
||||||
@@ -743,7 +709,7 @@
|
|||||||
kakao: String(connections.kakao || "").trim(),
|
kakao: String(connections.kakao || "").trim(),
|
||||||
naver: String(connections.naver || "").trim(),
|
naver: String(connections.naver || "").trim(),
|
||||||
},
|
},
|
||||||
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
adminEmails: adminEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||||
};
|
};
|
||||||
const over = loadAuthOverride();
|
const over = loadAuthOverride();
|
||||||
if (!over) return base;
|
if (!over) return base;
|
||||||
@@ -757,7 +723,7 @@
|
|||||||
kakao: over.connections.kakao || base.connections.kakao,
|
kakao: over.connections.kakao || base.connections.kakao,
|
||||||
naver: over.connections.naver || base.connections.naver,
|
naver: over.connections.naver || base.connections.naver,
|
||||||
},
|
},
|
||||||
allowedEmails: over.allowedEmails.length ? over.allowedEmails : base.allowedEmails,
|
adminEmails: over.adminEmails.length ? over.adminEmails : base.adminEmails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
161
script.js
161
script.js
@@ -10,7 +10,6 @@
|
|||||||
const THEME_KEY = "links_home_theme_v1";
|
const THEME_KEY = "links_home_theme_v1";
|
||||||
const AUTH_TOAST_ONCE_KEY = "links_home_auth_toast_once_v1";
|
const AUTH_TOAST_ONCE_KEY = "links_home_auth_toast_once_v1";
|
||||||
const AUTH_OVERRIDE_KEY = "links_home_auth_override_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_PKCE_KEY = "links_home_auth_pkce_v1";
|
||||||
const AUTH_TOKEN_KEY = "links_home_auth_tokens_v1";
|
const AUTH_TOKEN_KEY = "links_home_auth_tokens_v1";
|
||||||
|
|
||||||
@@ -31,14 +30,6 @@
|
|||||||
btnLogout: document.getElementById("btnLogout"),
|
btnLogout: document.getElementById("btnLogout"),
|
||||||
snsLogin: document.getElementById("snsLogin"),
|
snsLogin: document.getElementById("snsLogin"),
|
||||||
btnGoogle: document.getElementById("btnGoogle"),
|
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"),
|
modal: document.getElementById("modal"),
|
||||||
btnClose: document.getElementById("btnClose"),
|
btnClose: document.getElementById("btnClose"),
|
||||||
btnCancel: document.getElementById("btnCancel"),
|
btnCancel: document.getElementById("btnCancel"),
|
||||||
@@ -79,7 +70,7 @@
|
|||||||
"link-ncue-net",
|
"link-ncue-net",
|
||||||
"dreamgirl-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() {
|
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) : "";
|
||||||
@@ -88,7 +79,10 @@
|
|||||||
|
|
||||||
function isAdminEmail(email) {
|
function isAdminEmail(email) {
|
||||||
const e = String(email || "").trim().toLowerCase();
|
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) {
|
function canAccessLink(link) {
|
||||||
@@ -416,7 +410,12 @@
|
|||||||
const data = raw ? safeJsonParse(raw, null) : null;
|
const data = raw ? safeJsonParse(raw, null) : null;
|
||||||
if (!data || typeof data !== "object") return null;
|
if (!data || typeof data !== "object") return null;
|
||||||
const auth0 = data.auth0 && typeof data.auth0 === "object" ? data.auth0 : {};
|
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 {
|
return {
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: String(auth0.domain || "").trim(),
|
domain: String(auth0.domain || "").trim(),
|
||||||
@@ -431,7 +430,7 @@
|
|||||||
naver: String(data.connections.naver || "").trim(),
|
naver: String(data.connections.naver || "").trim(),
|
||||||
}
|
}
|
||||||
: { google: "", kakao: "", naver: "" },
|
: { 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() {
|
function getAuthConfig() {
|
||||||
const cfg = globalThis.AUTH_CONFIG && typeof globalThis.AUTH_CONFIG === "object" ? globalThis.AUTH_CONFIG : {};
|
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 apiBase = String(cfg.apiBase || "").trim(); // optional, e.g. https://api.ncue.net
|
||||||
const auth0 = cfg.auth0 && typeof cfg.auth0 === "object" ? cfg.auth0 : {};
|
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 = {
|
const base = {
|
||||||
allowEndUserConfig,
|
|
||||||
apiBase,
|
apiBase,
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: String(auth0.domain || "").trim(),
|
domain: String(auth0.domain || "").trim(),
|
||||||
@@ -461,13 +463,12 @@
|
|||||||
kakao: "",
|
kakao: "",
|
||||||
naver: "",
|
naver: "",
|
||||||
},
|
},
|
||||||
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
adminEmails: adminEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||||
};
|
};
|
||||||
const override = loadAuthOverride();
|
const override = loadAuthOverride();
|
||||||
if (!override) return base;
|
if (!override) return base;
|
||||||
// override가 있으면 우선 적용 (서버 재배포 없이 테스트 가능)
|
// override가 있으면 우선 적용 (서버 재배포 없이 테스트 가능)
|
||||||
return {
|
return {
|
||||||
allowEndUserConfig,
|
|
||||||
apiBase,
|
apiBase,
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: override.auth0.domain || base.auth0.domain,
|
domain: override.auth0.domain || base.auth0.domain,
|
||||||
@@ -478,7 +479,7 @@
|
|||||||
kakao: override.connections?.kakao || "",
|
kakao: override.connections?.kakao || "",
|
||||||
naver: override.connections?.naver || "",
|
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 v = data.value;
|
||||||
const auth0 = v.auth0 || {};
|
const auth0 = v.auth0 || {};
|
||||||
const connections = v.connections || {};
|
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 domain = String(auth0.domain || "").trim();
|
||||||
const clientId = String(auth0.clientId || "").trim();
|
const clientId = String(auth0.clientId || "").trim();
|
||||||
const google = String(connections.google || "").trim();
|
const google = String(connections.google || "").trim();
|
||||||
@@ -513,7 +519,7 @@
|
|||||||
saveAuthOverride({
|
saveAuthOverride({
|
||||||
auth0: { domain, clientId },
|
auth0: { domain, clientId },
|
||||||
connections: { google },
|
connections: { google },
|
||||||
allowedEmails,
|
adminEmails,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -597,7 +603,7 @@
|
|||||||
async function manualAuthorize(connection) {
|
async function manualAuthorize(connection) {
|
||||||
const cfg = getAuthConfig();
|
const cfg = getAuthConfig();
|
||||||
if (!cfg.auth0.domain || !cfg.auth0.clientId) {
|
if (!cfg.auth0.domain || !cfg.auth0.clientId) {
|
||||||
openAuthModal();
|
toast("로그인 설정이 서버(.env)에 없습니다. 관리자에게 문의하세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!globalThis.crypto || !crypto.subtle) {
|
if (!globalThis.crypto || !crypto.subtle) {
|
||||||
@@ -745,11 +751,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAllowedEmail(email) {
|
function isManageAdminEmail(email) {
|
||||||
const { allowedEmails } = getAuthConfig();
|
const cfg = getAuthConfig();
|
||||||
if (!allowedEmails.length) return true; // 설정이 비어있으면 로그인만으로 허용
|
const admins = Array.isArray(cfg.adminEmails) ? cfg.adminEmails : [];
|
||||||
const e = String(email || "").trim().toLowerCase();
|
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() {
|
function updateAuthUi() {
|
||||||
@@ -787,7 +795,7 @@
|
|||||||
// 로그인 기능이 "enabled"일 때만 관리 잠금을 적용합니다.
|
// 로그인 기능이 "enabled"일 때만 관리 잠금을 적용합니다.
|
||||||
state.canManage = auth.mode === "enabled" ? Boolean(auth.user && auth.authorized) : true;
|
state.canManage = auth.mode === "enabled" ? Boolean(auth.user && auth.authorized) : true;
|
||||||
|
|
||||||
const lockMsg = "관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.";
|
const lockMsg = "관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.";
|
||||||
el.btnAdd.disabled = !state.canManage;
|
el.btnAdd.disabled = !state.canManage;
|
||||||
el.btnImport.disabled = !state.canManage;
|
el.btnImport.disabled = !state.canManage;
|
||||||
|
|
||||||
@@ -814,11 +822,7 @@
|
|||||||
auth.mode = "misconfigured";
|
auth.mode = "misconfigured";
|
||||||
updateAuthUi();
|
updateAuthUi();
|
||||||
applyManageLock();
|
applyManageLock();
|
||||||
// 브라우저당 1회: 설정 모달 자동 오픈
|
toastOnce("misconf", "로그인 설정이 서버(.env)에 없습니다. 관리자에게 문의하세요.");
|
||||||
if (!localStorage.getItem(AUTH_SETUP_SHOWN_KEY)) {
|
|
||||||
localStorage.setItem(AUTH_SETUP_SHOWN_KEY, "1");
|
|
||||||
setTimeout(() => openAuthModal(), 80);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +833,7 @@
|
|||||||
await manualHandleCallbackIfNeeded().catch(() => {});
|
await manualHandleCallbackIfNeeded().catch(() => {});
|
||||||
auth.user = await manualLoadUser();
|
auth.user = await manualLoadUser();
|
||||||
const email = auth.user && auth.user.email ? String(auth.user.email) : "";
|
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;
|
auth.serverCanManage = null;
|
||||||
const t = loadTokens();
|
const t = loadTokens();
|
||||||
if (auth.user && t && t.id_token) {
|
if (auth.user && t && t.id_token) {
|
||||||
@@ -883,7 +887,7 @@
|
|||||||
const isAuthed = await auth.client.isAuthenticated();
|
const isAuthed = await auth.client.isAuthenticated();
|
||||||
auth.user = isAuthed ? await auth.client.getUser() : null;
|
auth.user = isAuthed ? await auth.client.getUser() : null;
|
||||||
const email = auth.user && auth.user.email ? auth.user.email : "";
|
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;
|
auth.serverCanManage = null;
|
||||||
|
|
||||||
// 로그인되었으면 서버에 사용자 upsert 및 can_manage 동기화(서버가 있을 때만)
|
// 로그인되었으면 서버에 사용자 upsert 및 can_manage 동기화(서버가 있을 때만)
|
||||||
@@ -893,7 +897,7 @@
|
|||||||
const raw = claims && claims.__raw ? String(claims.__raw) : "";
|
const raw = claims && claims.__raw ? String(claims.__raw) : "";
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const cfg = getAuthConfig();
|
const cfg = getAuthConfig();
|
||||||
const r = await fetch("/api/auth/sync", {
|
const r = await fetch(apiUrl("/api/auth/sync"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${raw}`,
|
Authorization: `Bearer ${raw}`,
|
||||||
@@ -915,9 +919,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auth.user && !auth.authorized) {
|
if (auth.user && !auth.authorized) {
|
||||||
toastOnce("deny", "로그인은 되었지만 허용 이메일이 아니라서 관리 기능이 잠금 상태입니다.");
|
toastOnce("deny", "로그인은 되었지만 관리자 이메일이 아니라서 관리 기능이 잠금 상태입니다.");
|
||||||
} else if (auth.user && getAuthConfig().allowedEmails.length === 0) {
|
|
||||||
toastOnce("allowall", "주의: 허용 이메일 목록이 비어있어서 로그인한 모든 계정이 관리 가능 상태입니다.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuthUi();
|
updateAuthUi();
|
||||||
@@ -926,7 +928,7 @@
|
|||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
if (auth.mode !== "enabled" || !auth.client) {
|
if (auth.mode !== "enabled" || !auth.client) {
|
||||||
openAuthModal();
|
toast("로그인 설정이 서버(.env)에 필요합니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await auth.client.loginWithRedirect();
|
await auth.client.loginWithRedirect();
|
||||||
@@ -934,7 +936,7 @@
|
|||||||
|
|
||||||
async function loginWithConnection(connection) {
|
async function loginWithConnection(connection) {
|
||||||
if (auth.mode !== "enabled") {
|
if (auth.mode !== "enabled") {
|
||||||
openAuthModal();
|
toast("로그인 설정이 서버(.env)에 필요합니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (auth.client) {
|
if (auth.client) {
|
||||||
@@ -983,29 +985,6 @@
|
|||||||
location.assign(u.toString());
|
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) {
|
async function copyText(text) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
@@ -1173,7 +1152,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (auth.mode === "enabled" && !state.canManage && (act === "fav" || act === "edit" || act === "del")) {
|
if (auth.mode === "enabled" && !state.canManage && (act === "fav" || act === "edit" || act === "del")) {
|
||||||
toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
|
toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (act === "fav") {
|
if (act === "fav") {
|
||||||
@@ -1281,7 +1260,7 @@
|
|||||||
|
|
||||||
el.btnAdd.addEventListener("click", () => {
|
el.btnAdd.addEventListener("click", () => {
|
||||||
if (auth.mode === "enabled" && !state.canManage)
|
if (auth.mode === "enabled" && !state.canManage)
|
||||||
return toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
|
return toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
|
||||||
openModal("add", null);
|
openModal("add", null);
|
||||||
});
|
});
|
||||||
el.btnClose.addEventListener("click", closeModal);
|
el.btnClose.addEventListener("click", closeModal);
|
||||||
@@ -1299,7 +1278,7 @@
|
|||||||
el.btnExport.addEventListener("click", exportJson);
|
el.btnExport.addEventListener("click", exportJson);
|
||||||
el.btnImport.addEventListener("click", () => {
|
el.btnImport.addEventListener("click", () => {
|
||||||
if (auth.mode === "enabled" && !state.canManage)
|
if (auth.mode === "enabled" && !state.canManage)
|
||||||
return toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
|
return toast("관리 기능은 로그인(관리자 이메일) 후 사용 가능합니다.");
|
||||||
el.file.click();
|
el.file.click();
|
||||||
});
|
});
|
||||||
el.file.addEventListener("change", async () => {
|
el.file.addEventListener("change", async () => {
|
||||||
@@ -1324,57 +1303,9 @@
|
|||||||
if (el.btnGoogle)
|
if (el.btnGoogle)
|
||||||
el.btnGoogle.addEventListener("click", () => {
|
el.btnGoogle.addEventListener("click", () => {
|
||||||
const c = getAuthConfig().connections.google;
|
const c = getAuthConfig().connections.google;
|
||||||
if (!c) return openAuthModal();
|
if (!c) return toast("서버(.env)에 AUTH0_GOOGLE_CONNECTION 설정이 필요합니다.");
|
||||||
return loginWithConnection(c).catch(() => toast("로그인에 실패했습니다."));
|
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() {
|
async function main() {
|
||||||
|
|||||||
63
server.js
63
server.js
@@ -25,6 +25,17 @@ function safeIdent(s) {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCsv(s) {
|
||||||
|
return String(s || "")
|
||||||
|
.split(",")
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEmailCsv(s) {
|
||||||
|
return parseCsv(s).map((x) => x.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const PORT = Number(env("PORT", "8000")) || 8000;
|
const PORT = Number(env("PORT", "8000")) || 8000;
|
||||||
const DB_HOST = must("DB_HOST");
|
const DB_HOST = must("DB_HOST");
|
||||||
const DB_PORT = Number(env("DB_PORT", "5432")) || 5432;
|
const DB_PORT = Number(env("DB_PORT", "5432")) || 5432;
|
||||||
@@ -34,7 +45,12 @@ const DB_PASSWORD = must("DB_PASSWORD");
|
|||||||
const TABLE = safeIdent(env("TABLE", "ncue_user") || "ncue_user");
|
const TABLE = safeIdent(env("TABLE", "ncue_user") || "ncue_user");
|
||||||
const CONFIG_TABLE = "ncue_app_config";
|
const CONFIG_TABLE = "ncue_app_config";
|
||||||
const CONFIG_TOKEN = env("CONFIG_TOKEN", "").trim();
|
const CONFIG_TOKEN = env("CONFIG_TOKEN", "").trim();
|
||||||
const ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]);
|
const ADMIN_EMAILS = new Set(parseEmailCsv(env("ADMIN_EMAILS", "dosangyoon@gmail.com,dsyoon@ncue.net")));
|
||||||
|
|
||||||
|
// Auth0 config via .env (preferred)
|
||||||
|
const AUTH0_DOMAIN = env("AUTH0_DOMAIN", "").trim();
|
||||||
|
const AUTH0_CLIENT_ID = env("AUTH0_CLIENT_ID", "").trim();
|
||||||
|
const AUTH0_GOOGLE_CONNECTION = env("AUTH0_GOOGLE_CONNECTION", "").trim();
|
||||||
|
|
||||||
const pool = new pg.Pool({
|
const pool = new pg.Pool({
|
||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
@@ -133,14 +149,15 @@ app.post("/api/auth/sync", async (req, res) => {
|
|||||||
const name = payload.name ? String(payload.name).trim() : null;
|
const name = payload.name ? String(payload.name).trim() : null;
|
||||||
const picture = payload.picture ? String(payload.picture).trim() : null;
|
const picture = payload.picture ? String(payload.picture).trim() : null;
|
||||||
const provider = sub.includes("|") ? sub.split("|", 1)[0] : null;
|
const provider = sub.includes("|") ? sub.split("|", 1)[0] : null;
|
||||||
|
const isAdmin = email ? isAdminEmail(email) : false;
|
||||||
|
|
||||||
if (!sub) return res.status(400).json({ ok: false, error: "missing_sub" });
|
if (!sub) return res.status(400).json({ ok: false, error: "missing_sub" });
|
||||||
|
|
||||||
const q = `
|
const q = `
|
||||||
insert into public.${TABLE}
|
insert into public.${TABLE}
|
||||||
(sub, email, name, picture, provider, first_login_at, last_login_at, updated_at)
|
(sub, email, name, picture, provider, first_login_at, last_login_at, can_manage, updated_at)
|
||||||
values
|
values
|
||||||
($1, $2, $3, $4, $5, now(), now(), now())
|
($1, $2, $3, $4, $5, now(), now(), $6, now())
|
||||||
on conflict (sub) do update set
|
on conflict (sub) do update set
|
||||||
email = excluded.email,
|
email = excluded.email,
|
||||||
name = excluded.name,
|
name = excluded.name,
|
||||||
@@ -148,10 +165,11 @@ app.post("/api/auth/sync", async (req, res) => {
|
|||||||
provider = excluded.provider,
|
provider = excluded.provider,
|
||||||
first_login_at = coalesce(public.${TABLE}.first_login_at, excluded.first_login_at),
|
first_login_at = coalesce(public.${TABLE}.first_login_at, excluded.first_login_at),
|
||||||
last_login_at = now(),
|
last_login_at = now(),
|
||||||
|
can_manage = (public.${TABLE}.can_manage or $6),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
returning can_manage, first_login_at, last_login_at, last_logout_at
|
returning can_manage, first_login_at, last_login_at, last_logout_at
|
||||||
`;
|
`;
|
||||||
const r = await pool.query(q, [sub, email, name, picture, provider]);
|
const r = await pool.query(q, [sub, email, name, picture, provider, isAdmin]);
|
||||||
const canManage = Boolean(r.rows?.[0]?.can_manage);
|
const canManage = Boolean(r.rows?.[0]?.can_manage);
|
||||||
|
|
||||||
res.json({ ok: true, canManage, user: r.rows?.[0] || null });
|
res.json({ ok: true, canManage, user: r.rows?.[0] || null });
|
||||||
@@ -191,10 +209,29 @@ app.post("/api/auth/logout", async (req, res) => {
|
|||||||
// Shared auth config for all browsers (read-only public)
|
// Shared auth config for all browsers (read-only public)
|
||||||
app.get("/api/config/auth", async (_req, res) => {
|
app.get("/api/config/auth", async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Prefer .env config (no UI needed)
|
||||||
|
if (AUTH0_DOMAIN && AUTH0_CLIENT_ID && AUTH0_GOOGLE_CONNECTION) {
|
||||||
|
return res.json({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
auth0: { domain: AUTH0_DOMAIN, clientId: AUTH0_CLIENT_ID },
|
||||||
|
connections: { google: AUTH0_GOOGLE_CONNECTION },
|
||||||
|
adminEmails: [...ADMIN_EMAILS],
|
||||||
|
},
|
||||||
|
updated_at: null,
|
||||||
|
source: "env",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await ensureConfigTable();
|
await ensureConfigTable();
|
||||||
const r = await pool.query(`select value, updated_at from public.${CONFIG_TABLE} where key = $1`, ["auth"]);
|
const r = await pool.query(`select value, updated_at from public.${CONFIG_TABLE} where key = $1`, ["auth"]);
|
||||||
if (!r.rows?.length) return res.status(404).json({ ok: false, error: "not_set" });
|
if (!r.rows?.length) return res.status(404).json({ ok: false, error: "not_set" });
|
||||||
res.json({ ok: true, value: r.rows[0].value, updated_at: r.rows[0].updated_at });
|
const v = r.rows[0].value || {};
|
||||||
|
// legacy: allowedEmails -> adminEmails
|
||||||
|
if (v && typeof v === "object" && !v.adminEmails && Array.isArray(v.allowedEmails)) {
|
||||||
|
v.adminEmails = v.allowedEmails;
|
||||||
|
}
|
||||||
|
res.json({ ok: true, value: v, updated_at: r.rows[0].updated_at, source: "db" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ ok: false, error: "server_error" });
|
res.status(500).json({ ok: false, error: "server_error" });
|
||||||
}
|
}
|
||||||
@@ -211,26 +248,26 @@ app.post("/api/config/auth", async (req, res) => {
|
|||||||
const body = req.body && typeof req.body === "object" ? req.body : {};
|
const body = req.body && typeof req.body === "object" ? req.body : {};
|
||||||
const auth0 = body.auth0 && typeof body.auth0 === "object" ? body.auth0 : {};
|
const auth0 = body.auth0 && typeof body.auth0 === "object" ? body.auth0 : {};
|
||||||
const connections = body.connections && typeof body.connections === "object" ? body.connections : {};
|
const connections = body.connections && typeof body.connections === "object" ? body.connections : {};
|
||||||
const allowedEmails = Array.isArray(body.allowedEmails) ? body.allowedEmails : [];
|
// legacy: allowedEmails -> adminEmails
|
||||||
|
const adminEmails = Array.isArray(body.adminEmails)
|
||||||
|
? body.adminEmails
|
||||||
|
: Array.isArray(body.allowedEmails)
|
||||||
|
? body.allowedEmails
|
||||||
|
: [];
|
||||||
|
|
||||||
const domain = String(auth0.domain || "").trim();
|
const domain = String(auth0.domain || "").trim();
|
||||||
const clientId = String(auth0.clientId || "").trim();
|
const clientId = String(auth0.clientId || "").trim();
|
||||||
const googleConn = String(connections.google || "").trim();
|
const googleConn = String(connections.google || "").trim();
|
||||||
const emails = allowedEmails.map((x) => String(x).trim().toLowerCase()).filter(Boolean);
|
const emails = adminEmails.map((x) => String(x).trim().toLowerCase()).filter(Boolean);
|
||||||
|
|
||||||
if (!domain || !clientId || !googleConn) {
|
if (!domain || !clientId || !googleConn) {
|
||||||
return res.status(400).json({ ok: false, error: "missing_fields" });
|
return res.status(400).json({ ok: false, error: "missing_fields" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional safety: ensure at least one admin is present in allowedEmails
|
|
||||||
if (emails.length && !emails.some(isAdminEmail)) {
|
|
||||||
return res.status(400).json({ ok: false, error: "admin_email_missing" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
auth0: { domain, clientId },
|
auth0: { domain, clientId },
|
||||||
connections: { google: googleConn },
|
connections: { google: googleConn },
|
||||||
allowedEmails: emails,
|
adminEmails: emails,
|
||||||
};
|
};
|
||||||
|
|
||||||
await pool.query(
|
await pool.query(
|
||||||
|
|||||||
Reference in New Issue
Block a user