Files
uldp-degree-mangement-system/controllers/travelController.js
2026-02-10 16:42:35 +07:00

291 lines
8.7 KiB
JavaScript

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;
}
/**
* Hàm Helper: Làm sạch danh sách blocks của Editor.js
* 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 [];
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;
});
// 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();
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;
// Get current data for before state
const currentTravel = await Travel.findOne();
// ✅ Capture BEFORE state
const beforeData = currentTravel
? JSON.parse(
JSON.stringify(
currentTravel.toObject ? currentTravel.toObject() : currentTravel,
),
)
: {};
const updateData = {};
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" });
}
};
// POST: Preview travel
exports.preview = async (req, res) => {
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);
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");
}
};
// 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);
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");
}
};