forked from UKSOURCE/cms.hailearning.edu.vn
feat: implement comprehensive audit logging system
This commit is contained in:
@@ -2,77 +2,87 @@ const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const AboutUs = require("../models/aboutUs");
|
||||
const Blog = require("../models/blog");
|
||||
const jsonHelper = require("../utils/jsonHelper");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
/**
|
||||
* GET /api/about
|
||||
* Lấy dữ liệu About Us (Public API cho website và CMS load dữ liệu)
|
||||
*/
|
||||
exports.getAbout = async (req, res) => {
|
||||
try {
|
||||
// Force no-cache headers
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
try {
|
||||
// Force no-cache headers
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
|
||||
const data = await AboutUs.getSingle();
|
||||
const rawData = data.toObject();
|
||||
const data = await AboutUs.getSingle();
|
||||
const rawData = data.toObject();
|
||||
|
||||
// === Dynamic Blog News Section ===
|
||||
const news = rawData.news || {};
|
||||
let blogs = [];
|
||||
// === Dynamic Blog News Section ===
|
||||
const news = rawData.news || {};
|
||||
let blogs = [];
|
||||
|
||||
// Nếu có chọn blog cụ thể
|
||||
if (news.selectedBlogIds && news.selectedBlogIds.length > 0) {
|
||||
blogs = await Blog.find({
|
||||
_id: { $in: news.selectedBlogIds },
|
||||
status: "published",
|
||||
}).lean();
|
||||
// Nếu có chọn blog cụ thể
|
||||
if (news.selectedBlogIds && news.selectedBlogIds.length > 0) {
|
||||
blogs = await Blog.find({
|
||||
_id: { $in: news.selectedBlogIds },
|
||||
status: "published",
|
||||
}).lean();
|
||||
|
||||
// Sắp xếp theo thứ tự đã chọn trong selectedBlogIds
|
||||
blogs.sort((a, b) => {
|
||||
return news.selectedBlogIds.indexOf(a._id.toString()) - news.selectedBlogIds.indexOf(b._id.toString());
|
||||
});
|
||||
}
|
||||
|
||||
// Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất
|
||||
if (blogs.length === 0) {
|
||||
blogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).limit(3).lean();
|
||||
}
|
||||
|
||||
// Map dữ liệu blog sang format mà frontend mong đợi
|
||||
news.items = blogs.map((blog) => ({
|
||||
title: blog.title,
|
||||
category: blog.category && blog.category[0] ? blog.category[0] : "Visa",
|
||||
date:
|
||||
blog.publishedAt ||
|
||||
new Date(blog.createdAt).toLocaleDateString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}),
|
||||
comments: blog.commentsCount || 0,
|
||||
author: {
|
||||
name: blog.author || "Admin",
|
||||
avatar: "/assets/img/home-1/news/client.png", // Default avatar
|
||||
},
|
||||
link: `/blog/${blog.slug}`,
|
||||
thumbnail: blog.featuredImage,
|
||||
}));
|
||||
|
||||
rawData.news = news;
|
||||
// ===============================
|
||||
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(rawData, baseUrl);
|
||||
|
||||
res.json(processedData);
|
||||
} catch (error) {
|
||||
console.error("Error getting about data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to get about data",
|
||||
});
|
||||
// Sắp xếp theo thứ tự đã chọn trong selectedBlogIds
|
||||
blogs.sort((a, b) => {
|
||||
return (
|
||||
news.selectedBlogIds.indexOf(a._id.toString()) -
|
||||
news.selectedBlogIds.indexOf(b._id.toString())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất
|
||||
if (blogs.length === 0) {
|
||||
blogs = await Blog.find({ status: "published" })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(3)
|
||||
.lean();
|
||||
}
|
||||
|
||||
// Map dữ liệu blog sang format mà frontend mong đợi
|
||||
news.items = blogs.map((blog) => ({
|
||||
title: blog.title,
|
||||
category: blog.category && blog.category[0] ? blog.category[0] : "Visa",
|
||||
date:
|
||||
blog.publishedAt ||
|
||||
new Date(blog.createdAt).toLocaleDateString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}),
|
||||
comments: blog.commentsCount || 0,
|
||||
author: {
|
||||
name: blog.author || "Admin",
|
||||
avatar: "/assets/img/home-1/news/client.png", // Default avatar
|
||||
},
|
||||
link: `/blog/${blog.slug}`,
|
||||
thumbnail: blog.featuredImage,
|
||||
}));
|
||||
|
||||
rawData.news = news;
|
||||
// ===============================
|
||||
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(rawData, baseUrl);
|
||||
|
||||
res.json(processedData);
|
||||
} catch (error) {
|
||||
console.error("Error getting about data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to get about data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -80,106 +90,159 @@ exports.getAbout = async (req, res) => {
|
||||
* Cập nhật dữ liệu About Us (Dùng cho AJAX từ CMS)
|
||||
*/
|
||||
exports.updateAbout = async (req, res) => {
|
||||
try {
|
||||
let updateData = req.body;
|
||||
try {
|
||||
let updateData = req.body;
|
||||
|
||||
// Nếu dữ liệu gửi qua trường aboutJson (dạng string JSON)
|
||||
if (updateData.aboutJson && typeof updateData.aboutJson === "string") {
|
||||
try {
|
||||
updateData = JSON.parse(updateData.aboutJson);
|
||||
} catch (e) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in aboutJson",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await AboutUs.getSingle();
|
||||
// Use .set() for better handling of nested objects/arrays in Mongoose
|
||||
doc.set(updateData);
|
||||
await doc.save();
|
||||
|
||||
// Fetch fresh data for syncing and returning
|
||||
const finalData = await AboutUs.findOne().select("-_id -__v -createdAt -updatedAt").lean();
|
||||
|
||||
// Update about.json file to keep it in sync
|
||||
jsonHelper.writeJsonFile("about", finalData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "About Us updated successfully",
|
||||
data: finalData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating about data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to update about data: " + error.message,
|
||||
// Nếu dữ liệu gửi qua trường aboutJson (dạng string JSON)
|
||||
if (updateData.aboutJson && typeof updateData.aboutJson === "string") {
|
||||
try {
|
||||
updateData = JSON.parse(updateData.aboutJson);
|
||||
} catch (e) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in aboutJson",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await AboutUs.getSingle();
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
|
||||
// Use .set() for better handling of nested objects/arrays in Mongoose
|
||||
doc.set(updateData);
|
||||
await doc.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - About Us Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "AboutUs",
|
||||
documentId: doc._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_ABOUT_US,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
console.log(
|
||||
`✅ Audit log created for About Us update: ${changes.length} changes`,
|
||||
);
|
||||
} else {
|
||||
console.log("ℹ️ No changes detected for About Us update");
|
||||
}
|
||||
|
||||
// Fetch fresh data for syncing and returning
|
||||
const finalData = await AboutUs.findOne()
|
||||
.select("-_id -__v -createdAt -updatedAt")
|
||||
.lean();
|
||||
|
||||
// Update about.json file to keep it in sync
|
||||
jsonHelper.writeJsonFile("about", finalData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "About Us updated successfully",
|
||||
data: finalData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating about data:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to update about data: " + error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render admin page (Dùng cho Admin UI)
|
||||
*/
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = await AboutUs.getSingle();
|
||||
const rawData = data.toObject();
|
||||
try {
|
||||
const data = await AboutUs.getSingle();
|
||||
const rawData = data.toObject();
|
||||
|
||||
// Lấy tất cả blog để chọn trong CMS
|
||||
const allBlogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).lean();
|
||||
// Lấy tất cả blog để chọn trong CMS
|
||||
const allBlogs = await Blog.find({ status: "published" })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
|
||||
const activeTab = req.query.activeTab || "hero";
|
||||
res.render("admin/aboutUs/index", {
|
||||
layout: "layouts/main",
|
||||
title: "About Us Management",
|
||||
data: rawData,
|
||||
allBlogs,
|
||||
activeTab,
|
||||
user: req.session.user,
|
||||
currentPath: req.path,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
backendUrl: process.env.BACKEND_URL || "http://localhost:3001",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in about index:", err);
|
||||
req.flash("error_msg", "Error loading About Us page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
const activeTab = req.query.activeTab || "hero";
|
||||
res.render("admin/aboutUs/index", {
|
||||
layout: "layouts/main",
|
||||
title: "About Us Management",
|
||||
data: rawData,
|
||||
allBlogs,
|
||||
activeTab,
|
||||
user: req.session.user,
|
||||
currentPath: req.path,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
backendUrl: process.env.BACKEND_URL || "http://localhost:3001",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in about index:", err);
|
||||
req.flash("error_msg", "Error loading About Us page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update method cho form-based submission (Admin UI - Post fallback)
|
||||
*/
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
let updateData = req.body;
|
||||
if (updateData.aboutJson && typeof updateData.aboutJson === "string") {
|
||||
try {
|
||||
updateData = JSON.parse(updateData.aboutJson);
|
||||
} catch (e) {
|
||||
req.flash("error_msg", "Invalid JSON data");
|
||||
return res.redirect("/admin/about-us");
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await AboutUs.getSingle();
|
||||
doc.set(updateData);
|
||||
await doc.save();
|
||||
|
||||
const finalData = await AboutUs.findOne().select("-_id -__v -createdAt -updatedAt").lean();
|
||||
jsonHelper.writeJsonFile("about", finalData);
|
||||
|
||||
req.flash("success_msg", "About Us updated successfully");
|
||||
const activeTab = req.query.activeTab || "hero";
|
||||
res.redirect(`/admin/about-us?activeTab=${activeTab}`);
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", "Error updating About Us: " + err.message);
|
||||
res.redirect("/admin/about-us");
|
||||
try {
|
||||
let updateData = req.body;
|
||||
if (updateData.aboutJson && typeof updateData.aboutJson === "string") {
|
||||
try {
|
||||
updateData = JSON.parse(updateData.aboutJson);
|
||||
} catch (e) {
|
||||
req.flash("error_msg", "Invalid JSON data");
|
||||
return res.redirect("/admin/about-us");
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await AboutUs.getSingle();
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
|
||||
doc.set(updateData);
|
||||
await doc.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - About Us Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "AboutUs",
|
||||
documentId: doc._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_ABOUT_US,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
const finalData = await AboutUs.findOne()
|
||||
.select("-_id -__v -createdAt -updatedAt")
|
||||
.lean();
|
||||
jsonHelper.writeJsonFile("about", finalData);
|
||||
|
||||
req.flash("success_msg", "About Us updated successfully");
|
||||
const activeTab = req.query.activeTab || "hero";
|
||||
res.redirect(`/admin/about-us?activeTab=${activeTab}`);
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", "Error updating About Us: " + err.message);
|
||||
res.redirect("/admin/about-us");
|
||||
}
|
||||
};
|
||||
|
||||
// Aliases for compatibility
|
||||
|
||||
@@ -1,377 +1,450 @@
|
||||
const AppointmentSubmission = require("../models/appointmentSubmission");
|
||||
const Appointment = require("../models/appointment");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// ==================== CMS ADMIN FUNCTIONS ====================
|
||||
|
||||
// Render admin page for appointment management
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
// If no data in DB, try to load from JSON file
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
// If no data in DB, try to load from JSON file
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
} else {
|
||||
// Create default appointment
|
||||
appointment = await Appointment.create({
|
||||
name: "default",
|
||||
hero: {
|
||||
title: "Make Appointment",
|
||||
backgroundImage: "",
|
||||
subtitle: "",
|
||||
heading: "",
|
||||
description: "",
|
||||
},
|
||||
visaOptions: [],
|
||||
form: {
|
||||
heading: "Request Appointment",
|
||||
fields: [],
|
||||
submitButton: {
|
||||
text: "Request Appointment",
|
||||
icon: "fa-solid fa-arrow-right",
|
||||
buttonClass: "theme-btn",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const query = {};
|
||||
|
||||
if (startDate || endDate) {
|
||||
query.createdAt = {};
|
||||
if (startDate) {
|
||||
query.createdAt.$gte = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
// Set end date to end of day
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
query.createdAt.$lte = end;
|
||||
}
|
||||
}
|
||||
|
||||
const submissions = await AppointmentSubmission.find(query).sort({ createdAt: -1 }).limit(50);
|
||||
|
||||
res.render("admin/appointment/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Appointment Management",
|
||||
data: appointment,
|
||||
submissions,
|
||||
startDate,
|
||||
endDate,
|
||||
user: req.session.user,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
} else {
|
||||
// Create default appointment
|
||||
appointment = await Appointment.create({
|
||||
name: "default",
|
||||
hero: {
|
||||
title: "Make Appointment",
|
||||
backgroundImage: "",
|
||||
subtitle: "",
|
||||
heading: "",
|
||||
description: "",
|
||||
},
|
||||
visaOptions: [],
|
||||
form: {
|
||||
heading: "Request Appointment",
|
||||
fields: [],
|
||||
submitButton: {
|
||||
text: "Request Appointment",
|
||||
icon: "fa-solid fa-arrow-right",
|
||||
buttonClass: "theme-btn",
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading appointment admin page:", err);
|
||||
req.flash("error", "Error loading appointment data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const query = {};
|
||||
|
||||
if (startDate || endDate) {
|
||||
query.createdAt = {};
|
||||
if (startDate) {
|
||||
query.createdAt.$gte = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
// Set end date to end of day
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
query.createdAt.$lte = end;
|
||||
}
|
||||
}
|
||||
|
||||
const submissions = await AppointmentSubmission.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(50);
|
||||
|
||||
res.render("admin/appointment/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Appointment Management",
|
||||
data: appointment,
|
||||
submissions,
|
||||
startDate,
|
||||
endDate,
|
||||
user: req.session.user,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading appointment admin page:", err);
|
||||
req.flash("error", "Error loading appointment data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Update appointment data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, visaOptions, form } = req.body;
|
||||
try {
|
||||
const { hero, visaOptions, form } = req.body;
|
||||
|
||||
// Parse JSON strings if needed
|
||||
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
|
||||
const visaOptionsData = typeof visaOptions === "string" ? JSON.parse(visaOptions) : visaOptions;
|
||||
const formData = typeof form === "string" ? JSON.parse(form) : form;
|
||||
// Parse JSON strings if needed
|
||||
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
|
||||
const visaOptionsData =
|
||||
typeof visaOptions === "string" ? JSON.parse(visaOptions) : visaOptions;
|
||||
const formData = typeof form === "string" ? JSON.parse(form) : form;
|
||||
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
if (appointment) {
|
||||
appointment.hero = heroData;
|
||||
appointment.visaOptions = visaOptionsData;
|
||||
appointment.form = formData;
|
||||
await appointment.save();
|
||||
} else {
|
||||
appointment = await Appointment.create({
|
||||
name: "default",
|
||||
hero: heroData,
|
||||
visaOptions: visaOptionsData,
|
||||
form: formData,
|
||||
});
|
||||
}
|
||||
// Capture before state for audit logging
|
||||
const beforeState = appointment
|
||||
? JSON.parse(JSON.stringify(appointment.toObject()))
|
||||
: null;
|
||||
|
||||
req.flash("success", "Appointment data updated successfully");
|
||||
res.redirect("/admin/appointment");
|
||||
} catch (err) {
|
||||
console.error("Error updating appointment:", err);
|
||||
req.flash("error", "Error updating appointment data");
|
||||
res.redirect("/admin/appointment");
|
||||
if (appointment) {
|
||||
appointment.hero = heroData;
|
||||
appointment.visaOptions = visaOptionsData;
|
||||
appointment.form = formData;
|
||||
await appointment.save();
|
||||
} else {
|
||||
appointment = await Appointment.create({
|
||||
name: "default",
|
||||
hero: heroData,
|
||||
visaOptions: visaOptionsData,
|
||||
form: formData,
|
||||
});
|
||||
}
|
||||
|
||||
// Capture after state for audit logging
|
||||
const afterState = JSON.parse(JSON.stringify(appointment.toObject()));
|
||||
|
||||
// Generate changes diff
|
||||
const changes = beforeState ? diffObject(beforeState, afterState) : [];
|
||||
|
||||
// Write audit log
|
||||
await writeAuditLog({
|
||||
model: "Appointment",
|
||||
documentId: appointment._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_APPOINTMENT,
|
||||
before: beforeState,
|
||||
after: afterState,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
|
||||
req.flash("success", "Appointment data updated successfully");
|
||||
res.redirect("/admin/appointment");
|
||||
} catch (err) {
|
||||
console.error("Error updating appointment:", err);
|
||||
req.flash("error", "Error updating appointment data");
|
||||
res.redirect("/admin/appointment");
|
||||
}
|
||||
};
|
||||
|
||||
// API to get appointment data
|
||||
exports.getAppointmentData = async (req, res) => {
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment data",
|
||||
});
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Public API to get appointment page data (for frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment data not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: appointment.hero,
|
||||
visaOptions: appointment.visaOptions,
|
||||
form: appointment.form,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment API data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment data",
|
||||
});
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment data not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: appointment.hero,
|
||||
visaOptions: appointment.visaOptions,
|
||||
form: appointment.form,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment API data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== APPOINTMENT SUBMISSIONS API ====================
|
||||
|
||||
// API để submit appointment form (từ frontend)
|
||||
exports.submitAppointment = async (req, res) => {
|
||||
try {
|
||||
const { name, email, phone, address, appointmentDate, message, visaTypes } = req.body;
|
||||
try {
|
||||
const { name, email, phone, address, appointmentDate, message, visaTypes } =
|
||||
req.body;
|
||||
|
||||
// Validation
|
||||
if (!name || !email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Create new submission
|
||||
const submission = new AppointmentSubmission({
|
||||
name: name.trim(),
|
||||
email: email.trim().toLowerCase(),
|
||||
phone: phone?.trim() || "",
|
||||
address: address?.trim() || "",
|
||||
appointmentDate: appointmentDate?.trim() || "",
|
||||
message: message?.trim() || "",
|
||||
visaTypes: Array.isArray(visaTypes) ? visaTypes : [],
|
||||
ipAddress: req.ip || req.connection?.remoteAddress || "",
|
||||
userAgent: req.get("User-Agent") || "",
|
||||
});
|
||||
|
||||
await submission.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Thank you! Your appointment request has been submitted. We will contact you soon.",
|
||||
data: {
|
||||
id: submission._id,
|
||||
name: submission.name,
|
||||
email: submission.email,
|
||||
appointmentDate: submission.appointmentDate,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error submitting appointment:", err);
|
||||
|
||||
// Handle validation errors
|
||||
if (err.name === "ValidationError") {
|
||||
const errors = Object.values(err.errors).map((e) => e.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: errors.join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error submitting appointment. Please try again later.",
|
||||
});
|
||||
// Validation
|
||||
if (!name || !email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Create new submission
|
||||
const submission = new AppointmentSubmission({
|
||||
name: name.trim(),
|
||||
email: email.trim().toLowerCase(),
|
||||
phone: phone?.trim() || "",
|
||||
address: address?.trim() || "",
|
||||
appointmentDate: appointmentDate?.trim() || "",
|
||||
message: message?.trim() || "",
|
||||
visaTypes: Array.isArray(visaTypes) ? visaTypes : [],
|
||||
ipAddress: req.ip || req.connection?.remoteAddress || "",
|
||||
userAgent: req.get("User-Agent") || "",
|
||||
});
|
||||
|
||||
await submission.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message:
|
||||
"Thank you! Your appointment request has been submitted. We will contact you soon.",
|
||||
data: {
|
||||
id: submission._id,
|
||||
name: submission.name,
|
||||
email: submission.email,
|
||||
appointmentDate: submission.appointmentDate,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error submitting appointment:", err);
|
||||
|
||||
// Handle validation errors
|
||||
if (err.name === "ValidationError") {
|
||||
const errors = Object.values(err.errors).map((e) => e.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: errors.join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error submitting appointment. Please try again later.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy danh sách appointments (cho admin)
|
||||
exports.getAppointments = async (req, res) => {
|
||||
try {
|
||||
const { status, page = 1, limit = 20 } = req.query;
|
||||
try {
|
||||
const { status, page = 1, limit = 20 } = req.query;
|
||||
|
||||
const query = {};
|
||||
if (status && ["pending", "confirmed", "completed", "cancelled"].includes(status)) {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [appointments, total] = await Promise.all([
|
||||
AppointmentSubmission.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
AppointmentSubmission.countDocuments(query),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointments,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
totalPages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointments:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointments",
|
||||
});
|
||||
const query = {};
|
||||
if (
|
||||
status &&
|
||||
["pending", "confirmed", "completed", "cancelled"].includes(status)
|
||||
) {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [appointments, total] = await Promise.all([
|
||||
AppointmentSubmission.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
AppointmentSubmission.countDocuments(query),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointments,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
totalPages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointments:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointments",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để cập nhật status của appointment
|
||||
exports.updateAppointmentStatus = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, notes } = req.body;
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, notes } = req.body;
|
||||
|
||||
const validStatuses = ["pending", "confirmed", "completed", "cancelled"];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Invalid status",
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = { status };
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
if (status === "confirmed") updateData.confirmedAt = new Date();
|
||||
if (status === "completed") updateData.completedAt = new Date();
|
||||
|
||||
const appointment = await AppointmentSubmission.findByIdAndUpdate(
|
||||
id,
|
||||
updateData,
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error updating appointment",
|
||||
});
|
||||
const validStatuses = ["pending", "confirmed", "completed", "cancelled"];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Invalid status",
|
||||
});
|
||||
}
|
||||
|
||||
// Get the appointment before update for audit logging
|
||||
const beforeAppointment = await AppointmentSubmission.findById(id);
|
||||
if (!beforeAppointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
const beforeState = JSON.parse(
|
||||
JSON.stringify(beforeAppointment.toObject()),
|
||||
);
|
||||
|
||||
const updateData = { status };
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
if (status === "confirmed") updateData.confirmedAt = new Date();
|
||||
if (status === "completed") updateData.completedAt = new Date();
|
||||
|
||||
const appointment = await AppointmentSubmission.findByIdAndUpdate(
|
||||
id,
|
||||
updateData,
|
||||
{ new: true },
|
||||
);
|
||||
|
||||
// Capture after state for audit logging
|
||||
const afterState = JSON.parse(JSON.stringify(appointment.toObject()));
|
||||
|
||||
// Generate changes diff
|
||||
const changes = diffObject(beforeState, afterState);
|
||||
|
||||
// Write audit log
|
||||
await writeAuditLog({
|
||||
model: "AppointmentSubmission",
|
||||
documentId: appointment._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_APPOINTMENT_STATUS,
|
||||
before: beforeState,
|
||||
after: afterState,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error updating appointment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy chi tiết một appointment
|
||||
exports.getAppointmentById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const appointment = await AppointmentSubmission.findById(id);
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const appointment = await AppointmentSubmission.findById(id);
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment",
|
||||
});
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để xóa appointment
|
||||
exports.deleteAppointment = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const appointment = await AppointmentSubmission.findByIdAndDelete(id);
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Appointment deleted successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error deleting appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error deleting appointment",
|
||||
});
|
||||
// Get the appointment before deletion for audit logging
|
||||
const appointment = await AppointmentSubmission.findById(id);
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
const beforeState = JSON.parse(JSON.stringify(appointment.toObject()));
|
||||
|
||||
// Delete the appointment
|
||||
await AppointmentSubmission.findByIdAndDelete(id);
|
||||
|
||||
// Write audit log
|
||||
await writeAuditLog({
|
||||
model: "AppointmentSubmission",
|
||||
documentId: appointment._id,
|
||||
action: AUDIT_ACTIONS.DELETE_APPOINTMENT,
|
||||
before: beforeState,
|
||||
after: null,
|
||||
changes: [],
|
||||
req,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Appointment deleted successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error deleting appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error deleting appointment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
178
controllers/auditLogController.js
Normal file
178
controllers/auditLogController.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const AuditLog = require("../models/auditLog");
|
||||
const User = require("../models/User");
|
||||
|
||||
// Display audit logs with pagination and filtering
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 8; // Default to 8, but allow override
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build filter query
|
||||
const filter = {};
|
||||
|
||||
if (req.query.model) {
|
||||
filter.model = req.query.model;
|
||||
}
|
||||
|
||||
if (req.query.action) {
|
||||
filter.action = req.query.action;
|
||||
}
|
||||
|
||||
if (req.query.user) {
|
||||
filter.performedBy = req.query.user;
|
||||
}
|
||||
|
||||
if (req.query.dateFrom || req.query.dateTo) {
|
||||
filter.createdAt = {};
|
||||
if (req.query.dateFrom) {
|
||||
filter.createdAt.$gte = new Date(req.query.dateFrom);
|
||||
}
|
||||
if (req.query.dateTo) {
|
||||
const dateTo = new Date(req.query.dateTo);
|
||||
dateTo.setHours(23, 59, 59, 999); // End of day
|
||||
filter.createdAt.$lte = dateTo;
|
||||
}
|
||||
}
|
||||
|
||||
// Get audit logs with user population
|
||||
const auditLogs = await AuditLog.find(filter)
|
||||
.populate("performedBy", "username email")
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit);
|
||||
|
||||
// Get total count for pagination
|
||||
const totalCount = await AuditLog.countDocuments(filter);
|
||||
const totalPages = Math.ceil(totalCount / limit);
|
||||
|
||||
// Get unique models and actions for filter dropdowns
|
||||
const uniqueModels = await AuditLog.distinct("model");
|
||||
const uniqueActions = await AuditLog.distinct("action");
|
||||
const users = await User.find({}, "username email").sort({ username: 1 });
|
||||
|
||||
res.render("admin/audit-log/index", {
|
||||
title: "Audit Logs",
|
||||
layout: "layouts/main",
|
||||
auditLogs,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
limit,
|
||||
totalCount,
|
||||
},
|
||||
query: req.query,
|
||||
uniqueModels,
|
||||
uniqueActions,
|
||||
users,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading audit logs:", err);
|
||||
req.flash("error_msg", "Error loading audit logs");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Display single audit log details
|
||||
exports.show = async (req, res) => {
|
||||
try {
|
||||
const auditLog = await AuditLog.findById(req.params.id).populate(
|
||||
"performedBy",
|
||||
"username email",
|
||||
);
|
||||
|
||||
if (!auditLog) {
|
||||
req.flash("error_msg", "Audit log not found");
|
||||
return res.redirect("/admin/audit-logs");
|
||||
}
|
||||
|
||||
res.render("admin/audit-log/show", {
|
||||
title: "Audit Log Details",
|
||||
layout: "layouts/main",
|
||||
auditLog,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading audit log:", err);
|
||||
req.flash("error_msg", "Error loading audit log");
|
||||
res.redirect("/admin/audit-logs");
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint to get audit logs (for AJAX requests)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 7; // Default to 7, but allow override
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const filter = {};
|
||||
|
||||
if (req.query.model) filter.model = req.query.model;
|
||||
if (req.query.action) filter.action = req.query.action;
|
||||
if (req.query.user) filter.performedBy = req.query.user;
|
||||
|
||||
if (req.query.dateFrom || req.query.dateTo) {
|
||||
filter.createdAt = {};
|
||||
if (req.query.dateFrom) {
|
||||
filter.createdAt.$gte = new Date(req.query.dateFrom);
|
||||
}
|
||||
if (req.query.dateTo) {
|
||||
const dateTo = new Date(req.query.dateTo);
|
||||
dateTo.setHours(23, 59, 59, 999);
|
||||
filter.createdAt.$lte = dateTo;
|
||||
}
|
||||
}
|
||||
|
||||
const auditLogs = await AuditLog.find(filter)
|
||||
.populate("performedBy", "username email")
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit);
|
||||
|
||||
const totalCount = await AuditLog.countDocuments(filter);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: auditLogs,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: Math.ceil(totalCount / limit),
|
||||
limit,
|
||||
totalCount,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("API Error:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading audit logs",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete old audit logs (cleanup)
|
||||
exports.cleanup = async (req, res) => {
|
||||
try {
|
||||
const daysToKeep = parseInt(req.body.days) || 90;
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
||||
|
||||
const result = await AuditLog.deleteMany({
|
||||
createdAt: { $lt: cutoffDate },
|
||||
});
|
||||
|
||||
req.flash(
|
||||
"success_msg",
|
||||
`Deleted ${result.deletedCount} old audit logs (older than ${daysToKeep} days)`,
|
||||
);
|
||||
res.redirect("/admin/audit-logs");
|
||||
} catch (err) {
|
||||
console.error("Error cleaning up audit logs:", err);
|
||||
req.flash("error_msg", "Error cleaning up audit logs");
|
||||
res.redirect("/admin/audit-logs");
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Contact = require("../models/contact");
|
||||
const ContactSubmission = require("../models/contactSubmission");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// Get contact data from MongoDB
|
||||
const getContactData = async () => {
|
||||
@@ -74,7 +77,11 @@ exports.index = async (req, res) => {
|
||||
heading: "",
|
||||
description: "",
|
||||
fields: [],
|
||||
submitButton: { text: "Send Message", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" },
|
||||
submitButton: {
|
||||
text: "Send Message",
|
||||
icon: "fa-solid fa-arrow-right",
|
||||
buttonClass: "theme-btn style-2",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -94,7 +101,9 @@ exports.index = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const submissions = await ContactSubmission.find(query).sort({ createdAt: -1 }).limit(50);
|
||||
const submissions = await ContactSubmission.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(50);
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
|
||||
res.render("admin/contact/index", {
|
||||
@@ -141,6 +150,11 @@ exports.update = async (req, res) => {
|
||||
// Tìm hoặc tạo contact
|
||||
let contact = await Contact.findOne({ name: "default" });
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = contact
|
||||
? JSON.parse(JSON.stringify(contact.toObject()))
|
||||
: {};
|
||||
|
||||
if (!contact) {
|
||||
// Tạo mới với default values
|
||||
contact = new Contact({
|
||||
@@ -157,7 +171,11 @@ exports.update = async (req, res) => {
|
||||
contactCards: (contactCardsData || []).map((card) => ({
|
||||
...card,
|
||||
iconType: card.iconType || "",
|
||||
iconSource: card.iconSource || (card.iconType && card.iconType.startsWith('/uploads/') ? 'image' : 'fontawesome'),
|
||||
iconSource:
|
||||
card.iconSource ||
|
||||
(card.iconType && card.iconType.startsWith("/uploads/")
|
||||
? "image"
|
||||
: "fontawesome"),
|
||||
})),
|
||||
map: mapData || {
|
||||
coordinates: { lat: 0, lng: 0 },
|
||||
@@ -177,7 +195,11 @@ exports.update = async (req, res) => {
|
||||
heading: "",
|
||||
description: "",
|
||||
fields: [],
|
||||
submitButton: { text: "Send Message", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" },
|
||||
submitButton: {
|
||||
text: "Send Message",
|
||||
icon: "fa-solid fa-arrow-right",
|
||||
buttonClass: "theme-btn style-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -188,7 +210,11 @@ exports.update = async (req, res) => {
|
||||
contact.contactCards = contactCardsData.map((card) => ({
|
||||
...card,
|
||||
iconType: card.iconType || "",
|
||||
iconSource: card.iconSource || (card.iconType && card.iconType.startsWith('/uploads/') ? 'image' : 'fontawesome'),
|
||||
iconSource:
|
||||
card.iconSource ||
|
||||
(card.iconType && card.iconType.startsWith("/uploads/")
|
||||
? "image"
|
||||
: "fontawesome"),
|
||||
}));
|
||||
}
|
||||
if (mapData) contact.map = mapData;
|
||||
@@ -197,6 +223,23 @@ exports.update = async (req, res) => {
|
||||
|
||||
await contact.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(contact.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Contact Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Contact",
|
||||
documentId: contact._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_CONTACT,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Contact updated successfully");
|
||||
res.redirect("/admin/contact");
|
||||
} catch (err) {
|
||||
@@ -321,7 +364,7 @@ exports.updateSubmissionStatus = async (req, res) => {
|
||||
const submission = await ContactSubmission.findByIdAndUpdate(
|
||||
id,
|
||||
updateData,
|
||||
{ new: true }
|
||||
{ new: true },
|
||||
);
|
||||
|
||||
if (!submission) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Home = require("../models/home");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// Helper to get FAQ data from Home model
|
||||
const getFaqData = async () => {
|
||||
@@ -9,7 +12,7 @@ const getFaqData = async () => {
|
||||
subheading: "",
|
||||
description: "",
|
||||
items: [],
|
||||
ctaButton: { label: "", href: "" }
|
||||
ctaButton: { label: "", href: "" },
|
||||
};
|
||||
}
|
||||
return home.faq.toObject ? home.faq.toObject() : home.faq;
|
||||
@@ -41,7 +44,7 @@ exports.index = async (req, res) => {
|
||||
subheading: data.subheading || "",
|
||||
description: data.description || "",
|
||||
ctaButton: data.ctaButton || { label: "", href: "" },
|
||||
items: data.items || []
|
||||
items: data.items || [],
|
||||
};
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
@@ -64,12 +67,13 @@ exports.index = async (req, res) => {
|
||||
// Update FAQ data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { heading, subheading, description, ctaLabel, ctaHref, items } = req.body;
|
||||
const { heading, subheading, description, ctaLabel, ctaHref, items } =
|
||||
req.body;
|
||||
|
||||
let parsedItems = [];
|
||||
if (items) {
|
||||
try {
|
||||
parsedItems = typeof items === 'string' ? JSON.parse(items) : items;
|
||||
parsedItems = typeof items === "string" ? JSON.parse(items) : items;
|
||||
} catch (e) {
|
||||
console.error("Error parsing items JSON:", e);
|
||||
parsedItems = [];
|
||||
@@ -81,22 +85,47 @@ exports.update = async (req, res) => {
|
||||
home = new Home({});
|
||||
}
|
||||
|
||||
home.faq = {
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = home.faq
|
||||
? JSON.parse(
|
||||
JSON.stringify(home.faq.toObject ? home.faq.toObject() : home.faq),
|
||||
)
|
||||
: {};
|
||||
|
||||
const updatedFaqData = {
|
||||
heading: heading || "",
|
||||
subheading: subheading || "",
|
||||
description: description || "",
|
||||
ctaButton: {
|
||||
label: ctaLabel || "",
|
||||
href: ctaHref || ""
|
||||
href: ctaHref || "",
|
||||
},
|
||||
items: parsedItems.map(item => ({
|
||||
items: parsedItems.map((item) => ({
|
||||
question: item.question || "",
|
||||
answer: item.answer || ""
|
||||
}))
|
||||
answer: item.answer || "",
|
||||
})),
|
||||
};
|
||||
|
||||
home.faq = updatedFaqData;
|
||||
await home.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(updatedFaqData));
|
||||
|
||||
// ✅ AUDIT LOGGING - FAQ Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Home",
|
||||
documentId: home._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_FAQ,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "FAQ section updated successfully");
|
||||
res.redirect("/admin/home/faq");
|
||||
} catch (err) {
|
||||
@@ -107,11 +136,19 @@ exports.update = async (req, res) => {
|
||||
};
|
||||
|
||||
// Placeholder methods to prevent route crashes if routes are not cleaned up immediately
|
||||
exports.addFAQ = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.updateFAQItem = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.deleteFAQItem = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.addFAQSection = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.updateFAQSection = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.deleteFAQSection = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.reorderFAQSection = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.updateSidebarNav = (req, res) => res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.addFAQ = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.updateFAQItem = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.deleteFAQItem = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.addFAQSection = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.updateFAQSection = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.deleteFAQSection = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.reorderFAQSection = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
exports.updateSidebarNav = (req, res) =>
|
||||
res.status(404).json({ error: "Endpoint deprecated" });
|
||||
|
||||
@@ -1,141 +1,167 @@
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Footer = require("../models/footer");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// GET /api/footer - Public API cho website và CMS load dữ liệu
|
||||
exports.getFooter = async (req, res) => {
|
||||
try {
|
||||
const footer = await Footer.getSingle();
|
||||
const processedData = addBaseUrlToImages(footer.toObject());
|
||||
try {
|
||||
const footer = await Footer.getSingle();
|
||||
const processedData = addBaseUrlToImages(footer.toObject());
|
||||
|
||||
res.json(processedData);
|
||||
} catch (error) {
|
||||
console.error("Error getting footer:", error);
|
||||
res.status(500).json({
|
||||
error: "Failed to get footer data",
|
||||
});
|
||||
}
|
||||
res.json(processedData);
|
||||
} catch (error) {
|
||||
console.error("Error getting footer:", error);
|
||||
res.status(500).json({
|
||||
error: "Failed to get footer data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /api/admin/footer - Update toàn bộ footer cho CMS
|
||||
exports.updateFooter = async (req, res) => {
|
||||
try {
|
||||
let updateData = req.body;
|
||||
try {
|
||||
let updateData = req.body;
|
||||
|
||||
console.log("=== FOOTER UPDATE REQUEST RECEIVED ===");
|
||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||
console.log("=== FOOTER UPDATE REQUEST RECEIVED ===");
|
||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||
|
||||
// Nếu có footerJson, parse nó (tương tự Header logic)
|
||||
if (updateData.footerJson && typeof updateData.footerJson === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(updateData.footerJson);
|
||||
console.log("✓ Parsed footerJson successfully:", parsedData);
|
||||
updateData = parsedData;
|
||||
} catch (e) {
|
||||
console.error("✗ Error parsing footerJson:", e.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in footerJson: " + e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Lấy footer hiện tại hoặc tạo mới (giống Header logic)
|
||||
let footer = await Footer.findOne();
|
||||
|
||||
if (!footer) {
|
||||
console.log("No existing footer found, creating new one");
|
||||
footer = new Footer(updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer created:", footer._id);
|
||||
} else {
|
||||
console.log("✓ Found existing footer:", footer._id);
|
||||
// Merge với dữ liệu cũ thay vì overwrite (giống Header)
|
||||
Object.assign(footer, updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer updated successfully");
|
||||
}
|
||||
|
||||
const processedData = addBaseUrlToImages(footer.toObject());
|
||||
|
||||
console.log("Updated footer data:", JSON.stringify(processedData, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Footer updated successfully",
|
||||
data: processedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("✗ Error updating footer:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to update footer: " + error.message,
|
||||
// Nếu có footerJson, parse nó (tương tự Header logic)
|
||||
if (updateData.footerJson && typeof updateData.footerJson === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(updateData.footerJson);
|
||||
console.log("✓ Parsed footerJson successfully:", parsedData);
|
||||
updateData = parsedData;
|
||||
} catch (e) {
|
||||
console.error("✗ Error parsing footerJson:", e.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid JSON in footerJson: " + e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Lấy footer hiện tại hoặc tạo mới (giống Header logic)
|
||||
let footer = await Footer.findOne();
|
||||
|
||||
if (!footer) {
|
||||
console.log("No existing footer found, creating new one");
|
||||
footer = new Footer(updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer created:", footer._id);
|
||||
} else {
|
||||
console.log("✓ Found existing footer:", footer._id);
|
||||
// Merge với dữ liệu cũ thay vì overwrite (giống Header)
|
||||
Object.assign(footer, updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer updated successfully");
|
||||
}
|
||||
|
||||
const processedData = addBaseUrlToImages(footer.toObject());
|
||||
|
||||
console.log("Updated footer data:", JSON.stringify(processedData, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Footer updated successfully",
|
||||
data: processedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("✗ Error updating footer:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to update footer: " + error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Render admin view (giữ lại cho UI hiện tại)
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = await Footer.getSingle();
|
||||
const processedData = addBaseUrlToImages(data.toObject());
|
||||
try {
|
||||
const data = await Footer.getSingle();
|
||||
const processedData = addBaseUrlToImages(data.toObject());
|
||||
|
||||
res.render("admin/footer/index", {
|
||||
title: "Footer Management",
|
||||
data: processedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in footer index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
res.render("admin/footer/index", {
|
||||
title: "Footer Management",
|
||||
data: processedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in footer index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Update method cho form hiện tại (giống Header pattern)
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
let updateData = req.body;
|
||||
try {
|
||||
let updateData = req.body;
|
||||
|
||||
console.log("=== FOOTER FORM UPDATE REQUEST RECEIVED ===");
|
||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||
console.log("=== FOOTER FORM UPDATE REQUEST RECEIVED ===");
|
||||
console.log("Raw body:", JSON.stringify(req.body, null, 2));
|
||||
|
||||
// Nếu có footerJson, parse nó (giống Header logic)
|
||||
if (updateData.footerJson && typeof updateData.footerJson === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(updateData.footerJson);
|
||||
console.log("✓ Parsed footerJson successfully:", parsedData);
|
||||
updateData = parsedData;
|
||||
} catch (e) {
|
||||
console.error("✗ Error parsing footerJson:", e.message);
|
||||
req.flash("error_msg", "Invalid JSON in footerJson: " + e.message);
|
||||
return res.redirect("/admin/footer");
|
||||
}
|
||||
}
|
||||
|
||||
// Lấy footer hiện tại hoặc tạo mới (giống Header)
|
||||
let footer = await Footer.findOne();
|
||||
|
||||
if (!footer) {
|
||||
console.log("No existing footer found, creating new one");
|
||||
footer = new Footer(updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer created:", footer._id);
|
||||
req.flash("success_msg", "Footer created successfully");
|
||||
} else {
|
||||
console.log("✓ Found existing footer:", footer._id);
|
||||
// Merge với dữ liệu cũ (giống Header)
|
||||
Object.assign(footer, updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer updated successfully");
|
||||
req.flash("success_msg", "Footer updated successfully");
|
||||
}
|
||||
|
||||
const activeTab = req.body.activeTab || "about";
|
||||
res.redirect(`/admin/footer?activeTab=${activeTab}`);
|
||||
} catch (err) {
|
||||
console.error("✗ Error updating footer:", err);
|
||||
req.flash("error_msg", err.message || "Error updating footer");
|
||||
res.redirect("/admin/footer");
|
||||
// Nếu có footerJson, parse nó (giống Header logic)
|
||||
if (updateData.footerJson && typeof updateData.footerJson === "string") {
|
||||
try {
|
||||
const parsedData = JSON.parse(updateData.footerJson);
|
||||
console.log("✓ Parsed footerJson successfully:", parsedData);
|
||||
updateData = parsedData;
|
||||
} catch (e) {
|
||||
console.error("✗ Error parsing footerJson:", e.message);
|
||||
req.flash("error_msg", "Invalid JSON in footerJson: " + e.message);
|
||||
return res.redirect("/admin/footer");
|
||||
}
|
||||
}
|
||||
|
||||
// Lấy footer hiện tại hoặc tạo mới (giống Header)
|
||||
let footer = await Footer.findOne();
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = footer
|
||||
? JSON.parse(JSON.stringify(footer.toObject()))
|
||||
: {};
|
||||
|
||||
if (!footer) {
|
||||
console.log("No existing footer found, creating new one");
|
||||
footer = new Footer(updateData);
|
||||
await footer.save();
|
||||
console.log("✓ Footer created:", footer._id);
|
||||
req.flash("success_msg", "Footer created successfully");
|
||||
} else {
|
||||
console.log("✓ Found existing footer:", footer._id);
|
||||
// Merge với dữ liệu cũ (giống Header)
|
||||
Object.assign(footer, updateData);
|
||||
await footer.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(footer.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Footer Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Footer",
|
||||
documentId: footer._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_FOOTER,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✓ Footer updated successfully");
|
||||
req.flash("success_msg", "Footer updated successfully");
|
||||
}
|
||||
|
||||
const activeTab = req.body.activeTab || "about";
|
||||
res.redirect(`/admin/footer?activeTab=${activeTab}`);
|
||||
} catch (err) {
|
||||
console.error("✗ Error updating footer:", err);
|
||||
req.flash("error_msg", err.message || "Error updating footer");
|
||||
res.redirect("/admin/footer");
|
||||
}
|
||||
};
|
||||
|
||||
// Legacy API endpoints (giữ lại cho tương thích)
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
|
||||
const Home = require("../models/home");
|
||||
const Blog = require("../models/blog");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// Các hàm hỗ trợ
|
||||
const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 });
|
||||
@@ -30,10 +33,28 @@ const getDefaultHomeData = () => ({
|
||||
ctaButton: {},
|
||||
},
|
||||
visaSolutions: { heading: "", subheading: "", items: [] },
|
||||
visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} },
|
||||
testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] },
|
||||
visaCountries: {
|
||||
heading: "",
|
||||
subheading: "",
|
||||
description: "",
|
||||
countries: [],
|
||||
ctaButton: {},
|
||||
},
|
||||
testimonials: {
|
||||
heading: "",
|
||||
subheading: "",
|
||||
videoUrl: "",
|
||||
videoThumbnail: "",
|
||||
items: [],
|
||||
},
|
||||
videoGallery: { heading: "", videoUrl: "", thumbnail: "" },
|
||||
faq: { heading: "", subheading: "", description: "", ctaButton: {}, items: [] },
|
||||
faq: {
|
||||
heading: "",
|
||||
subheading: "",
|
||||
description: "",
|
||||
ctaButton: {},
|
||||
items: [],
|
||||
},
|
||||
achievements: { heading: "", subheading: "", items: [] },
|
||||
partners: { visaConsultancy: { items: [] }, brands: { items: [] } },
|
||||
blogPreview: {
|
||||
@@ -41,7 +62,7 @@ const getDefaultHomeData = () => ({
|
||||
subheading: "Visa Tips & Guides",
|
||||
ctaButton: { label: "View All Articles", href: "/blog" },
|
||||
items: [],
|
||||
selectedBlogIds: [] // Array of manually selected blog IDs
|
||||
selectedBlogIds: [], // Array of manually selected blog IDs
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,7 +74,7 @@ exports.index = async (req, res) => {
|
||||
|
||||
// Merge dữ liệu mặc định cho tất cả các phần
|
||||
const sections = Object.keys(defaults);
|
||||
sections.forEach(s => {
|
||||
sections.forEach((s) => {
|
||||
data[s] = data[s] || defaults[s];
|
||||
});
|
||||
|
||||
@@ -61,7 +82,9 @@ exports.index = async (req, res) => {
|
||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||
|
||||
// Lấy tất cả blog để chọn trong CMS
|
||||
const allBlogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).lean();
|
||||
const allBlogs = await Blog.find({ status: "published" })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
|
||||
return res.render("admin/home/index", {
|
||||
layout: "layouts/main",
|
||||
@@ -85,17 +108,28 @@ exports.index = async (req, res) => {
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const sections = [
|
||||
"hero", "whyChooseUs", "visaSolutions", "visaCountries",
|
||||
"testimonials", "videoGallery", "faq", "achievements",
|
||||
"partners", "blogPreview"
|
||||
"hero",
|
||||
"whyChooseUs",
|
||||
"visaSolutions",
|
||||
"visaCountries",
|
||||
"testimonials",
|
||||
"videoGallery",
|
||||
"faq",
|
||||
"achievements",
|
||||
"partners",
|
||||
"blogPreview",
|
||||
];
|
||||
|
||||
let doc = await getHomeDoc();
|
||||
const beforeData = doc ? JSON.parse(JSON.stringify(doc.toObject())) : {};
|
||||
|
||||
if (!doc) {
|
||||
doc = new Home({});
|
||||
}
|
||||
|
||||
let hasChanges = false;
|
||||
const updatedSections = [];
|
||||
|
||||
for (const section of sections) {
|
||||
if (req.body[section]) {
|
||||
try {
|
||||
@@ -104,6 +138,7 @@ exports.update = async (req, res) => {
|
||||
doc[section] = payload;
|
||||
doc.markModified(section);
|
||||
hasChanges = true;
|
||||
updatedSections.push(section);
|
||||
} catch (e) {
|
||||
console.error(`Invalid JSON for ${section}:`, e);
|
||||
}
|
||||
@@ -116,6 +151,22 @@ exports.update = async (req, res) => {
|
||||
}
|
||||
|
||||
await doc.save();
|
||||
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Home Update
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Home",
|
||||
documentId: doc._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_HOME,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Home page configuration has been updated!");
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
} catch (err) {
|
||||
@@ -128,7 +179,10 @@ exports.update = async (req, res) => {
|
||||
// Public API// API lấy danh sách blog cho CMS
|
||||
exports.apiGetBlogs = async (req, res) => {
|
||||
try {
|
||||
const blogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).select("title slug featuredImage author publishedAt").lean();
|
||||
const blogs = await Blog.find({ status: "published" })
|
||||
.sort({ createdAt: -1 })
|
||||
.select("title slug featuredImage author publishedAt")
|
||||
.lean();
|
||||
res.json(blogs);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -137,7 +191,8 @@ exports.apiGetBlogs = async (req, res) => {
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
let data = await getHomeData();
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
// === Xử lý Blog Preview động ===
|
||||
const blogPreview = data.blogPreview || {};
|
||||
@@ -147,12 +202,15 @@ exports.api = async (req, res) => {
|
||||
if (blogPreview.selectedBlogIds && blogPreview.selectedBlogIds.length > 0) {
|
||||
blogs = await Blog.find({
|
||||
_id: { $in: blogPreview.selectedBlogIds },
|
||||
status: "published"
|
||||
status: "published",
|
||||
}).lean();
|
||||
|
||||
// Sắp xếp theo thứ tự đã chọn trong selectedBlogIds
|
||||
blogs.sort((a, b) => {
|
||||
return blogPreview.selectedBlogIds.indexOf(a._id.toString()) - blogPreview.selectedBlogIds.indexOf(b._id.toString());
|
||||
return (
|
||||
blogPreview.selectedBlogIds.indexOf(a._id.toString()) -
|
||||
blogPreview.selectedBlogIds.indexOf(b._id.toString())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,18 +223,18 @@ exports.api = async (req, res) => {
|
||||
}
|
||||
|
||||
// Map dữ liệu blog sang format mà frontend mong đợi
|
||||
blogPreview.items = blogs.map(blog => ({
|
||||
blogPreview.items = blogs.map((blog) => ({
|
||||
title: blog.title,
|
||||
excerpt: blog.excerpt,
|
||||
category: blog.category && blog.category[0] ? blog.category[0] : "Visa",
|
||||
date: blog.publishedAt || blog.createdAt,
|
||||
author: {
|
||||
name: blog.author || "Admin",
|
||||
avatar: "" // Frontend đang tự xử lý hoặc dùng logo hệ thống
|
||||
avatar: "", // Frontend đang tự xử lý hoặc dùng logo hệ thống
|
||||
},
|
||||
comments: blog.commentsCount || 0,
|
||||
link: `/blog/${blog.slug}`,
|
||||
thumbnail: blog.featuredImage
|
||||
thumbnail: blog.featuredImage,
|
||||
}));
|
||||
|
||||
data.blogPreview = blogPreview;
|
||||
@@ -189,4 +247,3 @@ exports.api = async (req, res) => {
|
||||
return res.status(500).json({ error: "Error loading home data" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
const Insurance = require("../models/insurance");
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// API để lấy insurance data (cho frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const language = req.query.lang || "en";
|
||||
|
||||
|
||||
// Sử dụng getDefault để đảm bảo luôn có data
|
||||
const insurance = await Insurance.getDefault(language);
|
||||
|
||||
|
||||
// Trả về data với cấu trúc mới
|
||||
const insuranceData = insurance.toObject();
|
||||
|
||||
|
||||
// Sử dụng helper để thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(insuranceData, baseUrl);
|
||||
|
||||
|
||||
// Trả về trực tiếp hero, page, content (không wrap trong object)
|
||||
res.json({
|
||||
hero: processedData.hero,
|
||||
page: processedData.page,
|
||||
content: processedData.content
|
||||
content: processedData.content,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("API Error:", error);
|
||||
res.status(500).json({
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading insurance data",
|
||||
message: error.message
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -37,31 +40,34 @@ exports.api = async (req, res) => {
|
||||
exports.getInsuranceData = async (req, res) => {
|
||||
try {
|
||||
const language = req.query.lang || "en";
|
||||
const insurance = await Insurance.findOne({ name: "default", language: language });
|
||||
|
||||
const insurance = await Insurance.findOne({
|
||||
name: "default",
|
||||
language: language,
|
||||
});
|
||||
|
||||
if (!insurance) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Insurance data not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Insurance data not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const insuranceData = insurance.toObject();
|
||||
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(insuranceData, baseUrl);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: processedData
|
||||
data: processedData,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error getting insurance data:", error);
|
||||
res.status(500).json({
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading insurance data"
|
||||
error: "Error loading insurance data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -70,36 +76,39 @@ exports.getInsuranceData = async (req, res) => {
|
||||
exports.getByLanguage = async (req, res) => {
|
||||
try {
|
||||
const language = req.params.lang || "en";
|
||||
|
||||
const insurance = await Insurance.findOne({ name: "default", language: language });
|
||||
|
||||
|
||||
const insurance = await Insurance.findOne({
|
||||
name: "default",
|
||||
language: language,
|
||||
});
|
||||
|
||||
if (!insurance) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Insurance data not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Insurance data not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const insuranceData = insurance.toObject();
|
||||
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(insuranceData, baseUrl);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processedData.hero,
|
||||
page: processedData.page,
|
||||
content: processedData.content
|
||||
}
|
||||
content: processedData.content,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error getting insurance by language:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading insurance data"
|
||||
error: "Error loading insurance data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -110,18 +119,17 @@ exports.index = async (req, res) => {
|
||||
// Luôn đảm bảo có default data
|
||||
const insurance = await Insurance.getDefault("en");
|
||||
const data = insurance.toObject();
|
||||
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
|
||||
res.render("admin/insurance/index", {
|
||||
title: "Insurance Management",
|
||||
layout: "layouts/main",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in insurance index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
@@ -132,18 +140,18 @@ exports.index = async (req, res) => {
|
||||
// Seed data từ JSON file (cấu trúc mới)
|
||||
exports.seed = async (req, res) => {
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
// Đọc file JSON
|
||||
const jsonPath = path.join(__dirname, '../data/insurance.json');
|
||||
const jsonData = JSON.parse(await fs.readFile(jsonPath, 'utf8'));
|
||||
|
||||
console.log('Seeding insurance from JSON...');
|
||||
|
||||
const jsonPath = path.join(__dirname, "../data/insurance.json");
|
||||
const jsonData = JSON.parse(await fs.readFile(jsonPath, "utf8"));
|
||||
|
||||
console.log("Seeding insurance from JSON...");
|
||||
|
||||
// Migrate từ cấu trúc cũ sang mới
|
||||
const insurance = await Insurance.migrateFromJson(jsonData, "en");
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Insurance data seeded successfully",
|
||||
@@ -151,15 +159,14 @@ exports.seed = async (req, res) => {
|
||||
id: insurance._id,
|
||||
hero: insurance.hero,
|
||||
page: insurance.page,
|
||||
content: insurance.content
|
||||
}
|
||||
content: insurance.content,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error seeding insurance:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error seeding insurance data"
|
||||
error: error.message || "Error seeding insurance data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -168,7 +175,7 @@ exports.seed = async (req, res) => {
|
||||
exports.preview = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content } = req.body;
|
||||
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
@@ -182,15 +189,16 @@ exports.preview = async (req, res) => {
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
const heroData = parseJson(hero) || {};
|
||||
const pageData = parseJson(page) || {};
|
||||
const contentData = parseJson(content) || {};
|
||||
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh cho preview
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedHeroData = addBaseUrlToImages(heroData, baseUrl);
|
||||
|
||||
|
||||
// Render preview HTML
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
@@ -198,13 +206,13 @@ exports.preview = async (req, res) => {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageData.title || 'Insurance Preview'}</title>
|
||||
<title>${pageData.title || "Insurance Preview"}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.hero-section {
|
||||
background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
|
||||
url('${processedHeroData.backgroundImage || ''}');
|
||||
url('${processedHeroData.backgroundImage || ""}');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: white;
|
||||
@@ -226,15 +234,15 @@ exports.preview = async (req, res) => {
|
||||
<body>
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<h1>${heroData.title || 'Insurance'}</h1>
|
||||
<p>${heroData.subtitle || ''}</p>
|
||||
<h1>${heroData.title || "Insurance"}</h1>
|
||||
<p>${heroData.subtitle || ""}</p>
|
||||
</div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h2>${pageData.title || 'Insurance Information'}</h2>
|
||||
${pageData.divider !== false ? '<hr>' : ''}
|
||||
<h2>${pageData.title || "Insurance Information"}</h2>
|
||||
${pageData.divider !== false ? "<hr>" : ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -247,9 +255,8 @@ exports.preview = async (req, res) => {
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error generating preview:", error);
|
||||
res.status(500).send("Error generating preview");
|
||||
@@ -259,72 +266,79 @@ exports.preview = async (req, res) => {
|
||||
// Helper function để render content items
|
||||
function renderContentItems(contentItems) {
|
||||
if (!Array.isArray(contentItems) || contentItems.length === 0) {
|
||||
return '<p>No content available.</p>';
|
||||
return "<p>No content available.</p>";
|
||||
}
|
||||
|
||||
return contentItems.map(item => {
|
||||
switch (item.type) {
|
||||
case 'header':
|
||||
return `<h${item.level || 2} class="content-item">${item.text}</h${item.level || 2}>`;
|
||||
|
||||
case 'paragraph':
|
||||
return `<p class="content-item">${item.text}</p>`;
|
||||
|
||||
case 'section':
|
||||
return `
|
||||
|
||||
return contentItems
|
||||
.map((item) => {
|
||||
switch (item.type) {
|
||||
case "header":
|
||||
return `<h${item.level || 2} class="content-item">${item.text}</h${item.level || 2}>`;
|
||||
|
||||
case "paragraph":
|
||||
return `<p class="content-item">${item.text}</p>`;
|
||||
|
||||
case "section":
|
||||
return `
|
||||
<div class="content-item">
|
||||
<h3>${item.title}</h3>
|
||||
<p>${item.content}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'list':
|
||||
const listItems = (item.items || []).map(li => `<li>${li}</li>`).join('');
|
||||
return `<ul class="content-item">${listItems}</ul>`;
|
||||
|
||||
case 'note':
|
||||
return `<div class="alert alert-info content-item">${item.text}</div>`;
|
||||
|
||||
case 'embed':
|
||||
if (item.source === 'youtube') {
|
||||
return `
|
||||
|
||||
case "list":
|
||||
const listItems = (item.items || [])
|
||||
.map((li) => `<li>${li}</li>`)
|
||||
.join("");
|
||||
return `<ul class="content-item">${listItems}</ul>`;
|
||||
|
||||
case "note":
|
||||
return `<div class="alert alert-info content-item">${item.text}</div>`;
|
||||
|
||||
case "embed":
|
||||
if (item.source === "youtube") {
|
||||
return `
|
||||
<div class="content-item">
|
||||
<iframe width="${item.width || 560}" height="${item.height || 315}"
|
||||
src="${item.url || item.embed}"
|
||||
frameborder="0" allowfullscreen></iframe>
|
||||
${item.caption ? `<p class="text-muted mt-2">${item.caption}</p>` : ''}
|
||||
${item.caption ? `<p class="text-muted mt-2">${item.caption}</p>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
return "";
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
// API để tạo insurance mới (cho các ngôn ngữ khác)
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content, language } = req.body;
|
||||
|
||||
|
||||
if (!language) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Language is required"
|
||||
error: "Language is required",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Kiểm tra đã tồn tại chưa
|
||||
const existing = await Insurance.findOne({ name: "default", language: language });
|
||||
const existing = await Insurance.findOne({
|
||||
name: "default",
|
||||
language: language,
|
||||
});
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Insurance already exists for this language"
|
||||
error: "Insurance already exists for this language",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Parse JSON nếu cần
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
@@ -338,7 +352,7 @@ exports.create = async (req, res) => {
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
const insurance = new Insurance({
|
||||
name: "default",
|
||||
language: language,
|
||||
@@ -347,22 +361,21 @@ exports.create = async (req, res) => {
|
||||
content: parseJson(content) || {},
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false
|
||||
migratedFromOldStructure: false,
|
||||
});
|
||||
|
||||
|
||||
await insurance.save();
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Insurance created successfully for language: " + language,
|
||||
data: insurance
|
||||
data: insurance,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating insurance:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error creating insurance"
|
||||
error: error.message || "Error creating insurance",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -371,7 +384,7 @@ exports.create = async (req, res) => {
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content } = req.body;
|
||||
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
@@ -385,7 +398,7 @@ exports.update = async (req, res) => {
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
// Parse all data với cấu trúc mới
|
||||
const heroData = parseJson(hero) || {};
|
||||
const pageData = parseJson(page) || {};
|
||||
@@ -393,22 +406,23 @@ exports.update = async (req, res) => {
|
||||
|
||||
// Normalize embed blocks (convert YouTube watch URLs to /embed/ URLs)
|
||||
function extractYouTubeId(url) {
|
||||
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
|
||||
const regex =
|
||||
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
if (contentData && Array.isArray(contentData.content)) {
|
||||
contentData.content.forEach(item => {
|
||||
if (item.type === 'embed' && item.source === 'youtube') {
|
||||
if (item.url && item.url.includes('watch?v=')) {
|
||||
contentData.content.forEach((item) => {
|
||||
if (item.type === "embed" && item.source === "youtube") {
|
||||
if (item.url && item.url.includes("watch?v=")) {
|
||||
const videoId = extractYouTubeId(item.url);
|
||||
if (videoId) {
|
||||
item.url = `https://www.youtube.com/embed/${videoId}`;
|
||||
item.videoId = videoId;
|
||||
}
|
||||
}
|
||||
if (item.embed && item.embed.includes('watch?v=')) {
|
||||
if (item.embed && item.embed.includes("watch?v=")) {
|
||||
const videoId = extractYouTubeId(item.embed);
|
||||
if (videoId) {
|
||||
item.embed = `https://www.youtube.com/embed/${videoId}`;
|
||||
@@ -418,10 +432,20 @@ exports.update = async (req, res) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Tìm hoặc tạo insurance
|
||||
let insurance = await Insurance.findOne({ name: "default", language: "en" });
|
||||
|
||||
let insurance = await Insurance.findOne({
|
||||
name: "default",
|
||||
language: "en",
|
||||
});
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = insurance
|
||||
? JSON.parse(
|
||||
JSON.stringify(insurance.toObject ? insurance.toObject() : insurance),
|
||||
)
|
||||
: {};
|
||||
|
||||
if (!insurance) {
|
||||
insurance = new Insurance({
|
||||
name: "default",
|
||||
@@ -430,7 +454,7 @@ exports.update = async (req, res) => {
|
||||
page: pageData,
|
||||
content: contentData,
|
||||
version: "2.0.0",
|
||||
isActive: true
|
||||
isActive: true,
|
||||
});
|
||||
} else {
|
||||
insurance.hero = heroData;
|
||||
@@ -438,12 +462,30 @@ exports.update = async (req, res) => {
|
||||
insurance.content = contentData;
|
||||
insurance.version = "2.0.0";
|
||||
}
|
||||
|
||||
|
||||
await insurance.save();
|
||||
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(
|
||||
JSON.stringify(insurance.toObject ? insurance.toObject() : insurance),
|
||||
);
|
||||
|
||||
// ✅ AUDIT LOGGING - Insurance Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Insurance",
|
||||
documentId: insurance._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_INSURANCE,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Insurance updated successfully");
|
||||
res.redirect("/admin/insurance");
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error updating insurance:", err);
|
||||
req.flash("error_msg", err.message || "Error updating insurance");
|
||||
@@ -455,41 +497,43 @@ exports.update = async (req, res) => {
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const language = req.params.lang;
|
||||
|
||||
|
||||
if (!language) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Language parameter is required"
|
||||
error: "Language parameter is required",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Không cho phép xóa tiếng Anh mặc định
|
||||
if (language === "en") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Cannot delete default English insurance data"
|
||||
error: "Cannot delete default English insurance data",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await Insurance.deleteOne({ name: "default", language: language });
|
||||
|
||||
|
||||
const result = await Insurance.deleteOne({
|
||||
name: "default",
|
||||
language: language,
|
||||
});
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Insurance not found for this language"
|
||||
error: "Insurance not found for this language",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Insurance deleted successfully for language: " + language
|
||||
message: "Insurance deleted successfully for language: " + language,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting insurance:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error deleting insurance"
|
||||
error: error.message || "Error deleting insurance",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,193 +1,229 @@
|
||||
const Pricing = require("../models/pricing");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// ==================== CMS ADMIN FUNCTIONS ====================
|
||||
|
||||
// Render admin page for pricing management
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
// If no data in DB, try to load from JSON file
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
// If no data in DB, try to load from JSON file
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
} else {
|
||||
// Create default pricing
|
||||
pricing = await Pricing.create({
|
||||
name: "default",
|
||||
hero: {
|
||||
title: "Pricing Plan",
|
||||
backgroundImage: "/assets/img/inner-page/breadcrumb.jpg",
|
||||
shapeImage: "/assets/img/inner-page/shape.png",
|
||||
breadcrumb: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Pricing Plan", link: "" },
|
||||
],
|
||||
},
|
||||
pricingSection: {
|
||||
subtitle: "pricing plan",
|
||||
heading: "Flexible Plans to Suit Every Traveler",
|
||||
description: "Choose the plan that fits your visa needs and enjoy expert guidance every step of the way.",
|
||||
},
|
||||
plans: {
|
||||
monthly: [],
|
||||
yearly: [],
|
||||
},
|
||||
testimonials: {
|
||||
subtitle: "What Our Clients Say",
|
||||
heading: "Immigration Success Stories",
|
||||
buttonText: "View All Review",
|
||||
buttonLink: "/contact",
|
||||
buttonIcon: "fa-solid fa-arrow-right",
|
||||
image: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.render("admin/pricing/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Pricing Management",
|
||||
data: pricing,
|
||||
user: req.session.user,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
} else {
|
||||
// Create default pricing
|
||||
pricing = await Pricing.create({
|
||||
name: "default",
|
||||
hero: {
|
||||
title: "Pricing Plan",
|
||||
backgroundImage: "/assets/img/inner-page/breadcrumb.jpg",
|
||||
shapeImage: "/assets/img/inner-page/shape.png",
|
||||
breadcrumb: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Pricing Plan", link: "" },
|
||||
],
|
||||
},
|
||||
pricingSection: {
|
||||
subtitle: "pricing plan",
|
||||
heading: "Flexible Plans to Suit Every Traveler",
|
||||
description:
|
||||
"Choose the plan that fits your visa needs and enjoy expert guidance every step of the way.",
|
||||
},
|
||||
plans: {
|
||||
monthly: [],
|
||||
yearly: [],
|
||||
},
|
||||
testimonials: {
|
||||
subtitle: "What Our Clients Say",
|
||||
heading: "Immigration Success Stories",
|
||||
buttonText: "View All Review",
|
||||
buttonLink: "/contact",
|
||||
buttonIcon: "fa-solid fa-arrow-right",
|
||||
image: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading pricing admin page:", err);
|
||||
req.flash("error", "Error loading pricing data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
res.render("admin/pricing/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Pricing Management",
|
||||
data: pricing,
|
||||
user: req.session.user,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading pricing admin page:", err);
|
||||
req.flash("error", "Error loading pricing data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Update pricing data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, pricingSection, plans, testimonials } = req.body;
|
||||
try {
|
||||
const { hero, pricingSection, plans, testimonials } = req.body;
|
||||
|
||||
// Parse JSON strings if needed
|
||||
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
|
||||
const pricingSectionData = typeof pricingSection === "string" ? JSON.parse(pricingSection) : pricingSection;
|
||||
const plansData = typeof plans === "string" ? JSON.parse(plans) : plans;
|
||||
const testimonialsData = typeof testimonials === "string" ? JSON.parse(testimonials) : testimonials;
|
||||
// Parse JSON strings if needed
|
||||
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
|
||||
const pricingSectionData =
|
||||
typeof pricingSection === "string"
|
||||
? JSON.parse(pricingSection)
|
||||
: pricingSection;
|
||||
const plansData = typeof plans === "string" ? JSON.parse(plans) : plans;
|
||||
const testimonialsData =
|
||||
typeof testimonials === "string"
|
||||
? JSON.parse(testimonials)
|
||||
: testimonials;
|
||||
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
if (pricing) {
|
||||
pricing.hero = heroData;
|
||||
pricing.pricingSection = pricingSectionData;
|
||||
pricing.plans = plansData;
|
||||
pricing.testimonials = testimonialsData;
|
||||
await pricing.save();
|
||||
} else {
|
||||
pricing = await Pricing.create({
|
||||
name: "default",
|
||||
hero: heroData,
|
||||
pricingSection: pricingSectionData,
|
||||
plans: plansData,
|
||||
testimonials: testimonialsData,
|
||||
});
|
||||
}
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = pricing
|
||||
? JSON.parse(JSON.stringify(pricing.toObject()))
|
||||
: {};
|
||||
|
||||
req.flash("success", "Pricing data updated successfully");
|
||||
res.redirect("/admin/pricing");
|
||||
} catch (err) {
|
||||
console.error("Error updating pricing:", err);
|
||||
req.flash("error", "Error updating pricing data");
|
||||
res.redirect("/admin/pricing");
|
||||
if (pricing) {
|
||||
pricing.hero = heroData;
|
||||
pricing.pricingSection = pricingSectionData;
|
||||
pricing.plans = plansData;
|
||||
pricing.testimonials = testimonialsData;
|
||||
await pricing.save();
|
||||
} else {
|
||||
pricing = await Pricing.create({
|
||||
name: "default",
|
||||
hero: heroData,
|
||||
pricingSection: pricingSectionData,
|
||||
plans: plansData,
|
||||
testimonials: testimonialsData,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(pricing.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Pricing Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Pricing",
|
||||
documentId: pricing._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_PRICING,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success", "Pricing data updated successfully");
|
||||
res.redirect("/admin/pricing");
|
||||
} catch (err) {
|
||||
console.error("Error updating pricing:", err);
|
||||
req.flash("error", "Error updating pricing data");
|
||||
res.redirect("/admin/pricing");
|
||||
}
|
||||
};
|
||||
|
||||
// API to get pricing data (admin)
|
||||
exports.getPricingData = async (req, res) => {
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: pricing,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting pricing data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading pricing data",
|
||||
});
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: pricing,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting pricing data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading pricing data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Public API to get pricing page data (for frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pricing) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Pricing data not found",
|
||||
});
|
||||
}
|
||||
|
||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||
|
||||
const getFullUrl = (path) => {
|
||||
if (!path || path.startsWith("http")) return path;
|
||||
return `${backendUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
};
|
||||
|
||||
// Convert to plain object to modify properties safely
|
||||
const pricingData = pricing.toObject ? pricing.toObject() : pricing;
|
||||
|
||||
if (pricingData.hero) {
|
||||
pricingData.hero.backgroundImage = getFullUrl(pricingData.hero.backgroundImage);
|
||||
pricingData.hero.shapeImage = getFullUrl(pricingData.hero.shapeImage);
|
||||
}
|
||||
|
||||
if (pricingData.testimonials) {
|
||||
pricingData.testimonials.image = getFullUrl(pricingData.testimonials.image);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: pricingData.hero,
|
||||
pricingSection: pricingData.pricingSection,
|
||||
plans: pricingData.plans,
|
||||
testimonials: pricingData.testimonials,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting pricing API data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading pricing data",
|
||||
});
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pricing) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Pricing data not found",
|
||||
});
|
||||
}
|
||||
|
||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||
|
||||
const getFullUrl = (path) => {
|
||||
if (!path || path.startsWith("http")) return path;
|
||||
return `${backendUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
};
|
||||
|
||||
// Convert to plain object to modify properties safely
|
||||
const pricingData = pricing.toObject ? pricing.toObject() : pricing;
|
||||
|
||||
if (pricingData.hero) {
|
||||
pricingData.hero.backgroundImage = getFullUrl(
|
||||
pricingData.hero.backgroundImage,
|
||||
);
|
||||
pricingData.hero.shapeImage = getFullUrl(pricingData.hero.shapeImage);
|
||||
}
|
||||
|
||||
if (pricingData.testimonials) {
|
||||
pricingData.testimonials.image = getFullUrl(
|
||||
pricingData.testimonials.image,
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: pricingData.hero,
|
||||
pricingSection: pricingData.pricingSection,
|
||||
plans: pricingData.plans,
|
||||
testimonials: pricingData.testimonials,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting pricing API data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading pricing data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,164 +1,197 @@
|
||||
const Safety = require("../models/safety");
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// Lấy dữ liệu Safety từ MongoDB
|
||||
const getSafetyData = async () => {
|
||||
const safety = await Safety.findOne().sort({ updatedAt: -1 });
|
||||
if (!safety) {
|
||||
return null;
|
||||
}
|
||||
return safety.toObject();
|
||||
const safety = await Safety.findOne().sort({ updatedAt: -1 });
|
||||
if (!safety) {
|
||||
return null;
|
||||
}
|
||||
return safety.toObject();
|
||||
};
|
||||
|
||||
// API endpoint cho frontend
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const safety = await getSafetyData();
|
||||
if (!safety) {
|
||||
return res.status(404).json({ error: "Safety data not found" });
|
||||
}
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(safety, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("Safety API error:", err);
|
||||
res.status(500).json({ error: "Error loading safety data" });
|
||||
}
|
||||
try {
|
||||
const safety = await getSafetyData();
|
||||
if (!safety) {
|
||||
return res.status(404).json({ error: "Safety data not found" });
|
||||
}
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(safety, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("Safety API error:", err);
|
||||
res.status(500).json({ error: "Error loading safety data" });
|
||||
}
|
||||
};
|
||||
|
||||
// Hiển thị danh sách Safety cho admin
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const items = await Safety.find().sort({ updatedAt: -1 }).limit(10);
|
||||
// Lấy bản ghi mới nhất hoặc object rỗng nếu chưa có dữ liệu
|
||||
const latest = items && items.length > 0 ? items[0] : null;
|
||||
const data = latest ? (latest.toObject ? latest.toObject() : latest) : {
|
||||
hero: { title: "", banner: "" },
|
||||
approach: {},
|
||||
approachImgs: [],
|
||||
approachStats: [],
|
||||
approachFeatures: [],
|
||||
approachCards: [],
|
||||
philosophy: {},
|
||||
philosophyCards: [],
|
||||
security: {},
|
||||
securityCards: []
|
||||
};
|
||||
res.render("admin/safety/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Safety Management",
|
||||
items,
|
||||
data,
|
||||
frontendUrl: process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"),
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading Safety data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
try {
|
||||
const items = await Safety.find().sort({ updatedAt: -1 }).limit(10);
|
||||
// Lấy bản ghi mới nhất hoặc object rỗng nếu chưa có dữ liệu
|
||||
const latest = items && items.length > 0 ? items[0] : null;
|
||||
const data = latest
|
||||
? latest.toObject
|
||||
? latest.toObject()
|
||||
: latest
|
||||
: {
|
||||
hero: { title: "", banner: "" },
|
||||
approach: {},
|
||||
approachImgs: [],
|
||||
approachStats: [],
|
||||
approachFeatures: [],
|
||||
approachCards: [],
|
||||
philosophy: {},
|
||||
philosophyCards: [],
|
||||
security: {},
|
||||
securityCards: [],
|
||||
};
|
||||
res.render("admin/safety/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Safety Management",
|
||||
items,
|
||||
data,
|
||||
frontendUrl:
|
||||
process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"),
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading Safety data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Hiển thị form tạo mới Safety
|
||||
exports.createForm = async (req, res) => {
|
||||
try {
|
||||
res.render("admin/safety/create", {
|
||||
layout: "layouts/main",
|
||||
title: "Create Safety",
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading create form");
|
||||
res.redirect("/admin/safety");
|
||||
}
|
||||
try {
|
||||
res.render("admin/safety/create", {
|
||||
layout: "layouts/main",
|
||||
title: "Create Safety",
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading create form");
|
||||
res.redirect("/admin/safety");
|
||||
}
|
||||
};
|
||||
|
||||
// Tạo mới Safety
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const safetyData = req.body; // Tùy chỉnh parse nếu cần
|
||||
const newSafety = new Safety(safetyData);
|
||||
await newSafety.save();
|
||||
req.flash("success_msg", "Safety created successfully");
|
||||
res.redirect("/admin/safety");
|
||||
} catch (err) {
|
||||
console.error("Create error:", err);
|
||||
req.flash("error_msg", `Create error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/safety/create");
|
||||
}
|
||||
try {
|
||||
const safetyData = req.body; // Tùy chỉnh parse nếu cần
|
||||
const newSafety = new Safety(safetyData);
|
||||
await newSafety.save();
|
||||
req.flash("success_msg", "Safety created successfully");
|
||||
res.redirect("/admin/safety");
|
||||
} catch (err) {
|
||||
console.error("Create error:", err);
|
||||
req.flash("error_msg", `Create error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/safety/create");
|
||||
}
|
||||
};
|
||||
|
||||
// Cập nhật Safety
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, approach, philosophy, security } = req.body;
|
||||
try {
|
||||
const { hero, approach, philosophy, security } = req.body;
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const heroData = parseJson(hero);
|
||||
const approachData = parseJson(approach);
|
||||
const philosophyData = parseJson(philosophy);
|
||||
const securityData = parseJson(security);
|
||||
const heroData = parseJson(hero);
|
||||
const approachData = parseJson(approach);
|
||||
const philosophyData = parseJson(philosophy);
|
||||
const securityData = parseJson(security);
|
||||
|
||||
// Tìm hoặc tạo safety record
|
||||
const items = await Safety.find().sort({ updatedAt: -1 }).limit(1);
|
||||
let safety = items && items.length > 0 ? items[0] : null;
|
||||
// Tìm hoặc tạo safety record
|
||||
const items = await Safety.find().sort({ updatedAt: -1 }).limit(1);
|
||||
let safety = items && items.length > 0 ? items[0] : null;
|
||||
|
||||
if (!safety) {
|
||||
// Tạo mới
|
||||
safety = new Safety({
|
||||
hero: heroData || { title: "", banner: "" },
|
||||
approach: approachData || {},
|
||||
philosophy: philosophyData || {},
|
||||
security: securityData || {}
|
||||
});
|
||||
} else {
|
||||
// Cập nhật
|
||||
if (heroData) safety.hero = heroData;
|
||||
if (approachData) safety.approach = approachData;
|
||||
if (philosophyData) safety.philosophy = philosophyData;
|
||||
if (securityData) safety.security = securityData;
|
||||
}
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = safety
|
||||
? JSON.parse(JSON.stringify(safety.toObject ? safety.toObject() : safety))
|
||||
: {};
|
||||
|
||||
await safety.save();
|
||||
if (!safety) {
|
||||
// Tạo mới
|
||||
safety = new Safety({
|
||||
hero: heroData || { title: "", banner: "" },
|
||||
approach: approachData || {},
|
||||
philosophy: philosophyData || {},
|
||||
security: securityData || {},
|
||||
});
|
||||
} else {
|
||||
// Cập nhật
|
||||
if (heroData) safety.hero = heroData;
|
||||
if (approachData) safety.approach = approachData;
|
||||
if (philosophyData) safety.philosophy = philosophyData;
|
||||
if (securityData) safety.security = securityData;
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Safety updated successfully");
|
||||
res.redirect("/admin/safety");
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/safety");
|
||||
}
|
||||
await safety.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(
|
||||
JSON.stringify(safety.toObject ? safety.toObject() : safety),
|
||||
);
|
||||
|
||||
// ✅ AUDIT LOGGING - Safety Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Safety",
|
||||
documentId: safety._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_SAFETY,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Safety updated successfully");
|
||||
res.redirect("/admin/safety");
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/safety");
|
||||
}
|
||||
};
|
||||
|
||||
// Xóa Safety
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const safety = await Safety.findById(req.params.id);
|
||||
if (!safety) {
|
||||
req.flash("error_msg", "Safety record not found");
|
||||
return res.redirect("/admin/safety");
|
||||
}
|
||||
await Safety.findByIdAndDelete(req.params.id);
|
||||
req.flash("success_msg", "Safety record deleted successfully");
|
||||
res.redirect("/admin/safety");
|
||||
} catch (err) {
|
||||
console.error("Delete error:", err);
|
||||
req.flash("error_msg", `Delete error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/safety");
|
||||
}
|
||||
try {
|
||||
const safety = await Safety.findById(req.params.id);
|
||||
if (!safety) {
|
||||
req.flash("error_msg", "Safety record not found");
|
||||
return res.redirect("/admin/safety");
|
||||
}
|
||||
await Safety.findByIdAndDelete(req.params.id);
|
||||
req.flash("success_msg", "Safety record deleted successfully");
|
||||
res.redirect("/admin/safety");
|
||||
} catch (err) {
|
||||
console.error("Delete error:", err);
|
||||
req.flash("error_msg", `Delete error: ${err.message || "Unknown"}`);
|
||||
res.redirect("/admin/safety");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const { getServiceData } = require("../services/service.service");
|
||||
const Service = require("../models/service");
|
||||
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
const slugify = require("slugify");
|
||||
|
||||
// Admin page - Service list
|
||||
@@ -60,6 +64,10 @@ exports.updateService = async (req, res) => {
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
const oldItem = JSON.parse(
|
||||
JSON.stringify(currentData.services.items[serviceIndex]),
|
||||
);
|
||||
|
||||
// Update service data
|
||||
const updatedData = { ...currentData.toObject?.() };
|
||||
updatedData.services.items[serviceIndex] = {
|
||||
@@ -76,7 +84,20 @@ exports.updateService = async (req, res) => {
|
||||
} else {
|
||||
await Service.create(updatedData);
|
||||
}
|
||||
const newItem = updatedData.services.items[serviceIndex];
|
||||
|
||||
const changes = diffObject(oldItem, newItem);
|
||||
console.log("USER:", req.session?.user || req.user || "No user found");
|
||||
|
||||
await writeAuditLog({
|
||||
model: "Service",
|
||||
documentId: currentData._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_SERVICE,
|
||||
before: oldItem,
|
||||
after: newItem,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
req.flash("success_msg", "Service updated successfully");
|
||||
res.redirect("/admin/service");
|
||||
} catch (err) {
|
||||
@@ -169,14 +190,16 @@ exports.updateDetails = async (req, res) => {
|
||||
req.flash("error_msg", "Service not found");
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
const beforeDetails = JSON.parse(
|
||||
JSON.stringify(currentData.services.items[serviceIndex].details || {}),
|
||||
);
|
||||
// Parse features and FAQ from JSON strings
|
||||
const features = req.body.features ? JSON.parse(req.body.features) : [];
|
||||
const faq = req.body.faq ? JSON.parse(req.body.faq) : [];
|
||||
|
||||
// Update service details
|
||||
const updatedData = { ...currentData.toObject?.() };
|
||||
updatedData.services.items[serviceIndex].details = {
|
||||
const updatedDetails = {
|
||||
title: req.body.title,
|
||||
description: req.body.description,
|
||||
mainImage: req.body.mainImage,
|
||||
@@ -185,17 +208,30 @@ exports.updateDetails = async (req, res) => {
|
||||
additionalDescription: req.body.additionalDescription,
|
||||
keyFeaturesTitle: req.body.keyFeaturesTitle,
|
||||
keyFeaturesImage: req.body.keyFeaturesImage,
|
||||
features: features,
|
||||
features,
|
||||
faqTitle: req.body.faqTitle,
|
||||
faqImage: req.body.faqImage,
|
||||
faq: faq,
|
||||
faq,
|
||||
};
|
||||
|
||||
updatedData.services.items[serviceIndex].details = updatedDetails;
|
||||
if (currentData._id) {
|
||||
await Service.findByIdAndUpdate(currentData._id, updatedData);
|
||||
} else {
|
||||
await Service.create(updatedData);
|
||||
}
|
||||
const changes = diffObject(beforeDetails, updatedDetails);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Service",
|
||||
documentId: currentData._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_SERVICE_DETAILS,
|
||||
before: beforeDetails,
|
||||
after: updatedDetails,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Service details updated successfully");
|
||||
res.redirect(`/admin/service/${slug}/details`);
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
// controllers/termsController.js
|
||||
const Terms = require("../models/terms");
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper"); // Import helper
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// API để lấy terms data (cho frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const language = req.query.lang || "en";
|
||||
|
||||
|
||||
// Sử dụng getDefault để đảm bảo luôn có data
|
||||
const terms = await Terms.getDefault(language);
|
||||
|
||||
|
||||
// Trả về data với cấu trúc mới
|
||||
const termsData = terms.toObject();
|
||||
|
||||
|
||||
// Sử dụng helper để thêm base URL vào đường dẫn ảnh
|
||||
// Truyền baseUrl từ request hoặc từ environment
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(termsData, baseUrl);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processedData.hero,
|
||||
page: processedData.page,
|
||||
content: processedData.content
|
||||
}
|
||||
content: processedData.content,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("API Error:", error);
|
||||
res.status(500).json({
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading terms data",
|
||||
message: error.message
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -42,30 +45,30 @@ exports.getTermsData = async (req, res) => {
|
||||
try {
|
||||
const language = req.query.lang || "en";
|
||||
const terms = await Terms.findOne({ name: "default", language: language });
|
||||
|
||||
|
||||
if (!terms) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms data not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms data not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const termsData = terms.toObject();
|
||||
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(termsData, baseUrl);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: processedData
|
||||
data: processedData,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error getting terms data:", error);
|
||||
res.status(500).json({
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading terms data"
|
||||
error: "Error loading terms data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -74,36 +77,36 @@ exports.getTermsData = async (req, res) => {
|
||||
exports.getByLanguage = async (req, res) => {
|
||||
try {
|
||||
const language = req.params.lang || "en";
|
||||
|
||||
|
||||
const terms = await Terms.findOne({ name: "default", language: language });
|
||||
|
||||
|
||||
if (!terms) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms data not found for language: " + language
|
||||
error: "Terms data not found for language: " + language,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const termsData = terms.toObject();
|
||||
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(termsData, baseUrl);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processedData.hero,
|
||||
page: processedData.page,
|
||||
content: processedData.content
|
||||
}
|
||||
content: processedData.content,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error getting terms by language:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading terms data"
|
||||
error: "Error loading terms data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -114,18 +117,17 @@ exports.index = async (req, res) => {
|
||||
// Luôn đảm bảo có default data
|
||||
const terms = await Terms.getDefault("en");
|
||||
const data = terms.toObject();
|
||||
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
|
||||
res.render("admin/terms/index", {
|
||||
title: "Terms & Conditions Management",
|
||||
layout: "layouts/main",
|
||||
data, // Không cần addBaseUrlToImages cho admin view
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in terms index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
@@ -137,7 +139,7 @@ exports.index = async (req, res) => {
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content } = req.body;
|
||||
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
@@ -151,7 +153,7 @@ exports.update = async (req, res) => {
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
// Parse all data với cấu trúc mới
|
||||
const heroData = parseJson(hero) || {};
|
||||
const pageData = parseJson(page) || {};
|
||||
@@ -159,41 +161,48 @@ exports.update = async (req, res) => {
|
||||
|
||||
// Normalize embed blocks (convert YouTube watch URLs to /embed/ URLs)
|
||||
function extractYouTubeId(url) {
|
||||
if (!url || typeof url !== 'string') return null;
|
||||
if (!url || typeof url !== "string") return null;
|
||||
// common YouTube URL patterns
|
||||
const m = url.match(/(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/);
|
||||
const m = url.match(
|
||||
/(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/,
|
||||
);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// Trong exports.update
|
||||
if (contentData && Array.isArray(contentData.content)) {
|
||||
contentData.content = contentData.content.map(item => {
|
||||
if (item && item.type === 'embed') {
|
||||
let embedUrl = item.embed || item.url || item.source || '';
|
||||
|
||||
// Luôn chuyển đổi sang embed URL nếu là watch URL
|
||||
if (embedUrl.includes('youtube.com/watch')) {
|
||||
const videoId = extractYouTubeId(embedUrl);
|
||||
if (videoId) {
|
||||
item.embed = `https://www.youtube.com/embed/${videoId}`;
|
||||
item.videoId = videoId;
|
||||
if (contentData && Array.isArray(contentData.content)) {
|
||||
contentData.content = contentData.content.map((item) => {
|
||||
if (item && item.type === "embed") {
|
||||
let embedUrl = item.embed || item.url || item.source || "";
|
||||
|
||||
// Luôn chuyển đổi sang embed URL nếu là watch URL
|
||||
if (embedUrl.includes("youtube.com/watch")) {
|
||||
const videoId = extractYouTubeId(embedUrl);
|
||||
if (videoId) {
|
||||
item.embed = `https://www.youtube.com/embed/${videoId}`;
|
||||
item.videoId = videoId;
|
||||
}
|
||||
}
|
||||
// Đảm bảo có videoId
|
||||
else if (embedUrl && !item.videoId) {
|
||||
const videoId = extractYouTubeId(embedUrl);
|
||||
if (videoId) {
|
||||
item.videoId = videoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Đảm bảo có videoId
|
||||
else if (embedUrl && !item.videoId) {
|
||||
const videoId = extractYouTubeId(embedUrl);
|
||||
if (videoId) {
|
||||
item.videoId = videoId;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Tìm hoặc tạo terms
|
||||
let terms = await Terms.findOne({ name: "default", language: "en" });
|
||||
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = terms
|
||||
? JSON.parse(JSON.stringify(terms.toObject ? terms.toObject() : terms))
|
||||
: {};
|
||||
|
||||
if (!terms) {
|
||||
// Tạo mới với cấu trúc mới
|
||||
terms = new Terms({
|
||||
@@ -204,7 +213,7 @@ if (contentData && Array.isArray(contentData.content)) {
|
||||
content: contentData,
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false
|
||||
migratedFromOldStructure: false,
|
||||
});
|
||||
} else {
|
||||
// Update existing với cấu trúc mới
|
||||
@@ -215,12 +224,30 @@ if (contentData && Array.isArray(contentData.content)) {
|
||||
terms.migratedFromOldStructure = false;
|
||||
terms.updatedAt = new Date();
|
||||
}
|
||||
|
||||
|
||||
await terms.save();
|
||||
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(
|
||||
JSON.stringify(terms.toObject ? terms.toObject() : terms),
|
||||
);
|
||||
|
||||
// ✅ AUDIT LOGGING - Terms Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Terms",
|
||||
documentId: terms._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_TERMS,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Terms & Conditions updated successfully");
|
||||
res.redirect("/admin/terms-conditions");
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error updating terms:", err);
|
||||
req.flash("error_msg", err.message || "Error updating terms");
|
||||
@@ -231,30 +258,30 @@ if (contentData && Array.isArray(contentData.content)) {
|
||||
// Seed data từ JSON file mới (cấu trúc mới)
|
||||
exports.seed = async (req, res) => {
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
// Đọc file JSON
|
||||
const jsonPath = path.join(__dirname, '../data/terms-conditions.json');
|
||||
const jsonData = JSON.parse(await fs.readFile(jsonPath, 'utf8'));
|
||||
|
||||
console.log('Seeding from JSON...');
|
||||
console.log('JSON structure keys:', Object.keys(jsonData));
|
||||
|
||||
const jsonPath = path.join(__dirname, "../data/terms-conditions.json");
|
||||
const jsonData = JSON.parse(await fs.readFile(jsonPath, "utf8"));
|
||||
|
||||
console.log("Seeding from JSON...");
|
||||
console.log("JSON structure keys:", Object.keys(jsonData));
|
||||
|
||||
// Kiểm tra cấu trúc JSON
|
||||
let terms;
|
||||
if (jsonData.hero && jsonData.page && jsonData.content) {
|
||||
// Cấu trúc mới
|
||||
console.log('Using new structure (hero, page, content)');
|
||||
console.log("Using new structure (hero, page, content)");
|
||||
terms = await Terms.migrateFromNewJson(jsonData, "en");
|
||||
} else if (jsonData.hero && jsonData.termsHeader && jsonData.sections) {
|
||||
// Cấu trúc cũ
|
||||
console.log('Using old structure, converting to new...');
|
||||
console.log("Using old structure, converting to new...");
|
||||
terms = await Terms.migrateFromJson(jsonData, "en");
|
||||
} else {
|
||||
throw new Error("Unknown JSON structure");
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Terms data seeded successfully",
|
||||
@@ -262,15 +289,14 @@ exports.seed = async (req, res) => {
|
||||
id: terms._id,
|
||||
hero: terms.hero,
|
||||
page: terms.page,
|
||||
content: terms.content
|
||||
}
|
||||
content: terms.content,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error seeding terms:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error seeding terms data"
|
||||
error: error.message || "Error seeding terms data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -279,7 +305,7 @@ exports.seed = async (req, res) => {
|
||||
exports.preview = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content } = req.body;
|
||||
|
||||
|
||||
// Parse JSON strings
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
@@ -293,15 +319,16 @@ exports.preview = async (req, res) => {
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
const heroData = parseJson(hero) || {};
|
||||
const pageData = parseJson(page) || {};
|
||||
const contentData = parseJson(content) || {};
|
||||
|
||||
|
||||
// Thêm base URL vào đường dẫn ảnh cho preview
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedHeroData = addBaseUrlToImages(heroData, baseUrl);
|
||||
|
||||
|
||||
// Render preview HTML
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
@@ -309,13 +336,13 @@ exports.preview = async (req, res) => {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageData.title || 'Terms & Conditions Preview'}</title>
|
||||
<title>${pageData.title || "Terms & Conditions Preview"}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.hero-section {
|
||||
background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
|
||||
url('${processedHeroData.backgroundImage || ''}');
|
||||
url('${processedHeroData.backgroundImage || ""}');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: white;
|
||||
@@ -337,14 +364,14 @@ exports.preview = async (req, res) => {
|
||||
<body>
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<h1>${heroData.title || 'Terms & Conditions'}</h1>
|
||||
<h1>${heroData.title || "Terms & Conditions"}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h2>${pageData.title || 'Terms & Conditions'}</h2>
|
||||
${pageData.divider !== false ? '<hr>' : ''}
|
||||
<h2>${pageData.title || "Terms & Conditions"}</h2>
|
||||
${pageData.divider !== false ? "<hr>" : ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,9 +384,8 @@ exports.preview = async (req, res) => {
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error generating preview:", error);
|
||||
res.status(500).send("Error generating preview");
|
||||
@@ -369,87 +395,98 @@ exports.preview = async (req, res) => {
|
||||
// Helper function để render content items
|
||||
function renderContentItems(contentItems) {
|
||||
if (!Array.isArray(contentItems) || contentItems.length === 0) {
|
||||
return '<p>No content available.</p>';
|
||||
return "<p>No content available.</p>";
|
||||
}
|
||||
|
||||
return contentItems.map(item => {
|
||||
switch (item.type) {
|
||||
case 'paragraph':
|
||||
return `<div class="content-item"><p>${item.text || ''}</p></div>`;
|
||||
|
||||
case 'section':
|
||||
let html = `<div class="content-item">`;
|
||||
html += `<h3>${item.title || ''}</h3>`;
|
||||
html += `<p>${item.content || ''}</p>`;
|
||||
|
||||
if (item.subsections && item.subsections.length > 0) {
|
||||
item.subsections.forEach(subsection => {
|
||||
if (subsection.type === 'cancellation_table') {
|
||||
html += `<h4>${subsection.title || ''}</h4>`;
|
||||
if (subsection.items && subsection.items.length > 0) {
|
||||
html += '<ul>';
|
||||
subsection.items.forEach(listItem => {
|
||||
html += `<li>${listItem}</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
return contentItems
|
||||
.map((item) => {
|
||||
switch (item.type) {
|
||||
case "paragraph":
|
||||
return `<div class="content-item"><p>${item.text || ""}</p></div>`;
|
||||
|
||||
case "section":
|
||||
let html = `<div class="content-item">`;
|
||||
html += `<h3>${item.title || ""}</h3>`;
|
||||
html += `<p>${item.content || ""}</p>`;
|
||||
|
||||
if (item.subsections && item.subsections.length > 0) {
|
||||
item.subsections.forEach((subsection) => {
|
||||
if (subsection.type === "cancellation_table") {
|
||||
html += `<h4>${subsection.title || ""}</h4>`;
|
||||
if (subsection.items && subsection.items.length > 0) {
|
||||
html += "<ul>";
|
||||
subsection.items.forEach((listItem) => {
|
||||
html += `<li>${listItem}</li>`;
|
||||
});
|
||||
html += "</ul>";
|
||||
}
|
||||
} else if (subsection.type === "cancellation_section") {
|
||||
html += `<h4>${subsection.title || ""}</h4>`;
|
||||
if (subsection.items && subsection.items.length > 0) {
|
||||
html += "<ul>";
|
||||
subsection.items.forEach((listItem) => {
|
||||
html += `<li>${listItem}</li>`;
|
||||
});
|
||||
html += "</ul>";
|
||||
}
|
||||
} else if (subsection.type === "note") {
|
||||
html += `<div class="alert alert-info">${subsection.text || ""}</div>`;
|
||||
}
|
||||
} else if (subsection.type === 'cancellation_section') {
|
||||
html += `<h4>${subsection.title || ''}</h4>`;
|
||||
if (subsection.items && subsection.items.length > 0) {
|
||||
html += '<ul>';
|
||||
subsection.items.forEach(listItem => {
|
||||
html += `<li>${listItem}</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
} else if (subsection.type === 'note') {
|
||||
html += `<div class="alert alert-info">${subsection.text || ''}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
|
||||
case 'note':
|
||||
return `<div class="content-item alert alert-info">${item.text || ''}</div>`;
|
||||
case 'embed':
|
||||
// Support several embed shapes: { embed }, { url }, { source }, { videoId }
|
||||
const embedSrc = (item.embed || item.url || item.source || (item.videoId ? `https://www.youtube.com/embed/${item.videoId}` : ''));
|
||||
if (!embedSrc) return `<div class="content-item">Invalid embed</div>`;
|
||||
return `<div class="content-item embed-item" style="margin-bottom:20px;">
|
||||
});
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
|
||||
case "note":
|
||||
return `<div class="content-item alert alert-info">${item.text || ""}</div>`;
|
||||
case "embed":
|
||||
// Support several embed shapes: { embed }, { url }, { source }, { videoId }
|
||||
const embedSrc =
|
||||
item.embed ||
|
||||
item.url ||
|
||||
item.source ||
|
||||
(item.videoId
|
||||
? `https://www.youtube.com/embed/${item.videoId}`
|
||||
: "");
|
||||
if (!embedSrc) return `<div class="content-item">Invalid embed</div>`;
|
||||
return `<div class="content-item embed-item" style="margin-bottom:20px;">
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
|
||||
<iframe src="${embedSrc}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
default:
|
||||
return `<div class="content-item">Unknown content type: ${item.type}</div>`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
default:
|
||||
return `<div class="content-item">Unknown content type: ${item.type}</div>`;
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
// API để tạo terms mới (cho các ngôn ngữ khác)
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const { hero, page, content, language } = req.body;
|
||||
|
||||
|
||||
if (!language) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Language is required"
|
||||
error: "Language is required",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Kiểm tra đã tồn tại chưa
|
||||
const existing = await Terms.findOne({ name: "default", language: language });
|
||||
const existing = await Terms.findOne({
|
||||
name: "default",
|
||||
language: language,
|
||||
});
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Terms already exists for language: " + language
|
||||
error: "Terms already exists for language: " + language,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Parse JSON nếu cần
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
@@ -463,7 +500,7 @@ exports.create = async (req, res) => {
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
const terms = new Terms({
|
||||
name: "default",
|
||||
language: language,
|
||||
@@ -472,22 +509,21 @@ exports.create = async (req, res) => {
|
||||
content: parseJson(content) || {},
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false
|
||||
migratedFromOldStructure: false,
|
||||
});
|
||||
|
||||
|
||||
await terms.save();
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Terms created successfully for language: " + language,
|
||||
data: terms
|
||||
data: terms,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating terms:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error creating terms"
|
||||
error: error.message || "Error creating terms",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -496,41 +532,43 @@ exports.create = async (req, res) => {
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const language = req.params.lang;
|
||||
|
||||
|
||||
if (!language) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Language is required"
|
||||
error: "Language is required",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Không cho phép xóa tiếng Anh mặc định
|
||||
if (language === "en") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Cannot delete default English terms"
|
||||
error: "Cannot delete default English terms",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await Terms.deleteOne({ name: "default", language: language });
|
||||
|
||||
|
||||
const result = await Terms.deleteOne({
|
||||
name: "default",
|
||||
language: language,
|
||||
});
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Terms not found for language: " + language
|
||||
error: "Terms not found for language: " + language,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Terms deleted successfully for language: " + language
|
||||
message: "Terms deleted successfully for language: " + language,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting terms:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Error deleting terms"
|
||||
error: error.message || "Error deleting terms",
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,103 +1,138 @@
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Home = require("../models/home");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// Get testimonial data from Home model
|
||||
const getTestimonialData = async () => {
|
||||
const home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
if (!home || !home.testimonials) {
|
||||
return null;
|
||||
}
|
||||
return home.testimonials.toObject ? home.testimonials.toObject() : home.testimonials;
|
||||
const home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
if (!home || !home.testimonials) {
|
||||
return null;
|
||||
}
|
||||
return home.testimonials.toObject
|
||||
? home.testimonials.toObject()
|
||||
: home.testimonials;
|
||||
};
|
||||
|
||||
// API to get testimonial data
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const testimonial = await getTestimonialData();
|
||||
if (!testimonial) {
|
||||
return res.status(404).json({ error: "Testimonial data not found" });
|
||||
}
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(testimonial, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("API Error:", err);
|
||||
res.status(500).json({ error: "Error loading testimonial data" });
|
||||
try {
|
||||
const testimonial = await getTestimonialData();
|
||||
if (!testimonial) {
|
||||
return res.status(404).json({ error: "Testimonial data not found" });
|
||||
}
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(testimonial, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("API Error:", err);
|
||||
res.status(500).json({ error: "Error loading testimonial data" });
|
||||
}
|
||||
};
|
||||
|
||||
// Render admin view
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = (await getTestimonialData()) || {
|
||||
heading: "Student Reviews & Testimonials",
|
||||
subheading: "What Our Students Say",
|
||||
videoUrl: "",
|
||||
videoThumbnail: "",
|
||||
items: [],
|
||||
};
|
||||
try {
|
||||
const data = (await getTestimonialData()) || {
|
||||
heading: "Student Reviews & Testimonials",
|
||||
subheading: "What Our Students Say",
|
||||
videoUrl: "",
|
||||
videoThumbnail: "",
|
||||
items: [],
|
||||
};
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
|
||||
res.render("admin/home/testimonial/index", {
|
||||
title: "Testimonials Management",
|
||||
layout: "layouts/main",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in testimonial index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
res.render("admin/home/testimonial/index", {
|
||||
title: "Testimonials Management",
|
||||
layout: "layouts/main",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in testimonial index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Cập nhật dữ liệu testimonial (chỉ update phần testimonials của Home)
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { heading, subheading, videoUrl, videoThumbnail, items } = req.body;
|
||||
try {
|
||||
const { heading, subheading, videoUrl, videoThumbnail, items } = req.body;
|
||||
|
||||
// Parse JSON strings nếu cần
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const itemsData = parseJson(items);
|
||||
|
||||
// Tìm hoặc tạo Home document
|
||||
let home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
|
||||
if (!home) {
|
||||
home = new Home({});
|
||||
// Parse JSON strings nếu cần
|
||||
const parseJson = (data) => {
|
||||
if (!data) return null;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// Cập nhật chỉ phần testimonials
|
||||
home.testimonials = {
|
||||
heading: heading || "Student Reviews & Testimonials",
|
||||
subheading: subheading || "What Our Students Say",
|
||||
videoUrl: videoUrl || "",
|
||||
videoThumbnail: videoThumbnail || "",
|
||||
items: itemsData || [],
|
||||
};
|
||||
const itemsData = parseJson(items);
|
||||
|
||||
await home.save();
|
||||
// Tìm hoặc tạo Home document
|
||||
let home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
|
||||
req.flash("success_msg", "Testimonials updated successfully");
|
||||
res.redirect("/admin/home/testimonials");
|
||||
} catch (err) {
|
||||
console.error("Error updating testimonials:", err);
|
||||
req.flash("error_msg", err.message || "Error updating testimonials");
|
||||
res.redirect("/admin/home/testimonials");
|
||||
if (!home) {
|
||||
home = new Home({});
|
||||
}
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = home.testimonials
|
||||
? JSON.parse(
|
||||
JSON.stringify(
|
||||
home.testimonials.toObject
|
||||
? home.testimonials.toObject()
|
||||
: home.testimonials,
|
||||
),
|
||||
)
|
||||
: {};
|
||||
|
||||
const updatedTestimonialData = {
|
||||
heading: heading || "Student Reviews & Testimonials",
|
||||
subheading: subheading || "What Our Students Say",
|
||||
videoUrl: videoUrl || "",
|
||||
videoThumbnail: videoThumbnail || "",
|
||||
items: itemsData || [],
|
||||
};
|
||||
|
||||
// Cập nhật chỉ phần testimonials
|
||||
home.testimonials = updatedTestimonialData;
|
||||
|
||||
await home.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(updatedTestimonialData));
|
||||
|
||||
// ✅ AUDIT LOGGING - Testimonial Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Home",
|
||||
documentId: home._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_TESTIMONIAL,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Testimonials updated successfully");
|
||||
res.redirect("/admin/home/testimonials");
|
||||
} catch (err) {
|
||||
console.error("Error updating testimonials:", err);
|
||||
req.flash("error_msg", err.message || "Error updating testimonials");
|
||||
res.redirect("/admin/home/testimonials");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,16 +2,20 @@ const Travel = require("../models/travel");
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
/**
|
||||
* Hàm Helper: Trích xuất ID YouTube từ nhiều định dạng link khác nhau
|
||||
*/
|
||||
function extractYouTubeId(url) {
|
||||
if (!url || typeof url !== 'string') return null;
|
||||
// Hỗ trợ: watch?v=, embed/, youtu.be/, shorts/
|
||||
const regex = /(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : null;
|
||||
if (!url || typeof url !== "string") return null;
|
||||
// Hỗ trợ: watch?v=, embed/, youtu.be/, shorts/
|
||||
const regex =
|
||||
/(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,214 +23,268 @@ function extractYouTubeId(url) {
|
||||
* Loại bỏ duplicate video (vừa embed vừa text link) và paragraph rỗng
|
||||
*/
|
||||
function sanitizeContentBlocks(blocks) {
|
||||
if (!blocks || !Array.isArray(blocks)) return [];
|
||||
if (!blocks || !Array.isArray(blocks)) return [];
|
||||
|
||||
const seenVideoIds = new Set();
|
||||
|
||||
// Bước 1: Duyệt qua để chuẩn hóa block embed và lấy danh sách ID video
|
||||
const processedBlocks = blocks.map(block => {
|
||||
if (block.type === 'embed') {
|
||||
const url = block.data.source || block.data.embed || '';
|
||||
const videoId = extractYouTubeId(url);
|
||||
if (videoId) {
|
||||
seenVideoIds.add(videoId);
|
||||
// Cập nhật lại data chuẩn cho Editor.js
|
||||
block.data.embed = `https://www.youtube.com/embed/${videoId}`;
|
||||
block.data.source = url;
|
||||
block.data.videoId = videoId;
|
||||
block.data.service = 'youtube';
|
||||
}
|
||||
}
|
||||
return block;
|
||||
});
|
||||
const seenVideoIds = new Set();
|
||||
|
||||
// Bước 2: Lọc bỏ paragraph rác
|
||||
return processedBlocks.filter(block => {
|
||||
if (block.type === 'paragraph') {
|
||||
const text = (block.data?.text || '').trim();
|
||||
|
||||
// Xóa paragraph rỗng
|
||||
if (text === '' || text === '<br>' || text === ' ') return false;
|
||||
// Bước 1: Duyệt qua để chuẩn hóa block embed và lấy danh sách ID video
|
||||
const processedBlocks = blocks.map((block) => {
|
||||
if (block.type === "embed") {
|
||||
const url = block.data.source || block.data.embed || "";
|
||||
const videoId = extractYouTubeId(url);
|
||||
if (videoId) {
|
||||
seenVideoIds.add(videoId);
|
||||
// Cập nhật lại data chuẩn cho Editor.js
|
||||
block.data.embed = `https://www.youtube.com/embed/${videoId}`;
|
||||
block.data.source = url;
|
||||
block.data.videoId = videoId;
|
||||
block.data.service = "youtube";
|
||||
}
|
||||
}
|
||||
return block;
|
||||
});
|
||||
|
||||
// Xóa paragraph nếu nó chỉ chứa 1 link YouTube đã được embed ở trên
|
||||
const videoIdInText = extractYouTubeId(text);
|
||||
if (videoIdInText && seenVideoIds.has(videoIdInText)) {
|
||||
console.log(`[Sanitizer] Removed duplicate text link for video: ${videoIdInText}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Bước 2: Lọc bỏ paragraph rác
|
||||
return processedBlocks.filter((block) => {
|
||||
if (block.type === "paragraph") {
|
||||
const text = (block.data?.text || "").trim();
|
||||
|
||||
// Xóa paragraph rỗng
|
||||
if (text === "" || text === "<br>" || text === " ") return false;
|
||||
|
||||
// Xóa paragraph nếu nó chỉ chứa 1 link YouTube đã được embed ở trên
|
||||
const videoIdInText = extractYouTubeId(text);
|
||||
if (videoIdInText && seenVideoIds.has(videoIdInText)) {
|
||||
console.log(
|
||||
`[Sanitizer] Removed duplicate text link for video: ${videoIdInText}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// GET: Show travel editor
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const travel = await Travel.findOne();
|
||||
try {
|
||||
const travel = await Travel.findOne();
|
||||
|
||||
if (!travel) {
|
||||
return res.render("admin/travel/index", {
|
||||
title: "Travel Management",
|
||||
data: {
|
||||
page: { title: "Travel Information", description: "", metadata: { title: "", description: "" } },
|
||||
hero: { title: "Travel Information", backgroundImage: "" },
|
||||
content: { blocks: [] },
|
||||
enableScrollspy: false,
|
||||
},
|
||||
message: "No travel data found. Please run migration first.",
|
||||
});
|
||||
}
|
||||
|
||||
res.render("admin/travel/index", {
|
||||
title: "Travel Management",
|
||||
data: travel,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading travel page:", error);
|
||||
res.status(500).send("Error loading travel page");
|
||||
if (!travel) {
|
||||
return res.render("admin/travel/index", {
|
||||
title: "Travel Management",
|
||||
data: {
|
||||
page: {
|
||||
title: "Travel Information",
|
||||
description: "",
|
||||
metadata: { title: "", description: "" },
|
||||
},
|
||||
hero: { title: "Travel Information", backgroundImage: "" },
|
||||
content: { blocks: [] },
|
||||
enableScrollspy: false,
|
||||
},
|
||||
message: "No travel data found. Please run migration first.",
|
||||
});
|
||||
}
|
||||
|
||||
res.render("admin/travel/index", {
|
||||
title: "Travel Management",
|
||||
data: travel,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading travel page:", error);
|
||||
res.status(500).send("Error loading travel page");
|
||||
}
|
||||
};
|
||||
|
||||
// POST: Update travel information
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { page, hero, content, enableScrollspy } = req.body;
|
||||
const updateData = {};
|
||||
try {
|
||||
const { page, hero, content, enableScrollspy } = req.body;
|
||||
|
||||
if (page) updateData.page = JSON.parse(page);
|
||||
if (hero) updateData.hero = JSON.parse(hero);
|
||||
|
||||
if (content) {
|
||||
let contentObj = JSON.parse(content);
|
||||
// Áp dụng bộ lọc dọn dẹp nội dung
|
||||
contentObj.blocks = sanitizeContentBlocks(contentObj.blocks);
|
||||
updateData.content = contentObj;
|
||||
}
|
||||
// Get current data for before state
|
||||
const currentTravel = await Travel.findOne();
|
||||
|
||||
if (enableScrollspy !== undefined) {
|
||||
updateData.enableScrollspy = (enableScrollspy === "true" || enableScrollspy === true);
|
||||
}
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = currentTravel
|
||||
? JSON.parse(
|
||||
JSON.stringify(
|
||||
currentTravel.toObject ? currentTravel.toObject() : currentTravel,
|
||||
),
|
||||
)
|
||||
: {};
|
||||
|
||||
await Travel.findOneAndUpdate({}, updateData, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
});
|
||||
const updateData = {};
|
||||
|
||||
req.flash("success", "Travel information updated and sanitized successfully");
|
||||
res.redirect("/admin/travel");
|
||||
} catch (error) {
|
||||
console.error("Error updating travel:", error);
|
||||
req.flash("error", "Error updating travel information");
|
||||
res.redirect("/admin/travel");
|
||||
if (page) updateData.page = JSON.parse(page);
|
||||
if (hero) updateData.hero = JSON.parse(hero);
|
||||
|
||||
if (content) {
|
||||
let contentObj = JSON.parse(content);
|
||||
// Áp dụng bộ lọc dọn dẹp nội dung
|
||||
contentObj.blocks = sanitizeContentBlocks(contentObj.blocks);
|
||||
updateData.content = contentObj;
|
||||
}
|
||||
|
||||
if (enableScrollspy !== undefined) {
|
||||
updateData.enableScrollspy =
|
||||
enableScrollspy === "true" || enableScrollspy === true;
|
||||
}
|
||||
|
||||
const updatedTravel = await Travel.findOneAndUpdate({}, updateData, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
});
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(
|
||||
JSON.stringify(
|
||||
updatedTravel.toObject ? updatedTravel.toObject() : updatedTravel,
|
||||
),
|
||||
);
|
||||
|
||||
// ✅ AUDIT LOGGING - Travel Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Travel",
|
||||
documentId: updatedTravel._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_TRAVEL,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash(
|
||||
"success",
|
||||
"Travel information updated and sanitized successfully",
|
||||
);
|
||||
res.redirect("/admin/travel");
|
||||
} catch (error) {
|
||||
console.error("Error updating travel:", error);
|
||||
req.flash("error", "Error updating travel information");
|
||||
res.redirect("/admin/travel");
|
||||
}
|
||||
};
|
||||
|
||||
// GET: Travel data API (Sử dụng cho Frontend/Public)
|
||||
exports.api = exports.getTravelData = async (req, res) => {
|
||||
try {
|
||||
const travel = await Travel.findOne();
|
||||
if (!travel) {
|
||||
return res.status(404).json({ error: "Travel data not found" });
|
||||
}
|
||||
|
||||
const travelObj = travel.toObject();
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processed = addBaseUrlToImages(travelObj, baseUrl);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processed.hero,
|
||||
page: processed.page,
|
||||
content: processed.content,
|
||||
enableScrollspy: processed.enableScrollspy
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching travel data:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
try {
|
||||
const travel = await Travel.findOne();
|
||||
if (!travel) {
|
||||
return res.status(404).json({ error: "Travel data not found" });
|
||||
}
|
||||
|
||||
const travelObj = travel.toObject();
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processed = addBaseUrlToImages(travelObj, baseUrl);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: processed.hero,
|
||||
page: processed.page,
|
||||
content: processed.content,
|
||||
enableScrollspy: processed.enableScrollspy,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching travel data:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
// POST: Preview travel
|
||||
exports.preview = async (req, res) => {
|
||||
try {
|
||||
const { content, pageTitle, heroTitle, heroBackgroundImage, pageYear } = req.body;
|
||||
try {
|
||||
const { content, pageTitle, heroTitle, heroBackgroundImage, pageYear } =
|
||||
req.body;
|
||||
|
||||
// Preview cũng cần được sanitize để hiển thị đúng thực tế khi lưu
|
||||
let contentObj = JSON.parse(content);
|
||||
contentObj.blocks = sanitizeContentBlocks(contentObj.blocks);
|
||||
// Preview cũng cần được sanitize để hiển thị đúng thực tế khi lưu
|
||||
let contentObj = JSON.parse(content);
|
||||
contentObj.blocks = sanitizeContentBlocks(contentObj.blocks);
|
||||
|
||||
const previewData = {
|
||||
page: {
|
||||
title: pageTitle || "Travel Information",
|
||||
year: pageYear || ""
|
||||
},
|
||||
hero: {
|
||||
title: heroTitle || "Travel Information",
|
||||
backgroundImage: heroBackgroundImage || "",
|
||||
},
|
||||
content: contentObj,
|
||||
enableScrollspy: false,
|
||||
};
|
||||
const previewData = {
|
||||
page: {
|
||||
title: pageTitle || "Travel Information",
|
||||
year: pageYear || "",
|
||||
},
|
||||
hero: {
|
||||
title: heroTitle || "Travel Information",
|
||||
backgroundImage: heroBackgroundImage || "",
|
||||
},
|
||||
content: contentObj,
|
||||
enableScrollspy: false,
|
||||
};
|
||||
|
||||
res.render("page/travel", {
|
||||
title: "Travel Preview",
|
||||
data: previewData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating preview:", error);
|
||||
res.status(500).send("Error generating preview");
|
||||
}
|
||||
res.render("page/travel", {
|
||||
title: "Travel Preview",
|
||||
data: previewData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating preview:", error);
|
||||
res.status(500).send("Error generating preview");
|
||||
}
|
||||
};
|
||||
|
||||
// GET: Seed/Import from JSON
|
||||
exports.seed = async (req, res) => {
|
||||
try {
|
||||
const jsonPath = path.join(__dirname, "../data/travel.json");
|
||||
const jsonData = await fs.readFile(jsonPath, "utf-8");
|
||||
const jsonTravelData = JSON.parse(jsonData);
|
||||
try {
|
||||
const jsonPath = path.join(__dirname, "../data/travel.json");
|
||||
const jsonData = await fs.readFile(jsonPath, "utf-8");
|
||||
const jsonTravelData = JSON.parse(jsonData);
|
||||
|
||||
let contentBlocks = [];
|
||||
let contentBlocks = [];
|
||||
|
||||
// Trường hợp JSON đã có định dạng bài viết (blog format)
|
||||
if (Array.isArray(jsonTravelData.posts) && jsonTravelData.posts.length > 0) {
|
||||
const firstPost = jsonTravelData.posts[0];
|
||||
contentBlocks = (firstPost.content && firstPost.content.blocks) ? firstPost.content.blocks : [];
|
||||
}
|
||||
// Trường hợp format cũ (legacy)
|
||||
else {
|
||||
// ... (Logic chuyển đổi legacy format giữ nguyên nhưng bọc qua sanitize)
|
||||
// Ví dụ: push các header, paragraph từ locations vào contentBlocks
|
||||
}
|
||||
|
||||
// Luôn làm sạch dữ liệu trước khi seed vào DB
|
||||
const cleanedBlocks = sanitizeContentBlocks(contentBlocks);
|
||||
|
||||
const travelData = {
|
||||
page: {
|
||||
title: jsonTravelData.page?.title || "Go and Grow Camp Travel Information",
|
||||
year: jsonTravelData.page?.year || "",
|
||||
metadata: {
|
||||
title: "Travel Guide - Go and Grow Camp",
|
||||
description: "Everything you need to know about traveling to our camps",
|
||||
},
|
||||
},
|
||||
hero: {
|
||||
title: jsonTravelData.hero?.title || "Travel Information",
|
||||
backgroundImage: jsonTravelData.hero?.backgroundImage || "",
|
||||
},
|
||||
content: { blocks: cleanedBlocks },
|
||||
enableScrollspy: true,
|
||||
};
|
||||
|
||||
await Travel.findOneAndUpdate({}, travelData, { upsert: true, new: true });
|
||||
|
||||
req.flash("success", "Travel data seeded and sanitized successfully");
|
||||
res.redirect("/admin/travel");
|
||||
} catch (error) {
|
||||
console.error("Error seeding travel data:", error);
|
||||
req.flash("error", "Failed to seed travel data");
|
||||
res.redirect("/admin/travel");
|
||||
// Trường hợp JSON đã có định dạng bài viết (blog format)
|
||||
if (
|
||||
Array.isArray(jsonTravelData.posts) &&
|
||||
jsonTravelData.posts.length > 0
|
||||
) {
|
||||
const firstPost = jsonTravelData.posts[0];
|
||||
contentBlocks =
|
||||
firstPost.content && firstPost.content.blocks
|
||||
? firstPost.content.blocks
|
||||
: [];
|
||||
}
|
||||
};
|
||||
// Trường hợp format cũ (legacy)
|
||||
else {
|
||||
// ... (Logic chuyển đổi legacy format giữ nguyên nhưng bọc qua sanitize)
|
||||
// Ví dụ: push các header, paragraph từ locations vào contentBlocks
|
||||
}
|
||||
|
||||
// Luôn làm sạch dữ liệu trước khi seed vào DB
|
||||
const cleanedBlocks = sanitizeContentBlocks(contentBlocks);
|
||||
|
||||
const travelData = {
|
||||
page: {
|
||||
title:
|
||||
jsonTravelData.page?.title || "Go and Grow Camp Travel Information",
|
||||
year: jsonTravelData.page?.year || "",
|
||||
metadata: {
|
||||
title: "Travel Guide - Go and Grow Camp",
|
||||
description:
|
||||
"Everything you need to know about traveling to our camps",
|
||||
},
|
||||
},
|
||||
hero: {
|
||||
title: jsonTravelData.hero?.title || "Travel Information",
|
||||
backgroundImage: jsonTravelData.hero?.backgroundImage || "",
|
||||
},
|
||||
content: { blocks: cleanedBlocks },
|
||||
enableScrollspy: true,
|
||||
};
|
||||
|
||||
await Travel.findOneAndUpdate({}, travelData, { upsert: true, new: true });
|
||||
|
||||
req.flash("success", "Travel data seeded and sanitized successfully");
|
||||
res.redirect("/admin/travel");
|
||||
} catch (error) {
|
||||
console.error("Error seeding travel data:", error);
|
||||
req.flash("error", "Failed to seed travel data");
|
||||
res.redirect("/admin/travel");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,84 +1,119 @@
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Home = require("../models/home");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
|
||||
// Get videoGallery data from Home model
|
||||
const getVideoGalleryData = async () => {
|
||||
const home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
if (!home || !home.videoGallery) {
|
||||
return null;
|
||||
}
|
||||
return home.videoGallery.toObject ? home.videoGallery.toObject() : home.videoGallery;
|
||||
const home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
if (!home || !home.videoGallery) {
|
||||
return null;
|
||||
}
|
||||
return home.videoGallery.toObject
|
||||
? home.videoGallery.toObject()
|
||||
: home.videoGallery;
|
||||
};
|
||||
|
||||
// API to get videoGallery data
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const videoGallery = await getVideoGalleryData();
|
||||
if (!videoGallery) {
|
||||
return res.status(404).json({ error: "Video Gallery data not found" });
|
||||
}
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(videoGallery, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("API Error:", err);
|
||||
res.status(500).json({ error: "Error loading video gallery data" });
|
||||
try {
|
||||
const videoGallery = await getVideoGalleryData();
|
||||
if (!videoGallery) {
|
||||
return res.status(404).json({ error: "Video Gallery data not found" });
|
||||
}
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(videoGallery, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error("API Error:", err);
|
||||
res.status(500).json({ error: "Error loading video gallery data" });
|
||||
}
|
||||
};
|
||||
|
||||
// Render admin view
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = (await getVideoGalleryData()) || {
|
||||
heading: "",
|
||||
videoUrl: "",
|
||||
thumbnail: "",
|
||||
};
|
||||
try {
|
||||
const data = (await getVideoGalleryData()) || {
|
||||
heading: "",
|
||||
videoUrl: "",
|
||||
thumbnail: "",
|
||||
};
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
|
||||
res.render("admin/home/videoGallery/index", {
|
||||
title: "Video Gallery Management",
|
||||
layout: "layouts/main",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in videoGallery index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
res.render("admin/home/videoGallery/index", {
|
||||
title: "Video Gallery Management",
|
||||
layout: "layouts/main",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in videoGallery index:", error);
|
||||
req.flash("error_msg", "An error occurred while loading the page");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Cập nhật dữ liệu videoGallery
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { heading, videoUrl, thumbnail } = req.body;
|
||||
try {
|
||||
const { heading, videoUrl, thumbnail } = req.body;
|
||||
|
||||
// Tìm hoặc tạo Home document
|
||||
let home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
// Tìm hoặc tạo Home document
|
||||
let home = await Home.findOne().sort({ updatedAt: -1 });
|
||||
|
||||
if (!home) {
|
||||
home = new Home({});
|
||||
}
|
||||
|
||||
// Cập nhật chỉ phần videoGallery
|
||||
home.videoGallery = {
|
||||
heading: heading || "",
|
||||
videoUrl: videoUrl || "",
|
||||
thumbnail: thumbnail || "",
|
||||
};
|
||||
|
||||
await home.save();
|
||||
|
||||
req.flash("success_msg", "Video Gallery updated successfully");
|
||||
res.redirect("/admin/home/video-gallery");
|
||||
} catch (err) {
|
||||
console.error("Error updating video gallery:", err);
|
||||
req.flash("error_msg", err.message || "Error updating video gallery");
|
||||
res.redirect("/admin/home/video-gallery");
|
||||
if (!home) {
|
||||
home = new Home({});
|
||||
}
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = home.videoGallery
|
||||
? JSON.parse(
|
||||
JSON.stringify(
|
||||
home.videoGallery.toObject
|
||||
? home.videoGallery.toObject()
|
||||
: home.videoGallery,
|
||||
),
|
||||
)
|
||||
: {};
|
||||
|
||||
const updatedVideoGalleryData = {
|
||||
heading: heading || "",
|
||||
videoUrl: videoUrl || "",
|
||||
thumbnail: thumbnail || "",
|
||||
};
|
||||
|
||||
// Cập nhật chỉ phần videoGallery
|
||||
home.videoGallery = updatedVideoGalleryData;
|
||||
|
||||
await home.save();
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(updatedVideoGalleryData));
|
||||
|
||||
// ✅ AUDIT LOGGING - Video Gallery Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Home",
|
||||
documentId: home._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_VIDEO_GALLERY,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Video Gallery updated successfully");
|
||||
res.redirect("/admin/home/video-gallery");
|
||||
} catch (err) {
|
||||
console.error("Error updating video gallery:", err);
|
||||
req.flash("error_msg", err.message || "Error updating video gallery");
|
||||
res.redirect("/admin/home/video-gallery");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,6 +45,9 @@ const addBaseUrlToImages = (data, baseUrl) => {
|
||||
};
|
||||
const Visa = require("../models/visa");
|
||||
const slugify = require("slugify");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
const createSlug = (text) => {
|
||||
return slugify(text, {
|
||||
lower: true,
|
||||
@@ -184,6 +187,15 @@ exports.update = async (req, res) => {
|
||||
// Get current data
|
||||
const currentData = await getVisaData();
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = currentData
|
||||
? JSON.parse(
|
||||
JSON.stringify(
|
||||
currentData.toObject ? currentData.toObject() : currentData,
|
||||
),
|
||||
)
|
||||
: {};
|
||||
|
||||
// Create updated data object
|
||||
const updatedData = {
|
||||
...(currentData.toObject ? currentData.toObject() : currentData),
|
||||
@@ -200,23 +212,37 @@ exports.update = async (req, res) => {
|
||||
updatedData.hero.title = req.body.heroTitle;
|
||||
}
|
||||
|
||||
// Check if there are changes
|
||||
const hasChanges =
|
||||
JSON.stringify(updatedData) !== JSON.stringify(currentData);
|
||||
|
||||
if (!hasChanges) {
|
||||
req.flash("info_msg", "No changes were made");
|
||||
return req.session.save(() => res.redirect("/admin/visa"));
|
||||
}
|
||||
|
||||
// Update or create document
|
||||
try {
|
||||
let savedData;
|
||||
if (currentData._id) {
|
||||
await Visa.findByIdAndUpdate(currentData._id, updatedData, {
|
||||
savedData = await Visa.findByIdAndUpdate(currentData._id, updatedData, {
|
||||
new: true,
|
||||
});
|
||||
} else {
|
||||
await Visa.create(updatedData);
|
||||
savedData = await Visa.create(updatedData);
|
||||
}
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(JSON.stringify(savedData.toObject()));
|
||||
|
||||
// ✅ AUDIT LOGGING - Visa Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Visa",
|
||||
documentId: savedData._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_VISA,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
console.log(
|
||||
`✅ Audit log created for Visa update: ${changes.length} changes`,
|
||||
);
|
||||
} else {
|
||||
console.log("ℹ️ No changes detected for Visa update");
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Visa data updated successfully");
|
||||
@@ -243,6 +269,11 @@ exports.addCountry = async (req, res) => {
|
||||
visaData = getDefaultVisaData();
|
||||
}
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = JSON.parse(
|
||||
JSON.stringify(visaData.toObject ? visaData.toObject() : visaData),
|
||||
);
|
||||
|
||||
// Validate required fields
|
||||
if (!req.body.name) {
|
||||
return res.status(400).json({ error: "Name is required" });
|
||||
@@ -305,6 +336,28 @@ exports.addCountry = async (req, res) => {
|
||||
savedData = await Visa.create(updatedData);
|
||||
}
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(
|
||||
JSON.stringify(savedData.toObject ? savedData.toObject() : savedData),
|
||||
);
|
||||
|
||||
// ✅ AUDIT LOGGING - Visa Country Added
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Visa",
|
||||
documentId: savedData._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_VISA,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
console.log(
|
||||
`✅ Audit log created for Visa country addition: ${changes.length} changes`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✅ Country "${newCountry.name}" added successfully`);
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -330,6 +383,11 @@ exports.updateCountry = async (req, res) => {
|
||||
.json({ error: "Cấu trúc dữ liệu Visa không hợp lệ" });
|
||||
}
|
||||
|
||||
// ✅ Capture BEFORE state
|
||||
const beforeData = JSON.parse(
|
||||
JSON.stringify(visaData.toObject ? visaData.toObject() : visaData),
|
||||
);
|
||||
|
||||
// 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác)
|
||||
const countryIndex = visaData.hero.summaryList.findIndex(
|
||||
(c) => c.id === parseInt(id),
|
||||
@@ -390,10 +448,33 @@ exports.updateCountry = async (req, res) => {
|
||||
visaData.markModified("hero.summaryList");
|
||||
}
|
||||
|
||||
let savedData;
|
||||
if (visaData._id) {
|
||||
await visaData.save(); // Sử dụng save() trực tiếp nếu visaData là Mongoose Document
|
||||
savedData = await visaData.save(); // Sử dụng save() trực tiếp nếu visaData là Mongoose Document
|
||||
} else {
|
||||
await Visa.create(visaData);
|
||||
savedData = await Visa.create(visaData);
|
||||
}
|
||||
|
||||
// ✅ Capture AFTER state
|
||||
const afterData = JSON.parse(
|
||||
JSON.stringify(savedData.toObject ? savedData.toObject() : savedData),
|
||||
);
|
||||
|
||||
// ✅ AUDIT LOGGING - Visa Country Updated
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "Visa",
|
||||
documentId: savedData._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_VISA,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
console.log(
|
||||
`✅ Audit log created for Visa country update: ${changes.length} changes`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user