forked from UKSOURCE/cms.hailearning.edu.vn
feat: implement comprehensive audit logging system
This commit is contained in:
@@ -1,367 +1,410 @@
|
||||
const Header = require("../models/header");
|
||||
const HeaderMenu = require("../models/HeaderMenu");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
/**
|
||||
* Helper function to build a tree structure (Mirroring logic in headerMenuController)
|
||||
*/
|
||||
const buildTree = (items, parentId = null) => {
|
||||
const branch = [];
|
||||
const children = items.filter(item =>
|
||||
String(item.parentId) === String(parentId) || (item.parentId === null && parentId === null)
|
||||
);
|
||||
const branch = [];
|
||||
const children = items.filter(
|
||||
(item) =>
|
||||
String(item.parentId) === String(parentId) ||
|
||||
(item.parentId === null && parentId === null),
|
||||
);
|
||||
|
||||
for (const child of children) {
|
||||
const item = child.toObject ? child.toObject() : { ...child };
|
||||
const subChildren = buildTree(items, item._id);
|
||||
item.children = subChildren.length > 0 ? subChildren : [];
|
||||
branch.push(item);
|
||||
}
|
||||
for (const child of children) {
|
||||
const item = child.toObject ? child.toObject() : { ...child };
|
||||
const subChildren = buildTree(items, item._id);
|
||||
item.children = subChildren.length > 0 ? subChildren : [];
|
||||
branch.push(item);
|
||||
}
|
||||
|
||||
return branch.sort((a, b) => a.order - b.order);
|
||||
return branch.sort((a, b) => a.order - b.order);
|
||||
};
|
||||
|
||||
// Admin: Render header management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const header = await Header.findOne().sort({ order: 1 });
|
||||
try {
|
||||
const header = await Header.findOne().sort({ order: 1 });
|
||||
|
||||
// Prepare data for view
|
||||
const data = header
|
||||
? {
|
||||
topbar: {
|
||||
contactInfo: {
|
||||
phone: header.top?.phone || "",
|
||||
email: header.top?.email || "",
|
||||
location: header.top?.location || "",
|
||||
},
|
||||
socialLinks: header.top?.socialLinks || [],
|
||||
},
|
||||
logo: header.logo?.light || "",
|
||||
}
|
||||
: {
|
||||
topbar: {
|
||||
contactInfo: {
|
||||
phone: "",
|
||||
email: "",
|
||||
location: "",
|
||||
},
|
||||
socialLinks: [],
|
||||
},
|
||||
logo: "",
|
||||
};
|
||||
|
||||
const activeTab = req.query.tab || "topbar";
|
||||
|
||||
// Always fetch menu items to ensure they are available even if the user
|
||||
// switches tabs client-side
|
||||
const items = await HeaderMenu.find().sort({ order: 1 });
|
||||
const menuData = {
|
||||
flat: items,
|
||||
tree: buildTree(items)
|
||||
// Prepare data for view
|
||||
const data = header
|
||||
? {
|
||||
topbar: {
|
||||
contactInfo: {
|
||||
phone: header.top?.phone || "",
|
||||
email: header.top?.email || "",
|
||||
location: header.top?.location || "",
|
||||
},
|
||||
socialLinks: header.top?.socialLinks || [],
|
||||
},
|
||||
logo: header.logo?.light || "",
|
||||
}
|
||||
: {
|
||||
topbar: {
|
||||
contactInfo: {
|
||||
phone: "",
|
||||
email: "",
|
||||
location: "",
|
||||
},
|
||||
socialLinks: [],
|
||||
},
|
||||
logo: "",
|
||||
};
|
||||
|
||||
res.render("admin/header/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Header Management",
|
||||
user: req.session.user || null,
|
||||
data: data,
|
||||
activeTab: activeTab,
|
||||
menuData: menuData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading header management:", error);
|
||||
res.status(500).render("page/error", {
|
||||
title: "Error",
|
||||
message: "Failed to load header management page",
|
||||
});
|
||||
}
|
||||
const activeTab = req.query.tab || "topbar";
|
||||
|
||||
// Always fetch menu items to ensure they are available even if the user
|
||||
// switches tabs client-side
|
||||
const items = await HeaderMenu.find().sort({ order: 1 });
|
||||
const menuData = {
|
||||
flat: items,
|
||||
tree: buildTree(items),
|
||||
};
|
||||
|
||||
res.render("admin/header/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Header Management",
|
||||
user: req.session.user || null,
|
||||
data: data,
|
||||
activeTab: activeTab,
|
||||
menuData: menuData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading header management:", error);
|
||||
res.status(500).render("page/error", {
|
||||
title: "Error",
|
||||
message: "Failed to load header management page",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Admin: Get all headers (API)
|
||||
exports.getAll = async (req, res) => {
|
||||
try {
|
||||
const headers = await Header.find().sort({ order: 1 });
|
||||
res.json({
|
||||
success: true,
|
||||
data: headers,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
try {
|
||||
const headers = await Header.find().sort({ order: 1 });
|
||||
res.json({
|
||||
success: true,
|
||||
data: headers,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Admin: Get single header
|
||||
exports.show = async (req, res) => {
|
||||
try {
|
||||
const header = await Header.findById(req.params.id);
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
try {
|
||||
const header = await Header.findById(req.params.id);
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Admin: Create header
|
||||
exports.store = async (req, res) => {
|
||||
try {
|
||||
const { top, offcanvas, menu, logo, ctaButton, status, order } = req.body;
|
||||
try {
|
||||
const { top, offcanvas, menu, logo, ctaButton, status, order } = req.body;
|
||||
|
||||
const header = new Header({
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
logo,
|
||||
ctaButton,
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
const header = new Header({
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
logo,
|
||||
ctaButton,
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
|
||||
await header.save();
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Header created successfully",
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
await header.save();
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Header created successfully",
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Admin: Update header
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
let { top, topbarJson, offcanvas, menu, logo, ctaButton, status, order } = req.body;
|
||||
try {
|
||||
let { top, topbarJson, offcanvas, menu, logo, ctaButton, status, order } =
|
||||
req.body;
|
||||
|
||||
console.log("=== UPDATE REQUEST RECEIVED ===");
|
||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||
console.log("topbarJson type:", typeof topbarJson);
|
||||
console.log("topbarJson value:", topbarJson);
|
||||
console.log("=== UPDATE REQUEST RECEIVED ===");
|
||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||
console.log("topbarJson type:", typeof topbarJson);
|
||||
console.log("topbarJson value:", topbarJson);
|
||||
|
||||
// Nếu có topbarJson, parse nó
|
||||
if (topbarJson && typeof topbarJson === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(topbarJson);
|
||||
console.log("✓ Parsed topbarJson successfully:", parsedData);
|
||||
// Chuyển đổi từ topbarData sang top format
|
||||
top = {
|
||||
phone: parsedData.contactInfo?.phone || "",
|
||||
email: parsedData.contactInfo?.email || "",
|
||||
location: parsedData.contactInfo?.location || "",
|
||||
socialLinks: parsedData.socialLinks || [],
|
||||
};
|
||||
console.log("✓ Converted to top object:", top);
|
||||
} catch (e) {
|
||||
console.error("✗ Error parsing topbarJson:", e.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in topbarJson: " + e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Nếu không có id, tìm header đầu tiên hoặc tạo mới
|
||||
let headerId = req.params.id;
|
||||
|
||||
if (!headerId) {
|
||||
// Tìm header đầu tiên
|
||||
let header = await Header.findOne().sort({ order: 1 });
|
||||
if (!header) {
|
||||
console.log("No existing header found, creating new one");
|
||||
// Tạo header mới nếu chưa có
|
||||
header = new Header({
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
logo: logo ? { light: logo } : {},
|
||||
ctaButton,
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
await header.save();
|
||||
console.log("✓ Header created:", header._id);
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Header created successfully",
|
||||
data: header,
|
||||
});
|
||||
}
|
||||
headerId = header._id;
|
||||
console.log("✓ Found existing header:", headerId);
|
||||
}
|
||||
|
||||
// Chuẩn bị dữ liệu logo - merge với dữ liệu cũ
|
||||
let logoData = {};
|
||||
if (logo) {
|
||||
// Nếu có logo mới, lấy dữ liệu cũ và update light
|
||||
const existingHeader = await Header.findById(headerId);
|
||||
logoData = {
|
||||
light: logo,
|
||||
dark: existingHeader?.logo?.dark || "",
|
||||
alt: existingHeader?.logo?.alt || "",
|
||||
};
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
ctaButton,
|
||||
status,
|
||||
order,
|
||||
// Nếu có topbarJson, parse nó
|
||||
if (topbarJson && typeof topbarJson === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(topbarJson);
|
||||
console.log("✓ Parsed topbarJson successfully:", parsedData);
|
||||
// Chuyển đổi từ topbarData sang top format
|
||||
top = {
|
||||
phone: parsedData.contactInfo?.phone || "",
|
||||
email: parsedData.contactInfo?.email || "",
|
||||
location: parsedData.contactInfo?.location || "",
|
||||
socialLinks: parsedData.socialLinks || [],
|
||||
};
|
||||
|
||||
if (logo) {
|
||||
updateData.logo = logoData;
|
||||
}
|
||||
|
||||
console.log("Preparing to update header with data:", JSON.stringify(updateData, null, 2));
|
||||
|
||||
const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, { new: true, runValidators: true });
|
||||
|
||||
if (!updatedHeader) {
|
||||
console.error("✗ Header not found with ID:", headerId);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✓ Header updated successfully:", updatedHeader._id);
|
||||
console.log("Updated header data:", JSON.stringify(updatedHeader, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Header updated successfully",
|
||||
data: updatedHeader,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("✗ Error updating header:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
console.log("✓ Converted to top object:", top);
|
||||
} catch (e) {
|
||||
console.error("✗ Error parsing topbarJson:", e.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in topbarJson: " + e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Nếu không có id, tìm header đầu tiên hoặc tạo mới
|
||||
let headerId = req.params.id;
|
||||
|
||||
if (!headerId) {
|
||||
// Tìm header đầu tiên
|
||||
let header = await Header.findOne().sort({ order: 1 });
|
||||
if (!header) {
|
||||
console.log("No existing header found, creating new one");
|
||||
// Tạo header mới nếu chưa có
|
||||
header = new Header({
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
logo: logo ? { light: logo } : {},
|
||||
ctaButton,
|
||||
status: status || "active",
|
||||
order: order || 1,
|
||||
});
|
||||
await header.save();
|
||||
console.log("✓ Header created:", header._id);
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Header created successfully",
|
||||
data: header,
|
||||
});
|
||||
}
|
||||
headerId = header._id;
|
||||
console.log("✓ Found existing header:", headerId);
|
||||
}
|
||||
|
||||
// Chuẩn bị dữ liệu logo - merge với dữ liệu cũ
|
||||
let logoData = {};
|
||||
if (logo) {
|
||||
// Nếu có logo mới, lấy dữ liệu cũ và update light
|
||||
const existingHeader = await Header.findById(headerId);
|
||||
logoData = {
|
||||
light: logo,
|
||||
dark: existingHeader?.logo?.dark || "",
|
||||
alt: existingHeader?.logo?.alt || "",
|
||||
};
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
top,
|
||||
offcanvas,
|
||||
menu,
|
||||
ctaButton,
|
||||
status,
|
||||
order,
|
||||
};
|
||||
|
||||
if (logo) {
|
||||
updateData.logo = logoData;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Preparing to update header with data:",
|
||||
JSON.stringify(updateData, null, 2),
|
||||
);
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeHeader = await Header.findById(headerId);
|
||||
const beforeData = beforeHeader
|
||||
? JSON.parse(JSON.stringify(beforeHeader.toObject()))
|
||||
: {};
|
||||
|
||||
const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, {
|
||||
new: true,
|
||||
runValidators: true,
|
||||
});
|
||||
|
||||
if (!updatedHeader) {
|
||||
console.error("✗ Header not found with ID:", headerId);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(updatedHeader.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Header Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Header",
|
||||
documentId: updatedHeader._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_HEADER,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✓ Header updated successfully:", updatedHeader._id);
|
||||
console.log("Updated header data:", JSON.stringify(updatedHeader, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Header updated successfully",
|
||||
data: updatedHeader,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("✗ Error updating header:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Admin: Update status
|
||||
exports.updateStatus = async (req, res) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
try {
|
||||
const { status } = req.body;
|
||||
|
||||
if (!["active", "inactive"].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid status",
|
||||
});
|
||||
}
|
||||
|
||||
const header = await Header.findByIdAndUpdate(req.params.id, { status }, { new: true });
|
||||
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Header status updated",
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
if (!["active", "inactive"].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid status",
|
||||
});
|
||||
}
|
||||
|
||||
const header = await Header.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
{ status },
|
||||
{ new: true },
|
||||
);
|
||||
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Header status updated",
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Admin: Delete header
|
||||
exports.destroy = async (req, res) => {
|
||||
try {
|
||||
const header = await Header.findByIdAndDelete(req.params.id);
|
||||
try {
|
||||
const header = await Header.findByIdAndDelete(req.params.id);
|
||||
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Header deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Header not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Header deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Public API: Get active header
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const header = await Header.findOne({ status: "active" }).sort({ order: 1 });
|
||||
try {
|
||||
const header = await Header.findOne({ status: "active" }).sort({
|
||||
order: 1,
|
||||
});
|
||||
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No active header found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
if (!header) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No active header found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: header,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Public API: Get menu tree structure
|
||||
exports.getMenuTreeAPI = async (req, res) => {
|
||||
try {
|
||||
const header = await Header.findOne({ status: "active" }).sort({ order: 1 });
|
||||
try {
|
||||
const header = await Header.findOne({ status: "active" }).sort({
|
||||
order: 1,
|
||||
});
|
||||
|
||||
if (!header || !header.menu) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No active menu found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: header.menu,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
if (!header || !header.menu) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No active menu found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: header.menu,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user