Add social quick login and user sync API

Add quick provider login buttons (Auth0 connections), an API to upsert users into Postgres and gate admin via can_manage, plus schema and Node server.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-07 18:04:18 +09:00
parent 5e898d3e04
commit fac88b6508
7 changed files with 320 additions and 0 deletions

View File

@@ -22,12 +22,18 @@
userText: document.getElementById("userText"),
btnLogin: document.getElementById("btnLogin"),
btnLogout: document.getElementById("btnLogout"),
btnGoogle: document.getElementById("btnGoogle"),
btnKakao: document.getElementById("btnKakao"),
btnNaver: document.getElementById("btnNaver"),
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"),
authConnKakao: document.getElementById("authConnKakao"),
authConnNaver: document.getElementById("authConnNaver"),
btnAuthReset: document.getElementById("btnAuthReset"),
modal: document.getElementById("modal"),
btnClose: document.getElementById("btnClose"),
@@ -65,6 +71,7 @@
authorized: false,
ready: false,
mode: "disabled", // enabled | misconfigured | sdk_missing | disabled
serverCanManage: null,
};
function nowIso() {
@@ -358,6 +365,14 @@
domain: String(auth0.domain || "").trim(),
clientId: String(auth0.clientId || "").trim(),
},
connections:
data.connections && typeof data.connections === "object"
? {
google: String(data.connections.google || "").trim(),
kakao: String(data.connections.kakao || "").trim(),
naver: String(data.connections.naver || "").trim(),
}
: { google: "", kakao: "", naver: "" },
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
};
}
@@ -379,6 +394,11 @@
domain: String(auth0.domain || "").trim(),
clientId: String(auth0.clientId || "").trim(),
},
connections: {
google: "",
kakao: "",
naver: "",
},
allowedEmails: allowedEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean),
};
const override = loadAuthOverride();
@@ -389,6 +409,11 @@
domain: override.auth0.domain || base.auth0.domain,
clientId: override.auth0.clientId || base.auth0.clientId,
},
connections: {
google: override.connections?.google || "",
kakao: override.connections?.kakao || "",
naver: override.connections?.naver || "",
},
allowedEmails: override.allowedEmails.length ? override.allowedEmails : base.allowedEmails,
};
}
@@ -427,6 +452,16 @@
// 설정/SDK 문제 상태에서도 버튼은 "클릭 가능"하게 두고, 클릭 시 토스트로 안내합니다.
el.btnLogin.disabled = false;
el.btnLogout.disabled = false;
// 간편 로그인 버튼 노출 (connection이 설정되어 있고, 미로그인 상태)
const cfg = getAuthConfig();
const showQuick = enabled && !auth.user;
const g = showQuick && Boolean(cfg.connections.google);
const k = showQuick && Boolean(cfg.connections.kakao);
const n = showQuick && Boolean(cfg.connections.naver);
if (el.btnGoogle) el.btnGoogle.hidden = !g;
if (el.btnKakao) el.btnKakao.hidden = !k;
if (el.btnNaver) el.btnNaver.hidden = !n;
}
function applyManageLock() {
@@ -519,6 +554,35 @@
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.serverCanManage = null;
// 로그인되었으면 서버에 사용자 upsert 및 can_manage 동기화(서버가 있을 때만)
if (auth.user) {
try {
const claims = await auth.client.getIdTokenClaims();
const raw = claims && claims.__raw ? String(claims.__raw) : "";
if (raw) {
const cfg = getAuthConfig();
const r = await fetch("/api/auth/sync", {
method: "POST",
headers: {
Authorization: `Bearer ${raw}`,
"X-Auth0-Issuer": `https://${cfg.auth0.domain}/`,
"X-Auth0-ClientId": cfg.auth0.clientId,
},
});
if (r.ok) {
const data = await r.json();
if (data && data.ok) {
auth.serverCanManage = Boolean(data.canManage);
auth.authorized = auth.serverCanManage;
}
}
}
} catch {
// ignore: server not running or blocked
}
}
if (auth.user && !auth.authorized) {
toastOnce("deny", "로그인은 되었지만 허용 이메일이 아니라서 관리 기능이 잠금 상태입니다.");
@@ -538,6 +602,16 @@
await auth.client.loginWithRedirect();
}
async function loginWithConnection(connection) {
if (auth.mode !== "enabled" || !auth.client) {
openAuthModal();
return;
}
await auth.client.loginWithRedirect({
authorizationParams: { connection },
});
}
async function logout() {
if (auth.mode !== "enabled" || !auth.client) return;
auth.user = null;
@@ -561,6 +635,9 @@
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 || "";
if (el.authConnKakao) el.authConnKakao.value = cfg.connections.kakao || "";
if (el.authConnNaver) el.authConnNaver.value = cfg.connections.naver || "";
el.authModal.hidden = false;
document.body.style.overflow = "hidden";
@@ -883,6 +960,24 @@
if (el.btnLogin) el.btnLogin.addEventListener("click", () => login().catch(() => toast("로그인에 실패했습니다.")));
if (el.btnLogout) el.btnLogout.addEventListener("click", () => logout().catch(() => toast("로그아웃에 실패했습니다.")));
if (el.btnGoogle)
el.btnGoogle.addEventListener("click", () => {
const c = getAuthConfig().connections.google;
if (!c) return openAuthModal();
return loginWithConnection(c).catch(() => toast("로그인에 실패했습니다."));
});
if (el.btnKakao)
el.btnKakao.addEventListener("click", () => {
const c = getAuthConfig().connections.kakao;
if (!c) return openAuthModal();
return loginWithConnection(c).catch(() => toast("로그인에 실패했습니다."));
});
if (el.btnNaver)
el.btnNaver.addEventListener("click", () => {
const c = getAuthConfig().connections.naver;
if (!c) return openAuthModal();
return loginWithConnection(c).catch(() => toast("로그인에 실패했습니다."));
});
if (el.btnAuthClose) el.btnAuthClose.addEventListener("click", closeAuthModal);
if (el.authModal) {
@@ -914,6 +1009,9 @@
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
const connGoogle = el.authConnGoogle ? String(el.authConnGoogle.value || "").trim() : "";
const connKakao = el.authConnKakao ? String(el.authConnKakao.value || "").trim() : "";
const connNaver = el.authConnNaver ? String(el.authConnNaver.value || "").trim() : "";
if (!domain || !clientId) {
toast("Domain과 Client ID를 입력하세요.");
@@ -922,6 +1020,7 @@
saveAuthOverride({
auth0: { domain, clientId },
connections: { google: connGoogle, kakao: connKakao, naver: connNaver },
allowedEmails: emails,
});
toast("저장했습니다. 페이지를 새로고침합니다.");