From c2ba78db5cbcd5b805969da232c2e176060c5fee Mon Sep 17 00:00:00 2001 From: dsyoon Date: Sat, 7 Feb 2026 21:16:37 +0900 Subject: [PATCH] Persist shared auth config in DB Add ncue_app_config and /api/config/auth endpoints, and hydrate Auth0 config from the server so other browsers don't see the setup modal. Co-authored-by: Cursor --- db/schema.sql | 8 ++++++ index.html | 2 ++ script.js | 29 ++++++++++++++++++++ server.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/db/schema.sql b/db/schema.sql index 64f3dcc..cc40004 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -31,3 +31,11 @@ END $$; ALTER TABLE public.ncue_user ADD COLUMN IF NOT EXISTS first_login_at timestamptz; ALTER TABLE public.ncue_user ADD COLUMN IF NOT EXISTS last_logout_at timestamptz; +-- App config (shared across browsers) +CREATE TABLE IF NOT EXISTS public.ncue_app_config ( + key text PRIMARY KEY, + value jsonb NOT NULL, + updated_at timestamptz NOT NULL DEFAULT now() +); + + diff --git a/index.html b/index.html index 21682f1..4443437 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,8 @@ window.AUTH_CONFIG = { // end-user가 설정 모달을 사용하는지 여부(기본: false) allowEndUserConfig: false, + // (선택) API 서버가 다른 도메인이면 지정. 예: https://api.ncue.net + apiBase: "", auth0: { domain: "", clientId: "", diff --git a/script.js b/script.js index 8dcd18d..71f86c8 100644 --- a/script.js +++ b/script.js @@ -493,6 +493,34 @@ } } + 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 allowedEmails = 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; + saveAuthOverride({ + auth0: { domain, clientId }, + connections: { google }, + allowedEmails, + }); + return true; + } catch { + return false; + } + } + function currentUrlNoQuery() { // Auth0 callback 후 URL 정리용 const u = new URL(location.href); @@ -1352,6 +1380,7 @@ async function main() { initTheme(); wire(); + await hydrateAuthConfigFromServerIfNeeded(); await initAuth(); state.baseLinks = await loadBaseLinks(); state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i])); diff --git a/server.js b/server.js index 13a0806..0ac200b 100644 --- a/server.js +++ b/server.js @@ -32,6 +32,9 @@ const DB_NAME = must("DB_NAME"); const DB_USER = must("DB_USER"); const DB_PASSWORD = must("DB_PASSWORD"); const TABLE = safeIdent(env("TABLE", "ncue_user") || "ncue_user"); +const CONFIG_TABLE = "ncue_app_config"; +const CONFIG_TOKEN = env("CONFIG_TOKEN", "").trim(); +const ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]); const pool = new pg.Pool({ host: DB_HOST, @@ -98,6 +101,21 @@ async function ensureUserTable() { await pool.query(`alter table public.${TABLE} add column if not exists last_logout_at timestamptz`); } +async function ensureConfigTable() { + await pool.query(` + create table if not exists public.${CONFIG_TABLE} ( + key text primary key, + value jsonb not null, + updated_at timestamptz not null default now() + ) + `); +} + +function isAdminEmail(email) { + const e = String(email || "").trim().toLowerCase(); + return ADMIN_EMAILS.has(e); +} + app.post("/api/auth/sync", async (req, res) => { try { await ensureUserTable(); @@ -170,6 +188,63 @@ app.post("/api/auth/logout", async (req, res) => { } }); +// Shared auth config for all browsers (read-only public) +app.get("/api/config/auth", async (_req, res) => { + try { + await ensureConfigTable(); + 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" }); + res.json({ ok: true, value: r.rows[0].value, updated_at: r.rows[0].updated_at }); + } catch (e) { + res.status(500).json({ ok: false, error: "server_error" }); + } +}); + +// Write auth config (protected by CONFIG_TOKEN) +app.post("/api/config/auth", async (req, res) => { + try { + await ensureConfigTable(); + if (!CONFIG_TOKEN) return res.status(403).json({ ok: false, error: "config_token_not_set" }); + const token = String(req.headers["x-config-token"] || "").trim(); + if (token !== CONFIG_TOKEN) return res.status(403).json({ ok: false, error: "forbidden" }); + + const body = req.body && typeof req.body === "object" ? req.body : {}; + const auth0 = body.auth0 && typeof body.auth0 === "object" ? body.auth0 : {}; + const connections = body.connections && typeof body.connections === "object" ? body.connections : {}; + const allowedEmails = Array.isArray(body.allowedEmails) ? body.allowedEmails : []; + + const domain = String(auth0.domain || "").trim(); + const clientId = String(auth0.clientId || "").trim(); + const googleConn = String(connections.google || "").trim(); + const emails = allowedEmails.map((x) => String(x).trim().toLowerCase()).filter(Boolean); + + if (!domain || !clientId || !googleConn) { + 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 = { + auth0: { domain, clientId }, + connections: { google: googleConn }, + allowedEmails: emails, + }; + + await pool.query( + `insert into public.${CONFIG_TABLE} (key, value, updated_at) + values ($1, $2::jsonb, now()) + on conflict (key) do update set value = excluded.value, updated_at = now()`, + ["auth", JSON.stringify(value)] + ); + res.json({ ok: true }); + } catch (e) { + res.status(500).json({ ok: false, error: "server_error" }); + } +}); + // Serve static site app.use(express.static(__dirname, { extensions: ["html"] }));