feat(cms): add management for partnerships, history, accreditation, admissions, and policies pages

Implement a singleton page content system to manage static informational pages. This includes:
- New controllers, models, and data files for Partnerships, History, Accreditation, Admissions, and Policies.
- Admin routes and views for updating page content.
- Public API endpoints for fetching page data.
- Migration scripts for initializing page data.
- Updated admin navigation layout to include a dropdown for "About" sections.
- Audit action constants for tracking updates to these pages.
This commit is contained in:
Tống Thành Đạt
2026-04-20 14:08:38 +07:00
parent fab72e86a6
commit 8b6bf5fe6e
48 changed files with 2357 additions and 4 deletions
+113
View File
@@ -0,0 +1,113 @@
const { addBaseUrlToImages } = require("../utils/imageHelper");
const jsonHelper = require("../utils/jsonHelper");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
function createPageContentController({
model,
modelName,
auditAction,
editorConfig,
}) {
return {
async index(req, res) {
try {
const doc = await model.getSingle();
const data = doc.toObject();
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const backendUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
res.render("admin/pageContent/index", {
layout: "layouts/main",
title: editorConfig.title,
subtitle: editorConfig.subtitle,
data,
editorConfig,
activeTab: req.query.tab || editorConfig.tabs[0].key,
frontendUrl,
backendUrl,
previewUrl: `${frontendUrl}${editorConfig.previewPath}`,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error(`${editorConfig.key} index error:`, error);
req.flash("error_msg", `Error loading ${editorConfig.title}`);
return req.session.save(() => res.redirect("/admin/dashboard"));
}
},
async update(req, res) {
try {
const payload =
typeof req.body.pageJson === "string"
? JSON.parse(req.body.pageJson)
: req.body.pageJson || {};
const activeTab = req.body.activeTab || editorConfig.tabs[0].key;
const doc = await model.getSingle();
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
doc.set(payload);
Object.keys(payload).forEach((key) => doc.markModified(key));
await doc.save();
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: modelName,
documentId: doc._id,
action: auditAction,
before: beforeData,
after: afterData,
changes,
req,
});
}
const finalData = await model
.findOne()
.select("-_id -__v -createdAt -updatedAt")
.lean();
jsonHelper.writeJsonFile(editorConfig.dataFile, finalData);
req.flash("success_msg", `${editorConfig.title} updated successfully`);
return req.session.save(() =>
res.redirect(`${editorConfig.routeBase}?tab=${activeTab}`),
);
} catch (error) {
console.error(`${editorConfig.key} update error:`, error);
req.flash(
"error_msg",
`Error updating ${editorConfig.title}: ${error.message}`,
);
return req.session.save(() =>
res.redirect(
`${editorConfig.routeBase}?tab=${req.body.activeTab || ""}`,
),
);
}
},
async api(req, res) {
try {
const doc = await model.getSingle();
const rawData = doc.toObject();
const backendUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processed = addBaseUrlToImages(rawData, backendUrl);
return res.json(processed);
} catch (error) {
console.error(`${editorConfig.key} api error:`, error);
return res
.status(500)
.json({ error: `Error loading ${editorConfig.key} data` });
}
},
};
}
module.exports = createPageContentController;
+11
View File
@@ -0,0 +1,11 @@
const AccreditationPage = require("../models/accreditationPage");
const AUDIT_ACTIONS = require("../constants/auditAction");
const pageContentConfig = require("../utils/pageContentConfig");
const createPageContentController = require("./_createPageContentController");
module.exports = createPageContentController({
model: AccreditationPage,
modelName: "AccreditationPage",
auditAction: AUDIT_ACTIONS.UPDATE_ACCREDITATION,
editorConfig: pageContentConfig.accreditation,
});
+11
View File
@@ -0,0 +1,11 @@
const AdmissionsPage = require("../models/admissionsPage");
const AUDIT_ACTIONS = require("../constants/auditAction");
const pageContentConfig = require("../utils/pageContentConfig");
const createPageContentController = require("./_createPageContentController");
module.exports = createPageContentController({
model: AdmissionsPage,
modelName: "AdmissionsPage",
auditAction: AUDIT_ACTIONS.UPDATE_ADMISSIONS,
editorConfig: pageContentConfig.admissions,
});
+11
View File
@@ -0,0 +1,11 @@
const HistoryPage = require("../models/historyPage");
const AUDIT_ACTIONS = require("../constants/auditAction");
const pageContentConfig = require("../utils/pageContentConfig");
const createPageContentController = require("./_createPageContentController");
module.exports = createPageContentController({
model: HistoryPage,
modelName: "HistoryPage",
auditAction: AUDIT_ACTIONS.UPDATE_HISTORY,
editorConfig: pageContentConfig.history,
});
+11
View File
@@ -0,0 +1,11 @@
const PartnershipsPage = require("../models/partnerships");
const AUDIT_ACTIONS = require("../constants/auditAction");
const pageContentConfig = require("../utils/pageContentConfig");
const createPageContentController = require("./_createPageContentController");
module.exports = createPageContentController({
model: PartnershipsPage,
modelName: "PartnershipsPage",
auditAction: AUDIT_ACTIONS.UPDATE_PARTNERSHIPS,
editorConfig: pageContentConfig.partnerships,
});
+11
View File
@@ -0,0 +1,11 @@
const PoliciesPage = require("../models/policiesPage");
const AUDIT_ACTIONS = require("../constants/auditAction");
const pageContentConfig = require("../utils/pageContentConfig");
const createPageContentController = require("./_createPageContentController");
module.exports = createPageContentController({
model: PoliciesPage,
modelName: "PoliciesPage",
auditAction: AUDIT_ACTIONS.UPDATE_POLICIES,
editorConfig: pageContentConfig.policies,
});