forked from UKSOURCE/cms.hailearning.edu.vn
feat: implement comprehensive audit logging system
This commit is contained in:
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user