feat: implement comprehensive audit logging system

This commit is contained in:
nguyenvanbao
2026-02-10 16:42:35 +07:00
parent d440a04618
commit 970fcbac7d
28 changed files with 4783 additions and 2221 deletions

View File

@@ -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

View File

@@ -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",
});
}
};

View 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

View File

@@ -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) {

View File

@@ -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" });

View File

@@ -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)

View File

@@ -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,
});
}
};

View File

@@ -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" });
}
};

View File

@@ -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",
});
}
};

View File

@@ -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",
});
}
};

View File

@@ -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");
}
};

View File

@@ -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`);

View File

@@ -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",
});
}
};
};

View File

@@ -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");
}
};

View File

@@ -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 === '&nbsp;') 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 === "&nbsp;") 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");
}
};

View File

@@ -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");
}
};

View File

@@ -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(