Add in-page login config modal
Allow setting Auth0 domain/clientId and allowed emails via a modal saved to localStorage to enable login testing without redeploying. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,6 +32,11 @@ python3 -m http.server 8000
|
|||||||
- Allowed Logout URLs: 사이트 주소 (예: `https://example.com/`)
|
- Allowed Logout URLs: 사이트 주소 (예: `https://example.com/`)
|
||||||
4. `allowedEmails`에 관리 허용 이메일 목록을 입력
|
4. `allowedEmails`에 관리 허용 이메일 목록을 입력
|
||||||
|
|
||||||
|
팁:
|
||||||
|
|
||||||
|
- 서버에 바로 반영하기 전 테스트가 필요하면, 페이지 상단의 **로그인**을 누르면 뜨는 **로그인 설정 모달**에서
|
||||||
|
`domain/clientId/allowedEmails`를 입력하면 브라우저에 저장되어 즉시 테스트할 수 있습니다.
|
||||||
|
|
||||||
## 데이터 저장
|
## 데이터 저장
|
||||||
|
|
||||||
- 기본 링크: `links.json`
|
- 기본 링크: `links.json`
|
||||||
|
|||||||
36
index.html
36
index.html
@@ -175,6 +175,42 @@
|
|||||||
</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>
|
||||||
|
|
||||||
|
<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 />
|
||||||
|
|
||||||
|
|||||||
119
script.js
119
script.js
@@ -4,6 +4,7 @@
|
|||||||
const STORAGE_KEY = "links_home_v1";
|
const STORAGE_KEY = "links_home_v1";
|
||||||
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 el = {
|
const el = {
|
||||||
subtitle: document.getElementById("subtitle"),
|
subtitle: document.getElementById("subtitle"),
|
||||||
@@ -21,6 +22,13 @@
|
|||||||
userText: document.getElementById("userText"),
|
userText: document.getElementById("userText"),
|
||||||
btnLogin: document.getElementById("btnLogin"),
|
btnLogin: document.getElementById("btnLogin"),
|
||||||
btnLogout: document.getElementById("btnLogout"),
|
btnLogout: document.getElementById("btnLogout"),
|
||||||
|
authModal: document.getElementById("authModal"),
|
||||||
|
btnAuthClose: document.getElementById("btnAuthClose"),
|
||||||
|
authForm: document.getElementById("authForm"),
|
||||||
|
authDomain: document.getElementById("authDomain"),
|
||||||
|
authClientId: document.getElementById("authClientId"),
|
||||||
|
authAllowedEmails: document.getElementById("authAllowedEmails"),
|
||||||
|
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"),
|
||||||
@@ -339,10 +347,12 @@
|
|||||||
toast(msg);
|
toast(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthConfig() {
|
function loadAuthOverride() {
|
||||||
const cfg = globalThis.AUTH_CONFIG && typeof globalThis.AUTH_CONFIG === "object" ? globalThis.AUTH_CONFIG : {};
|
const raw = localStorage.getItem(AUTH_OVERRIDE_KEY);
|
||||||
const auth0 = cfg.auth0 && typeof cfg.auth0 === "object" ? cfg.auth0 : {};
|
const data = raw ? safeJsonParse(raw, null) : null;
|
||||||
const allowedEmails = Array.isArray(cfg.allowedEmails) ? cfg.allowedEmails : [];
|
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 : [];
|
||||||
return {
|
return {
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: String(auth0.domain || "").trim(),
|
domain: String(auth0.domain || "").trim(),
|
||||||
@@ -352,6 +362,37 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveAuthOverride(cfg) {
|
||||||
|
localStorage.setItem(AUTH_OVERRIDE_KEY, JSON.stringify(cfg));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuthOverride() {
|
||||||
|
localStorage.removeItem(AUTH_OVERRIDE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthConfig() {
|
||||||
|
const cfg = globalThis.AUTH_CONFIG && typeof globalThis.AUTH_CONFIG === "object" ? globalThis.AUTH_CONFIG : {};
|
||||||
|
const auth0 = cfg.auth0 && typeof cfg.auth0 === "object" ? cfg.auth0 : {};
|
||||||
|
const allowedEmails = Array.isArray(cfg.allowedEmails) ? cfg.allowedEmails : [];
|
||||||
|
const base = {
|
||||||
|
auth0: {
|
||||||
|
domain: String(auth0.domain || "").trim(),
|
||||||
|
clientId: String(auth0.clientId || "").trim(),
|
||||||
|
},
|
||||||
|
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
|
||||||
|
};
|
||||||
|
const override = loadAuthOverride();
|
||||||
|
if (!override) return base;
|
||||||
|
// override가 있으면 우선 적용 (서버 재배포 없이 테스트 가능)
|
||||||
|
return {
|
||||||
|
auth0: {
|
||||||
|
domain: override.auth0.domain || base.auth0.domain,
|
||||||
|
clientId: override.auth0.clientId || base.auth0.clientId,
|
||||||
|
},
|
||||||
|
allowedEmails: override.allowedEmails.length ? override.allowedEmails : base.allowedEmails,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function currentUrlNoQuery() {
|
function currentUrlNoQuery() {
|
||||||
// Auth0 callback 후 URL 정리용
|
// Auth0 callback 후 URL 정리용
|
||||||
const u = new URL(location.href);
|
const u = new URL(location.href);
|
||||||
@@ -491,7 +532,7 @@
|
|||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
if (auth.mode !== "enabled" || !auth.client) {
|
if (auth.mode !== "enabled" || !auth.client) {
|
||||||
toast("로그인 설정이 필요합니다. index.html의 AUTH_CONFIG(auth0.domain/clientId, allowedEmails)를 확인하세요.");
|
openAuthModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await auth.client.loginWithRedirect();
|
await auth.client.loginWithRedirect();
|
||||||
@@ -510,6 +551,28 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(", ");
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -820,6 +883,52 @@
|
|||||||
|
|
||||||
if (el.btnLogin) el.btnLogin.addEventListener("click", () => login().catch(() => toast("로그인에 실패했습니다.")));
|
if (el.btnLogin) el.btnLogin.addEventListener("click", () => login().catch(() => toast("로그인에 실패했습니다.")));
|
||||||
if (el.btnLogout) el.btnLogout.addEventListener("click", () => logout().catch(() => toast("로그아웃에 실패했습니다.")));
|
if (el.btnLogout) el.btnLogout.addEventListener("click", () => logout().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);
|
||||||
|
|
||||||
|
if (!domain || !clientId) {
|
||||||
|
toast("Domain과 Client ID를 입력하세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAuthOverride({
|
||||||
|
auth0: { domain, clientId },
|
||||||
|
allowedEmails: emails,
|
||||||
|
});
|
||||||
|
toast("저장했습니다. 페이지를 새로고침합니다.");
|
||||||
|
closeAuthModal();
|
||||||
|
setTimeout(() => location.reload(), 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user