import express, { type Router } from "express";
|
import path from "node:path";
|
import { fileURLToPath } from "node:url";
|
import type { AppConfig } from "../config/types.js";
|
import {
|
loadLdapConfig,
|
loadPagesConfig,
|
loadUserScenariosConfig,
|
} from "../config/loadConfig.js";
|
import { requireAuth } from "../middleware/requireAuth.js";
|
import {
|
buildScenarioImageMapForUser,
|
loadHomeCatalogForUser,
|
resolveScenarioForUser,
|
scenarioToSlides,
|
} from "../services/scenarioLookup.js";
|
import {
|
createUserScenario,
|
deleteUserScenario,
|
STAYTIME_MAX,
|
STAYTIME_MIN,
|
} from "../services/userScenarios.js";
|
import { verifyCredentials } from "../utils/ldapAuth.js";
|
import { resolveBoardUrl } from "../utils/resolveBoardUrl.js";
|
import { loadWarehousePlansGroupedByDate } from "../services/warehouseCalendarData.js";
|
import {
|
coerceParamsFromRequest,
|
executeDataSourceById,
|
} from "../services/sqlDataSource.js";
|
import { buildMonthRows, formatDateKey } from "../utils/calendarMonth.js";
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
function shiftMonth(
|
year: number,
|
month: number,
|
delta: number
|
): { year: number; month: number } {
|
const d = new Date(year, month - 1 + delta, 1);
|
return { year: d.getFullYear(), month: d.getMonth() + 1 };
|
}
|
|
type ScenarioViewSlide = {
|
id: string;
|
title: string;
|
staytime: number;
|
boardUrl: string;
|
};
|
|
type ScenarioViewSlidesData = {
|
documentTitle: string;
|
scenarioId: string;
|
scenarioName: string;
|
scenarioImage: string;
|
slides: ScenarioViewSlide[];
|
slidesJson: string;
|
startIndex: number;
|
};
|
|
function buildScenarioViewSlides(
|
appCfg: AppConfig,
|
username: string,
|
scenarioId: string,
|
pageIdHint: string
|
): ScenarioViewSlidesData | null {
|
const resolved = resolveScenarioForUser(username, scenarioId);
|
if (!resolved) {
|
return null;
|
}
|
const sc = resolved.scenario;
|
const rawSlides = scenarioToSlides(sc);
|
const slides = rawSlides.map((p) => ({
|
id: p.id,
|
title: p.title,
|
staytime:
|
typeof p.staytime === "number" && p.staytime > 0 ? p.staytime : 30,
|
boardUrl: resolveBoardUrl(appCfg.dataEasePublicBase, p.boardUrl),
|
}));
|
let startIndex = 0;
|
if (pageIdHint) {
|
const idx = slides.findIndex((p) => p.id === pageIdHint);
|
if (idx >= 0) {
|
startIndex = idx;
|
}
|
}
|
return {
|
documentTitle: sc.name,
|
scenarioId: sc.id,
|
scenarioName: sc.name,
|
scenarioImage: sc.image,
|
slides,
|
slidesJson: JSON.stringify(slides),
|
startIndex,
|
};
|
}
|
|
export function createBoardRouter(appCfg: AppConfig): Router {
|
const router = express.Router();
|
|
router.get("/check/", (req, res) => {
|
if (req.session.user) {
|
res.status(200).type("text/plain").send("ok");
|
return;
|
}
|
res.status(401).type("text/plain").send("unauthorized");
|
});
|
|
router.get("/login", (req, res) => {
|
if (req.session.user) {
|
res.redirect("/board/");
|
return;
|
}
|
res.render("login", {
|
basePath: "/board",
|
error: null as string | null,
|
});
|
});
|
|
router.use(express.json());
|
|
router.post("/api/login", async (req, res) => {
|
const username = String(req.body?.username ?? "").trim();
|
const password = String(req.body?.password ?? "");
|
if (!username || !password) {
|
res.status(400).json({ ok: false, error: "缺少用户名或密码" });
|
return;
|
}
|
const skipAd = appCfg.skipLdapAuthForDebug === true;
|
if (!skipAd) {
|
const ldapCfg = loadLdapConfig();
|
const result = await verifyCredentials(username, password, ldapCfg);
|
if (!result.ok) {
|
const status =
|
result.reason === "connection_error" ? 503 : 401;
|
const msg =
|
result.reason === "user_not_found"
|
? "用户不存在"
|
: result.reason === "connection_error"
|
? "目录服务不可用"
|
: "用户名或密码错误";
|
res.status(status).json({ ok: false, error: msg });
|
return;
|
}
|
}
|
req.session.user = { username };
|
const next = String(req.body?.next ?? "");
|
const safeNext =
|
next.startsWith("/board/") && !next.includes("//") ? next : "/board/";
|
await new Promise<void>((resolve, reject) => {
|
req.session.save((err) => (err != null ? reject(err) : resolve()));
|
});
|
res.json({ ok: true, redirect: safeNext });
|
});
|
|
router.post("/api/logout", (req, res) => {
|
req.session.destroy((err) => {
|
if (err) {
|
res.status(500).json({ ok: false, error: "登出失败" });
|
return;
|
}
|
res.clearCookie("board.sid", { path: "/" });
|
res.json({ ok: true });
|
});
|
});
|
|
router.use(
|
"/static",
|
express.static(path.join(__dirname, "../../public/board"))
|
);
|
|
router.get("/", requireAuth, (req, res) => {
|
const username = req.session.user!.username;
|
const categories = loadHomeCatalogForUser(username);
|
res.render("home", {
|
basePath: "/board",
|
username,
|
categories,
|
scenarioImagesJson: JSON.stringify(buildScenarioImageMapForUser(username)),
|
});
|
});
|
|
router.get("/warehouse-calendar", requireAuth, async (req, res) => {
|
const now = new Date();
|
const qy = Number(req.query.year);
|
const qm = Number(req.query.month);
|
const year =
|
Number.isFinite(qy) && qy >= 2000 && qy <= 2100 ? qy : now.getFullYear();
|
const month =
|
Number.isFinite(qm) && qm >= 1 && qm <= 12 ? qm : now.getMonth() + 1;
|
|
const monthTitle = new Date(year, month - 1, 1).toLocaleString("zh-CN", {
|
year: "numeric",
|
month: "long",
|
});
|
|
const prev = shiftMonth(year, month, -1);
|
const next = shiftMonth(year, month, 1);
|
|
const todayKey = formatDateKey(
|
now.getFullYear(),
|
now.getMonth() + 1,
|
now.getDate()
|
);
|
const baseLocals = {
|
basePath: "/board",
|
username: req.session.user!.username,
|
year,
|
month,
|
monthTitle,
|
todayKey,
|
prevYear: prev.year,
|
prevMonth: prev.month,
|
nextYear: next.year,
|
nextMonth: next.month,
|
weekdayLabels: ["一", "二", "三", "四", "五", "六", "日"],
|
};
|
|
try {
|
const plansByDate = await loadWarehousePlansGroupedByDate(year, month);
|
const rows = buildMonthRows(year, month, plansByDate);
|
res.render("warehouseCalendar", {
|
...baseLocals,
|
rows,
|
dataError: null as string | null,
|
});
|
} catch (err) {
|
const msg = err instanceof Error ? err.message : String(err);
|
const rows = buildMonthRows(year, month, {});
|
res.status(503).render("warehouseCalendar", {
|
...baseLocals,
|
rows,
|
dataError: `数据加载失败:${msg}`,
|
});
|
}
|
});
|
|
/** 按 datasrc.json 的 id 执行 SQL,参数来自 query(除 id 外均传入存储过程/语句) */
|
router.get("/api/datasrc/query", requireAuth, async (req, res) => {
|
const id = String(req.query.id ?? "").trim();
|
if (!id) {
|
res.status(400).json({ ok: false, error: "缺少参数 id" });
|
return;
|
}
|
const raw = { ...req.query } as Record<string, unknown>;
|
delete raw.id;
|
try {
|
const params = coerceParamsFromRequest(raw);
|
const data = await executeDataSourceById(id, params);
|
res.json({ ok: true, data });
|
} catch (e) {
|
const msg = e instanceof Error ? e.message : String(e);
|
res.status(500).json({ ok: false, error: msg });
|
}
|
});
|
|
/** 同上,参数来自 JSON body */
|
router.post("/api/datasrc/query", requireAuth, async (req, res) => {
|
const id = String(req.body?.id ?? "").trim();
|
if (!id) {
|
res.status(400).json({ ok: false, error: "缺少参数 id" });
|
return;
|
}
|
const raw = { ...(req.body ?? {}) } as Record<string, unknown>;
|
delete raw.id;
|
try {
|
const params = coerceParamsFromRequest(raw);
|
const data = await executeDataSourceById(id, params);
|
res.json({ ok: true, data });
|
} catch (e) {
|
const msg = e instanceof Error ? e.message : String(e);
|
res.status(500).json({ ok: false, error: msg });
|
}
|
});
|
|
/** 用户场景管理(仅显示/修改 user 级别) */
|
router.get("/config/scenarios", requireAuth, (req, res) => {
|
const username = req.session.user!.username;
|
const { pages } = loadPagesConfig();
|
const userScenarios = loadUserScenariosConfig().users[username]?.scenarios ?? [];
|
res.render("userScenarios", {
|
basePath: "/board",
|
username,
|
allPages: pages,
|
userScenarios,
|
staytimeMin: STAYTIME_MIN,
|
staytimeMax: STAYTIME_MAX,
|
});
|
});
|
|
router.post("/api/user-scenarios/delete", requireAuth, (req, res) => {
|
const username = req.session.user!.username;
|
const scenarioId = String(req.body?.scenarioId ?? "").trim();
|
if (!scenarioId) {
|
res.status(400).json({ ok: false, error: "缺少 scenarioId" });
|
return;
|
}
|
const result = deleteUserScenario(username, scenarioId);
|
if (!result.ok) {
|
res.status(400).json({ ok: false, error: result.error });
|
return;
|
}
|
res.json({ ok: true });
|
});
|
|
router.post("/api/user-scenarios/create", requireAuth, (req, res) => {
|
const username = req.session.user!.username;
|
const name = String(req.body?.name ?? "");
|
const pages = Array.isArray(req.body?.pages) ? req.body.pages : [];
|
const pageRefs = pages.map((x: any) => ({
|
pageId: String(x?.pageId ?? "").trim(),
|
staytime: Number(x?.staytime),
|
}));
|
const result = createUserScenario(username, name, pageRefs);
|
if (!result.ok) {
|
res.status(400).json({ ok: false, error: result.error });
|
return;
|
}
|
res.json({ ok: true, scenario: result.scenario });
|
});
|
|
/** 默认:多 iframe 叠放、按 staytime 切换可见层(不轮改 src) */
|
router.get("/view", requireAuth, (req, res) => {
|
const scenarioId = String(req.query.scenarioId ?? "");
|
const pageIdHint = String(req.query.pageId ?? "");
|
const username = req.session.user!.username;
|
const data = buildScenarioViewSlides(appCfg, username, scenarioId, pageIdHint);
|
if (!data) {
|
res.status(404).send("场景不存在");
|
return;
|
}
|
res.render("view", {
|
basePath: "/board",
|
...data,
|
});
|
});
|
|
/** 旧版:单 iframe 定时切换 src */
|
router.get("/view-i", requireAuth, (req, res) => {
|
const scenarioId = String(req.query.scenarioId ?? "");
|
const pageIdHint = String(req.query.pageId ?? "");
|
const username = req.session.user!.username;
|
const data = buildScenarioViewSlides(appCfg, username, scenarioId, pageIdHint);
|
if (!data) {
|
res.status(404).send("场景不存在");
|
return;
|
}
|
res.render("view-i", {
|
basePath: "/board",
|
documentTitle: data.documentTitle,
|
scenarioId: data.scenarioId,
|
scenarioName: data.scenarioName,
|
scenarioImage: data.scenarioImage,
|
slidesJson: data.slidesJson,
|
startIndex: data.startIndex,
|
});
|
});
|
|
/** 兼容旧链接,重定向到 /view */
|
router.get("/view-tabs", requireAuth, (req, res) => {
|
const q = req.url.includes("?")
|
? req.url.slice(req.url.indexOf("?"))
|
: "";
|
res.redirect(302, "/board/view" + q);
|
});
|
|
return router;
|
}
|