feat(auth): expose ADMIN_EMAILS via /api/config/auth and grant SPA admin when email matches
Made-with: Cursor
This commit is contained in:
19
flask_app.py
19
flask_app.py
@@ -515,8 +515,8 @@ def api_config_auth_get() -> Response:
|
|||||||
"value": {
|
"value": {
|
||||||
"auth0": {"domain": AUTH0_DOMAIN, "clientId": AUTH0_CLIENT_ID},
|
"auth0": {"domain": AUTH0_DOMAIN, "clientId": AUTH0_CLIENT_ID},
|
||||||
"connections": {"google": AUTH0_GOOGLE_CONNECTION},
|
"connections": {"google": AUTH0_GOOGLE_CONNECTION},
|
||||||
# Deprecated: admin is stored per-user in DB (ncue_user.is_admin)
|
# Mirrors .env ADMIN_EMAILS for SPA: unlock UI when /api/auth/sync lags or misreads .env
|
||||||
"adminEmails": [],
|
"adminEmails": sorted(ADMIN_EMAILS),
|
||||||
},
|
},
|
||||||
"updated_at": None,
|
"updated_at": None,
|
||||||
"source": "env",
|
"source": "env",
|
||||||
@@ -534,9 +534,22 @@ def api_config_auth_get() -> Response:
|
|||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = json.loads(value)
|
value = json.loads(value)
|
||||||
|
|
||||||
if isinstance(value, dict) and "adminEmails" not in value and isinstance(value.get("allowedEmails"), list):
|
if not isinstance(value, dict):
|
||||||
|
return jsonify({"ok": False, "error": "not_set"}), 404
|
||||||
|
|
||||||
|
if "adminEmails" not in value and isinstance(value.get("allowedEmails"), list):
|
||||||
value["adminEmails"] = value.get("allowedEmails")
|
value["adminEmails"] = value.get("allowedEmails")
|
||||||
|
|
||||||
|
merged: set[str] = set()
|
||||||
|
ae = value.get("adminEmails")
|
||||||
|
if isinstance(ae, list):
|
||||||
|
for x in ae:
|
||||||
|
if isinstance(x, str) and x.strip():
|
||||||
|
merged.add(x.strip().lower())
|
||||||
|
merged |= ADMIN_EMAILS
|
||||||
|
value = dict(value)
|
||||||
|
value["adminEmails"] = sorted(merged)
|
||||||
|
|
||||||
return jsonify({"ok": True, "value": value, "updated_at": row[1], "source": "db"})
|
return jsonify({"ok": True, "value": value, "updated_at": row[1], "source": "db"})
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"ok": False, "error": "server_error"}), 500
|
return jsonify({"ok": False, "error": "server_error"}), 500
|
||||||
|
|||||||
19
index.html
19
index.html
@@ -865,6 +865,18 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isConfigListedAdmin(emailRaw) {
|
||||||
|
const e = String(emailRaw || "").trim().toLowerCase();
|
||||||
|
if (!e) return false;
|
||||||
|
const cfg = getAuthConfig();
|
||||||
|
const list = Array.isArray(cfg.adminEmails) ? cfg.adminEmails : [];
|
||||||
|
return list.some((x) => String(x).trim().toLowerCase() === e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAuthorizedAfterSync(canManageFromServer) {
|
||||||
|
return Boolean(canManageFromServer) || isConfigListedAdmin(sessionEmail);
|
||||||
|
}
|
||||||
|
|
||||||
function currentUrlNoQuery() {
|
function currentUrlNoQuery() {
|
||||||
const u = new URL(location.href);
|
const u = new URL(location.href);
|
||||||
u.searchParams.delete("code");
|
u.searchParams.delete("code");
|
||||||
@@ -932,12 +944,17 @@
|
|||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
if (r && r.ok) {
|
if (r && r.ok) {
|
||||||
const data = await r.json().catch(() => null);
|
const data = await r.json().catch(() => null);
|
||||||
if (data && data.ok) auth.authorized = Boolean(data.canManage);
|
if (data && data.ok) auth.authorized = resolveAuthorizedAfterSync(data.canManage);
|
||||||
|
} else if (auth.user) {
|
||||||
|
auth.authorized = isConfigListedAdmin(sessionEmail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
if (auth.user && !auth.authorized) {
|
||||||
|
auth.authorized = isConfigListedAdmin(sessionEmail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyManageLock();
|
applyManageLock();
|
||||||
|
|||||||
26
script.js
26
script.js
@@ -77,6 +77,21 @@
|
|||||||
return e.trim().toLowerCase();
|
return e.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 서버 /api/config/auth 의 adminEmails(.env ADMIN_EMAILS와 동기)에 포함된 로그인 사용자 */
|
||||||
|
function isConfigListedAdmin(emailRaw) {
|
||||||
|
const e = String(emailRaw || "").trim().toLowerCase();
|
||||||
|
if (!e) return false;
|
||||||
|
const cfg = getAuthConfig();
|
||||||
|
const list = Array.isArray(cfg.adminEmails) ? cfg.adminEmails : [];
|
||||||
|
return list.some((x) => String(x).trim().toLowerCase() === e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAuthorizedAfterSync(canManageFromServer) {
|
||||||
|
const fromApi = Boolean(canManageFromServer);
|
||||||
|
const fromList = isConfigListedAdmin(getUserEmail());
|
||||||
|
return fromApi || fromList;
|
||||||
|
}
|
||||||
|
|
||||||
function canAccessLink(link) {
|
function canAccessLink(link) {
|
||||||
// Admin (DB: ncue_user.is_admin) can access all links.
|
// Admin (DB: ncue_user.is_admin) can access all links.
|
||||||
if (auth && auth.authorized) return true;
|
if (auth && auth.authorized) return true;
|
||||||
@@ -977,7 +992,9 @@
|
|||||||
const can = await syncUserToServerWithIdToken(t.id_token);
|
const can = await syncUserToServerWithIdToken(t.id_token);
|
||||||
if (typeof can === "boolean") {
|
if (typeof can === "boolean") {
|
||||||
auth.serverCanManage = can;
|
auth.serverCanManage = can;
|
||||||
auth.authorized = can;
|
auth.authorized = resolveAuthorizedAfterSync(can);
|
||||||
|
} else if (auth.user) {
|
||||||
|
auth.authorized = isConfigListedAdmin(getUserEmail());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateAuthUi();
|
updateAuthUi();
|
||||||
@@ -1046,13 +1063,18 @@
|
|||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (data && data.ok) {
|
if (data && data.ok) {
|
||||||
auth.serverCanManage = Boolean(data.canManage);
|
auth.serverCanManage = Boolean(data.canManage);
|
||||||
auth.authorized = auth.serverCanManage;
|
auth.authorized = resolveAuthorizedAfterSync(data.canManage);
|
||||||
}
|
}
|
||||||
|
} else if (auth.user) {
|
||||||
|
auth.authorized = isConfigListedAdmin(getUserEmail());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore: server not running or blocked
|
// ignore: server not running or blocked
|
||||||
}
|
}
|
||||||
|
if (auth.user && !auth.authorized) {
|
||||||
|
auth.authorized = isConfigListedAdmin(getUserEmail());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.user && !auth.authorized) {
|
if (auth.user && !auth.authorized) {
|
||||||
|
|||||||
17
server.js
17
server.js
@@ -238,8 +238,7 @@ app.get("/api/config/auth", async (_req, res) => {
|
|||||||
value: {
|
value: {
|
||||||
auth0: { domain: AUTH0_DOMAIN, clientId: AUTH0_CLIENT_ID },
|
auth0: { domain: AUTH0_DOMAIN, clientId: AUTH0_CLIENT_ID },
|
||||||
connections: { google: AUTH0_GOOGLE_CONNECTION },
|
connections: { google: AUTH0_GOOGLE_CONNECTION },
|
||||||
// Deprecated: admin is stored per-user in DB (ncue_user.is_admin)
|
adminEmails: Array.from(ADMIN_EMAILS).sort(),
|
||||||
adminEmails: [],
|
|
||||||
},
|
},
|
||||||
updated_at: null,
|
updated_at: null,
|
||||||
source: "env",
|
source: "env",
|
||||||
@@ -249,11 +248,19 @@ app.get("/api/config/auth", async (_req, res) => {
|
|||||||
await ensureConfigTable();
|
await ensureConfigTable();
|
||||||
const r = await pool.query(`select value, updated_at from public.${CONFIG_TABLE} where key = $1`, ["auth"]);
|
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" });
|
if (!r.rows?.length) return res.status(404).json({ ok: false, error: "not_set" });
|
||||||
const v = r.rows[0].value || {};
|
let v = r.rows[0].value || {};
|
||||||
|
if (typeof v !== "object" || v === null) v = {};
|
||||||
// legacy: allowedEmails -> adminEmails
|
// legacy: allowedEmails -> adminEmails
|
||||||
if (v && typeof v === "object" && !v.adminEmails && Array.isArray(v.allowedEmails)) {
|
if (!v.adminEmails && Array.isArray(v.allowedEmails)) v = { ...v, adminEmails: v.allowedEmails };
|
||||||
v.adminEmails = v.allowedEmails;
|
const merged = new Set(ADMIN_EMAILS);
|
||||||
|
const ae = v.adminEmails;
|
||||||
|
if (Array.isArray(ae)) {
|
||||||
|
for (const x of ae) {
|
||||||
|
const s = String(x || "").trim().toLowerCase();
|
||||||
|
if (s) merged.add(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
v = { ...v, adminEmails: Array.from(merged).sort() };
|
||||||
res.json({ ok: true, value: v, updated_at: r.rows[0].updated_at, source: "db" });
|
res.json({ ok: true, value: v, updated_at: r.rows[0].updated_at, source: "db" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ ok: false, error: "server_error" });
|
res.status(500).json({ ok: false, error: "server_error" });
|
||||||
|
|||||||
Reference in New Issue
Block a user