DB 기반 관리자 권한으로 전환
- ncue_user에 is_admin 추가 및 can_manage 호환 유지 - /api/auth/sync 및 관리 API를 DB is_admin 기반으로 변경 - index.html 폴백에서 관리자 이메일 하드코딩 제거 - .env로 관리자 부트스트랩(ADMIN_EMAILS) 지원 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
60
server.js
60
server.js
@@ -45,7 +45,7 @@ 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(parseEmailCsv(env("ADMIN_EMAILS", "dosangyoon@gmail.com,dsyoon@ncue.net")));
|
||||
const ADMIN_EMAILS = new Set(parseEmailCsv(env("ADMIN_EMAILS", "")));
|
||||
|
||||
// Auth0 config via .env (preferred)
|
||||
const AUTH0_DOMAIN = env("AUTH0_DOMAIN", "").trim();
|
||||
@@ -107,6 +107,7 @@ async function ensureUserTable() {
|
||||
first_login_at timestamptz,
|
||||
last_login_at timestamptz,
|
||||
last_logout_at timestamptz,
|
||||
is_admin boolean not null default false,
|
||||
can_manage boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
@@ -115,6 +116,11 @@ async function ensureUserTable() {
|
||||
await pool.query(`create index if not exists idx_${TABLE}_email on public.${TABLE} (email)`);
|
||||
await pool.query(`alter table public.${TABLE} add column if not exists first_login_at timestamptz`);
|
||||
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 is_admin boolean not null default false`);
|
||||
// Backward-compat: previously can_manage was used as admin flag. Preserve existing admins.
|
||||
await pool.query(`update public.${TABLE} set is_admin = true where can_manage = true and is_admin = false`);
|
||||
// Keep can_manage consistent with is_admin going forward.
|
||||
await pool.query(`update public.${TABLE} set can_manage = is_admin where can_manage is distinct from is_admin`);
|
||||
}
|
||||
|
||||
async function ensureConfigTable() {
|
||||
@@ -127,11 +133,6 @@ async function ensureConfigTable() {
|
||||
`);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -149,15 +150,16 @@ 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 bootstrapAdmin = Boolean(email && ADMIN_EMAILS.has(email));
|
||||
|
||||
const q = `
|
||||
insert into public.${TABLE}
|
||||
(sub, email, name, picture, provider, first_login_at, last_login_at, can_manage, updated_at)
|
||||
(sub, email, name, picture, provider, first_login_at, last_login_at, is_admin, can_manage, updated_at)
|
||||
values
|
||||
($1, $2, $3, $4, $5, now(), now(), $6, now())
|
||||
($1, $2, $3, $4, $5, now(), now(), $6, $6, now())
|
||||
on conflict (sub) do update set
|
||||
email = excluded.email,
|
||||
name = excluded.name,
|
||||
@@ -165,14 +167,15 @@ 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),
|
||||
is_admin = (public.${TABLE}.is_admin or public.${TABLE}.can_manage or $6),
|
||||
can_manage = (public.${TABLE}.is_admin or public.${TABLE}.can_manage or $6),
|
||||
updated_at = now()
|
||||
returning can_manage, first_login_at, last_login_at, last_logout_at
|
||||
returning is_admin, can_manage, first_login_at, last_login_at, last_logout_at
|
||||
`;
|
||||
const r = await pool.query(q, [sub, email, name, picture, provider, isAdmin]);
|
||||
const canManage = Boolean(r.rows?.[0]?.can_manage);
|
||||
const r = await pool.query(q, [sub, email, name, picture, provider, bootstrapAdmin]);
|
||||
const isAdmin = Boolean(r.rows?.[0]?.is_admin);
|
||||
|
||||
res.json({ ok: true, canManage, user: r.rows?.[0] || null });
|
||||
res.json({ ok: true, canManage: isAdmin, user: r.rows?.[0] || null });
|
||||
} catch (e) {
|
||||
res.status(401).json({ ok: false, error: "verify_failed" });
|
||||
}
|
||||
@@ -216,7 +219,8 @@ app.get("/api/config/auth", async (_req, res) => {
|
||||
value: {
|
||||
auth0: { domain: AUTH0_DOMAIN, clientId: AUTH0_CLIENT_ID },
|
||||
connections: { google: AUTH0_GOOGLE_CONNECTION },
|
||||
adminEmails: [...ADMIN_EMAILS],
|
||||
// Deprecated: admin is stored per-user in DB (ncue_user.is_admin)
|
||||
adminEmails: [],
|
||||
},
|
||||
updated_at: null,
|
||||
source: "env",
|
||||
@@ -282,6 +286,32 @@ app.post("/api/config/auth", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// CONFIG_TOKEN-protected: set per-user admin flag in DB
|
||||
app.post("/api/admin/users/set_admin", async (req, res) => {
|
||||
try {
|
||||
await ensureUserTable();
|
||||
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 sub = String(body.sub || "").trim();
|
||||
const email = String(body.email || "").trim().toLowerCase();
|
||||
const isAdmin = typeof body.isAdmin === "boolean" ? body.isAdmin : typeof body.admin === "boolean" ? body.admin : null;
|
||||
if (typeof isAdmin !== "boolean") return res.status(400).json({ ok: false, error: "missing_isAdmin" });
|
||||
if (!sub && !email) return res.status(400).json({ ok: false, error: "missing_identifier" });
|
||||
|
||||
const q = sub
|
||||
? `update public.${TABLE} set is_admin = $1, can_manage = $1, updated_at = now() where sub = $2 returning sub, email, is_admin`
|
||||
: `update public.${TABLE} set is_admin = $1, can_manage = $1, updated_at = now() where email = $2 returning sub, email, is_admin`;
|
||||
const r = await pool.query(q, [isAdmin, sub ? sub : email]);
|
||||
if (!r.rows?.length) return res.status(404).json({ ok: false, error: "not_found" });
|
||||
res.json({ ok: true, user: r.rows[0] });
|
||||
} catch {
|
||||
res.status(500).json({ ok: false, error: "server_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static site
|
||||
app.use(express.static(__dirname, { extensions: ["html"] }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user