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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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 first_login_at timestamptz;
|
||||||
ALTER TABLE public.ncue_user ADD COLUMN IF NOT EXISTS last_logout_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()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
window.AUTH_CONFIG = {
|
window.AUTH_CONFIG = {
|
||||||
// end-user가 설정 모달을 사용하는지 여부(기본: false)
|
// end-user가 설정 모달을 사용하는지 여부(기본: false)
|
||||||
allowEndUserConfig: false,
|
allowEndUserConfig: false,
|
||||||
|
// (선택) API 서버가 다른 도메인이면 지정. 예: https://api.ncue.net
|
||||||
|
apiBase: "",
|
||||||
auth0: {
|
auth0: {
|
||||||
domain: "",
|
domain: "",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
|
|||||||
29
script.js
29
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() {
|
function currentUrlNoQuery() {
|
||||||
// Auth0 callback 후 URL 정리용
|
// Auth0 callback 후 URL 정리용
|
||||||
const u = new URL(location.href);
|
const u = new URL(location.href);
|
||||||
@@ -1352,6 +1380,7 @@
|
|||||||
async function main() {
|
async function main() {
|
||||||
initTheme();
|
initTheme();
|
||||||
wire();
|
wire();
|
||||||
|
await hydrateAuthConfigFromServerIfNeeded();
|
||||||
await initAuth();
|
await initAuth();
|
||||||
state.baseLinks = await loadBaseLinks();
|
state.baseLinks = await loadBaseLinks();
|
||||||
state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i]));
|
state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i]));
|
||||||
|
|||||||
75
server.js
75
server.js
@@ -32,6 +32,9 @@ const DB_NAME = must("DB_NAME");
|
|||||||
const DB_USER = must("DB_USER");
|
const DB_USER = must("DB_USER");
|
||||||
const DB_PASSWORD = must("DB_PASSWORD");
|
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_TOKEN = env("CONFIG_TOKEN", "").trim();
|
||||||
|
const ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]);
|
||||||
|
|
||||||
const pool = new pg.Pool({
|
const pool = new pg.Pool({
|
||||||
host: DB_HOST,
|
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`);
|
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) => {
|
app.post("/api/auth/sync", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await ensureUserTable();
|
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
|
// Serve static site
|
||||||
app.use(express.static(__dirname, { extensions: ["html"] }));
|
app.use(express.static(__dirname, { extensions: ["html"] }));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user