feat: 대시보드 메뉴 DASHBOARD_MENU_ALLOWED_EMAILS(.env) 화이트리스트
- OPS 로그인 이메일만 메뉴·/dashboard·경영성과 API 허용 - DEV 옵션: DASHBOARD_MENU_DEV_USE_MEETING_EMAIL+MEETING_DEV_EMAIL Made-with: Cursor
This commit is contained in:
67
server.js
67
server.js
@@ -968,6 +968,52 @@ function isAiExploreDevGuestRestricted(req, res) {
|
||||
return isOpsStateDev() && !res.locals.adminMode;
|
||||
}
|
||||
|
||||
/** `.env` DASHBOARD_MENU_ALLOWED_EMAILS (쉼표 구분, 소문자 정규화) */
|
||||
function parseDashboardMenuAllowlist() {
|
||||
return String(process.env.DASHBOARD_MENU_ALLOWED_EMAILS || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
const DASHBOARD_MENU_ALLOWLIST = parseDashboardMenuAllowlist();
|
||||
|
||||
/**
|
||||
* 대시보드 메뉴·경로 허용 여부: 목록에 있고, (OPS 로그인 이메일 또는 DEV에서 관리자+MEETING_DEV_EMAIL 대조)
|
||||
*/
|
||||
function getEmailForDashboardAccess(req, res) {
|
||||
if (res.locals.opsUserEmail) {
|
||||
return String(res.locals.opsUserEmail).trim().toLowerCase();
|
||||
}
|
||||
if (
|
||||
isOpsStateDev() &&
|
||||
res.locals.adminMode &&
|
||||
process.env.DASHBOARD_MENU_DEV_USE_MEETING_EMAIL === "1"
|
||||
) {
|
||||
return (process.env.MEETING_DEV_EMAIL || "").trim().toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function computeDashboardMenuAllowed(req, res) {
|
||||
if (DASHBOARD_MENU_ALLOWLIST.length === 0) return false;
|
||||
const email = getEmailForDashboardAccess(req, res);
|
||||
if (!email) return false;
|
||||
return DASHBOARD_MENU_ALLOWLIST.includes(email);
|
||||
}
|
||||
|
||||
function requireDashboardAccess(req, res, next) {
|
||||
if (computeDashboardMenuAllowed(req, res)) return next();
|
||||
if (String(req.path || "").startsWith("/api/")) {
|
||||
return res.status(403).json({ error: "대시보드 접근 권한이 없습니다." });
|
||||
}
|
||||
return res
|
||||
.status(403)
|
||||
.type("html")
|
||||
.send(
|
||||
`<!DOCTYPE html><html lang="ko"><head><meta charset="utf-8"/><title>권한 없음</title><link rel="icon" href="/favicon.ico" type="image/x-icon"/></head><body style="font-family:sans-serif;padding:24px;"><p>대시보드 접근 권한이 없습니다.</p><p><a href="/learning">학습센터로</a></p></body></html>`
|
||||
);
|
||||
}
|
||||
|
||||
/** OPS 이메일 세션, DEV+관리자(MEETING_DEV_EMAIL), SUPER(데모 이메일) — 회의록 AI */
|
||||
function getMeetingMinutesUserEmail(req, res) {
|
||||
if (res.locals.opsUserEmail) return String(res.locals.opsUserEmail).trim().toLowerCase();
|
||||
@@ -1154,9 +1200,13 @@ app.get("/resources/ax-apply/AX_과제_신청서.docx", (req, res) => {
|
||||
app.use("/resources/ax-apply", express.static(RESOURCES_AX_APPLY_DIR));
|
||||
|
||||
app.use(opsAuth.middleware);
|
||||
app.use((req, res, next) => {
|
||||
res.locals.dashboardMenuAllowed = computeDashboardMenuAllowed(req, res);
|
||||
next();
|
||||
});
|
||||
opsAuth.registerRoutes(app);
|
||||
|
||||
app.get("/api/mgmt-perf/status", async (req, res) => {
|
||||
app.get("/api/mgmt-perf/status", requireDashboardAccess, async (req, res) => {
|
||||
try {
|
||||
const row = await mgmtPerf.getLatestPayloadRow(pgPool);
|
||||
const p = row.payload && typeof row.payload === "object" ? row.payload : {};
|
||||
@@ -1176,7 +1226,11 @@ app.get("/api/mgmt-perf/status", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/mgmt-perf/upload", uploadMgmtPerfExcel.single("file"), async (req, res) => {
|
||||
app.post(
|
||||
"/api/mgmt-perf/upload",
|
||||
requireDashboardAccess,
|
||||
uploadMgmtPerfExcel.single("file"),
|
||||
async (req, res) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: "파일이 없습니다." });
|
||||
const fiscalYear = Math.min(2100, Math.max(2020, parseInt(req.body.fiscalYear, 10) || new Date().getFullYear()));
|
||||
@@ -1198,7 +1252,8 @@ app.post("/api/mgmt-perf/upload", uploadMgmtPerfExcel.single("file"), async (req
|
||||
console.error("mgmt-perf upload:", err);
|
||||
res.status(500).json({ error: err.message || "처리 실패" });
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const pageRouter = express.Router();
|
||||
pageRouter.get("/chat", (req, res) =>
|
||||
@@ -1237,14 +1292,14 @@ pageRouter.get("/ai-explore/task-checklist", (req, res) =>
|
||||
opsState: normalizeOpsState(),
|
||||
})
|
||||
);
|
||||
pageRouter.get("/dashboard", (req, res) =>
|
||||
pageRouter.get("/dashboard", requireDashboardAccess, (req, res) =>
|
||||
res.render("dashboard", {
|
||||
activeMenu: "dashboard",
|
||||
adminMode: res.locals.adminMode,
|
||||
opsUserEmail: !!res.locals.opsUserEmail,
|
||||
})
|
||||
);
|
||||
pageRouter.get("/dashboard/business-performance", async (req, res, next) => {
|
||||
pageRouter.get("/dashboard/business-performance", requireDashboardAccess, async (req, res, next) => {
|
||||
try {
|
||||
const latest = await mgmtPerf.getLatestPayloadRow(pgPool);
|
||||
const uploadHistory = await mgmtPerf.listUploads(pgPool, 12);
|
||||
@@ -1268,7 +1323,7 @@ pageRouter.get("/dashboard/business-performance", async (req, res, next) => {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
pageRouter.get("/dashboard/business-performance/embed", async (req, res, next) => {
|
||||
pageRouter.get("/dashboard/business-performance/embed", requireDashboardAccess, async (req, res, next) => {
|
||||
try {
|
||||
const row = await mgmtPerf.getLatestPayloadRow(pgPool);
|
||||
const fy = row.fiscal_year || new Date().getFullYear();
|
||||
|
||||
Reference in New Issue
Block a user