로그인 설정 UI 제거 및 .env 기반 설정

- 로그인 설정 모달(UI) 제거
- 허용 이메일 라벨을 관리자 이메일로 변경
- Auth0/관리자 이메일을 서버 .env에서 제공하고 클라이언트가 자동 로드

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-07 21:31:52 +09:00
parent c2ba78db5c
commit fb2153cbb0
5 changed files with 138 additions and 179 deletions

View File

@@ -25,6 +25,17 @@ function safeIdent(s) {
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 DB_HOST = must("DB_HOST");
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 CONFIG_TABLE = "ncue_app_config";
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({
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 picture = payload.picture ? String(payload.picture).trim() : 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" });
const q = `
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
($1, $2, $3, $4, $5, now(), now(), now())
($1, $2, $3, $4, $5, now(), now(), $6, now())
on conflict (sub) do update set
email = excluded.email,
name = excluded.name,
@@ -148,10 +165,11 @@ app.post("/api/auth/sync", async (req, res) => {
provider = excluded.provider,
first_login_at = coalesce(public.${TABLE}.first_login_at, excluded.first_login_at),
last_login_at = now(),
can_manage = (public.${TABLE}.can_manage or $6),
updated_at = now()
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);
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)
app.get("/api/config/auth", async (_req, res) => {
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();
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 });
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) {
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 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 : [];
// legacy: allowedEmails -> adminEmails
const adminEmails = Array.isArray(body.adminEmails)
? body.adminEmails
: 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);
const emails = adminEmails.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,
adminEmails: emails,
};
await pool.query(