Thay đổi ARCHITEC.md cập nhật các thông tin để chuẩn bị refactor lại dự án
This commit is contained in:
@@ -51,11 +51,18 @@ router.post('/create', protect, async (req, res) => {
|
||||
});
|
||||
await hotspot.save();
|
||||
|
||||
// Logic tạo liên kết quay lại tự động nếu có scene đích
|
||||
// [BẢO MẬT] Logic tạo liên kết quay lại tự động nếu có scene đích
|
||||
if (target_scene_id) {
|
||||
const targetScene = await Scene.findById(target_scene_id);
|
||||
if (targetScene) {
|
||||
const reverseYaw = calculateReverseYaw(coordinates.yaw);
|
||||
// [TASK 3] BẢO VỆ tourId & CHẶN VANDALISM:
|
||||
// Chỉ tự động tạo link quay lại (tức là ghi dữ liệu vào cảnh đích)
|
||||
// nếu người dùng hiện tại cũng là chủ sở hữu của cảnh đích đó.
|
||||
// Điều này đảm bảo:
|
||||
// 1. Không thay đổi cấu trúc tour của người khác khi tạo liên kết chéo (Cross-link).
|
||||
// 2. Tuyệt đối không can thiệp vào trường 'tourId' của targetScene.
|
||||
if (targetScene.createdBy.toString() === req.user._id.toString()) {
|
||||
const reverseYaw = calculateReverseYaw(coordinates.yaw);
|
||||
const reverseHotspot = new Hotspot({
|
||||
parent_scene_id: target_scene_id,
|
||||
target_scene_id: parent_scene_id,
|
||||
@@ -64,6 +71,7 @@ router.post('/create', protect, async (req, res) => {
|
||||
is_auto_return: true
|
||||
});
|
||||
await reverseHotspot.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,16 +34,43 @@ const uploadSinglePanorama = (req, res, next) => {
|
||||
// @route POST /api/scenes
|
||||
router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) => {
|
||||
try {
|
||||
const { title, lat, lng, privacy, sharedWithUsers, tourId } = req.body;
|
||||
const { title, lat, lng, privacy, sharedWithUsers, sharedEmails, shareExpireDays, tourId } = req.body;
|
||||
if (!req.file) return res.status(400).json({ message: 'Please upload a panorama image' });
|
||||
|
||||
// [BẢO MẬT] Làm sạch tourId từ client gửi lên
|
||||
const cleanedTourId = (tourId && tourId !== 'null' && tourId !== '') ? tourId : undefined;
|
||||
// [BẢO MẬT] Xác định quan hệ: Nếu có tourId thì là "Con đẻ", nếu không là "Gốc"
|
||||
const cleanedTourId = (tourId && tourId !== 'null' && tourId !== 'undefined' && tourId !== '') ? tourId : undefined;
|
||||
|
||||
let finalPrivacy = privacy || 'private';
|
||||
let finalSharedWith = [];
|
||||
let finalSharedEmails = [];
|
||||
let finalShareToken = undefined;
|
||||
let finalExpires = undefined;
|
||||
let assignedTourId = cleanedTourId; // Biến tạm để lưu tourId cuối cùng được gán
|
||||
|
||||
try { if (sharedWithUsers) finalSharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
||||
|
||||
// [BẢO MẬT] Xác thực tourId nếu được cung cấp
|
||||
if (cleanedTourId) {
|
||||
const tourExists = await Scene.exists({ _id: cleanedTourId });
|
||||
if (!tourExists) return res.status(400).json({ message: 'Tour gốc không tồn tại hoặc đã bị xóa.' });
|
||||
const rootScene = await Scene.findById(cleanedTourId);
|
||||
if (!rootScene) return res.status(400).json({ message: 'Tour gốc không tồn tại hoặc đã bị xóa.' });
|
||||
|
||||
// [SECURITY] Chỉ cho phép gán tourId nếu người dùng hiện tại là chủ sở hữu của cảnh gốc đó
|
||||
if (rootScene.createdBy.toString() !== req.user._id.toString()) {
|
||||
// Nếu không phải chủ sở hữu, cảnh mới này sẽ tự làm gốc của chính nó
|
||||
assignedTourId = undefined;
|
||||
} else {
|
||||
// [ENFORCE INHERITANCE] Cảnh con bắt buộc kế thừa toàn bộ cấu hình từ cảnh gốc
|
||||
finalPrivacy = rootScene.privacy;
|
||||
finalSharedWith = rootScene.sharedWith;
|
||||
finalSharedEmails = rootScene.sharedEmails;
|
||||
finalShareToken = rootScene.shareToken;
|
||||
finalExpires = rootScene.shareTokenExpires;
|
||||
}
|
||||
} else {
|
||||
// Nếu là cảnh gốc mới, tạo token nếu chế độ là shared
|
||||
if (finalPrivacy === 'shared') {
|
||||
finalShareToken = crypto.randomBytes(24).toString('hex');
|
||||
}
|
||||
}
|
||||
|
||||
const latitude = Number(lat) || 0;
|
||||
@@ -60,21 +87,19 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
|
||||
});
|
||||
await asset.save();
|
||||
|
||||
let shareToken = privacy === 'shared' ? crypto.randomBytes(24).toString('hex') : undefined;
|
||||
let parsedSharedWith = [];
|
||||
try { if (sharedWithUsers) parsedSharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
||||
|
||||
const scene = new Scene({
|
||||
name: title,
|
||||
assetId: asset._id,
|
||||
scene_url: tempFilePath,
|
||||
gps: { lat: latitude, lng: longitude },
|
||||
createdBy: req.user._id,
|
||||
privacy: privacy || 'private',
|
||||
shareToken,
|
||||
sharedWith: parsedSharedWith,
|
||||
privacy: finalPrivacy,
|
||||
shareToken: finalShareToken,
|
||||
shareTokenExpires: finalExpires,
|
||||
sharedWith: finalSharedWith,
|
||||
sharedEmails: finalSharedEmails,
|
||||
status: 'processing',
|
||||
tourId: cleanedTourId
|
||||
tourId: assignedTourId
|
||||
});
|
||||
// Mặc định mỗi cảnh mới khi tạo ra là cảnh gốc của chính nó
|
||||
if (!scene.tourId) scene.tourId = scene._id;
|
||||
@@ -219,15 +244,23 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
await fs.promises.unlink(req.file.path).catch(() => {});
|
||||
}
|
||||
|
||||
// [FIX] Đảm bảo root scene luôn có tourId để logic lan truyền hoạt động (cho cả dữ liệu cũ)
|
||||
if (!scene.tourId) scene.tourId = scene._id;
|
||||
// Đảm bảo tính nhất quán: Nếu không có tourId cha, scene này tự làm gốc
|
||||
if (!scene.tourId) scene.tourId = scene._id;
|
||||
await scene.save();
|
||||
|
||||
// [BẢO MẬT] Lan truyền Privacy xuống các cảnh con nếu đây là cảnh gốc của Tour.
|
||||
const isRoot = scene.tourId && scene.tourId.toString() === scene._id.toString();
|
||||
|
||||
if (isRoot) {
|
||||
await propagateScenePrivacy(scene._id, scene, req.user._id);
|
||||
// [TASK 2] Chuẩn hóa dữ liệu truyền vào helper
|
||||
// Chuyển đổi sharedWith thành mảng string ID thuần túy để tránh lỗi Mongoose
|
||||
await propagateScenePrivacy(scene._id, {
|
||||
privacy: scene.privacy,
|
||||
shareToken: scene.shareToken,
|
||||
shareTokenExpires: scene.shareTokenExpires,
|
||||
sharedWith: scene.sharedWith.map(id => id.toString ? id.toString() : id),
|
||||
sharedEmails: scene.sharedEmails
|
||||
}, req.user._id);
|
||||
}
|
||||
|
||||
res.json({ message: 'Cập nhật thành công và đã đồng bộ quyền riêng tư cho các cảnh liên quan.', scene });
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
const mongoose = require('mongoose');
|
||||
const connectDB = require('../config/db');
|
||||
const User = require('../models/User');
|
||||
const Scene = require('../models/Scene');
|
||||
|
||||
/**
|
||||
* Script rà soát tính toàn vẹn của tourId và quyền sở hữu.
|
||||
* Mục tiêu:
|
||||
* 1. Phát hiện các Scene không có tourId (Mồ côi).
|
||||
* 2. Phát hiện các Scene trỏ tourId vào một cảnh không tồn tại (Link hỏng).
|
||||
* 3. Phát hiện rủi ro Scenario 2: Scene của người A nhưng tourId lại trỏ vào Tour của người B.
|
||||
*/
|
||||
const auditTourIds = async () => {
|
||||
try {
|
||||
console.log('--- BẮT ĐẦU RÀ SOÁT TOUR ID ---');
|
||||
await connectDB();
|
||||
|
||||
// Lấy tất cả scene và populate thông tin người tạo
|
||||
const scenes = await Scene.find().populate('createdBy', 'username');
|
||||
console.log(`Đang kiểm tra ${scenes.length} bản ghi...\n`);
|
||||
|
||||
const report = {
|
||||
orphan: [], // Không có tourId
|
||||
brokenLink: [], // tourId trỏ vào hư vô
|
||||
mismatchOwner: [], // Chủ sở hữu không khớp (Rủi ro Scenario 2)
|
||||
validRoots: 0,
|
||||
validChildren: 0
|
||||
};
|
||||
|
||||
for (const scene of scenes) {
|
||||
const sId = scene._id.toString();
|
||||
|
||||
// 1. Kiểm tra tồn tại tourId
|
||||
// [ROBUST CHECK] Kiểm tra cả giá trị null, undefined và chuỗi rác
|
||||
const tourIdRaw = scene.tourId;
|
||||
if (!tourIdRaw || tourIdRaw === "" || tourIdRaw === "null" || tourIdRaw === "undefined") {
|
||||
report.orphan.push({
|
||||
id: sId,
|
||||
name: scene.name || scene.title,
|
||||
value: JSON.stringify(tourIdRaw) // In ra giá trị thực tế để debug
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const tId = scene.tourId.toString();
|
||||
|
||||
// Trường hợp là Root (Cảnh gốc của chính nó)
|
||||
if (sId === tId) {
|
||||
report.validRoots++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trường hợp là Child -> Kiểm tra quan hệ với cha
|
||||
const rootScene = await Scene.findById(scene.tourId).populate('createdBy', 'username');
|
||||
|
||||
if (!rootScene) {
|
||||
report.brokenLink.push({ id: sId, name: scene.name || scene.title, target: tId });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. KIỂM TRA QUAN TRỌNG: Đồng nhất chủ sở hữu (Scenario 2)
|
||||
// Nếu scene con có chủ sở hữu khác với scene gốc mà nó đang trỏ tourId vào,
|
||||
// nghĩa là quyền riêng tư của nó đang bị điều khiển bởi một người khác.
|
||||
const sceneOwner = scene.createdBy?._id?.toString() || scene.createdBy?.toString();
|
||||
const rootOwner = rootScene.createdBy?._id?.toString() || rootScene.createdBy?.toString();
|
||||
|
||||
if (sceneOwner !== rootOwner) {
|
||||
report.mismatchOwner.push({
|
||||
childId: sId,
|
||||
childName: scene.name || scene.title,
|
||||
childOwner: scene.createdBy?.username || 'N/A',
|
||||
parentId: tId,
|
||||
parentName: rootScene.name || rootScene.title,
|
||||
parentOwner: rootScene.createdBy?.username || 'N/A'
|
||||
});
|
||||
} else {
|
||||
report.validChildren++;
|
||||
}
|
||||
}
|
||||
|
||||
// --- XUẤT BÁO CÁO ---
|
||||
console.log('=== KẾT QUẢ RÀ SOÁT ===');
|
||||
console.log(`- Scene gốc hợp lệ: ${report.validRoots}`);
|
||||
console.log(`- Scene con hợp lệ: ${report.validChildren}`);
|
||||
console.log('-----------------------');
|
||||
|
||||
if (report.orphan.length > 0) {
|
||||
console.error(`[!] LỖI: ${report.orphan.length} Scene mồ côi (thiếu tourId):`);
|
||||
report.orphan.forEach(x => console.log(` - ID: ${x.id} | Tên: ${x.name}`));
|
||||
}
|
||||
|
||||
if (report.brokenLink.length > 0) {
|
||||
console.error(`[!] LỖI: ${report.brokenLink.length} Scene trỏ vào Tour không tồn tại:`);
|
||||
report.brokenLink.forEach(x => console.log(` - ID: ${x.id} | Tên: ${x.name} -> Target: ${x.target}`));
|
||||
}
|
||||
|
||||
if (report.mismatchOwner.length > 0) {
|
||||
console.warn(`[!] CẢNH BÁO: ${report.mismatchOwner.length} Scene bị "lây nhiễm" tourId (Nguy cơ Scenario 2):`);
|
||||
report.mismatchOwner.forEach(x => {
|
||||
console.log(` - Cảnh [${x.childName}] (ID: ${x.childId}) của user [${x.childOwner}]`);
|
||||
console.log(` đang bị điều khiển bởi Tour [${x.parentName}] của user [${x.parentOwner}]`);
|
||||
console.log(` => GIẢI PHÁP: Cần cập nhật tourId của cảnh này về chính nó.\n`);
|
||||
});
|
||||
}
|
||||
|
||||
if (report.orphan.length === 0 && report.brokenLink.length === 0 && report.mismatchOwner.length === 0) {
|
||||
console.log('[✓] Database sạch sẽ. Không phát hiện lỗi tourId hay xâm lấn quyền sở hữu.');
|
||||
}
|
||||
|
||||
console.log('\n--- HOÀN TẤT ---');
|
||||
mongoose.connection.close();
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Lỗi thực thi script:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
auditTourIds();
|
||||
@@ -0,0 +1,42 @@
|
||||
const mongoose = require('mongoose');
|
||||
const connectDB = require('../config/db');
|
||||
const Scene = require('../models/Scene');
|
||||
|
||||
/**
|
||||
* Script sửa lỗi các Scene mồ côi (không có tourId)
|
||||
* Logic: Nếu một Scene không có tourId, nó sẽ được gán tourId = _id (trở thành Root)
|
||||
*/
|
||||
const fixOrphans = async () => {
|
||||
try {
|
||||
await connectDB();
|
||||
|
||||
// Sử dụng logic quét rộng tương tự audit script để tìm tất cả các loại "rác" tourId
|
||||
const allScenes = await Scene.find({});
|
||||
const orphans = allScenes.filter(s =>
|
||||
!s.tourId ||
|
||||
s.tourId === "" ||
|
||||
s.tourId === "null" ||
|
||||
s.tourId === "undefined"
|
||||
);
|
||||
|
||||
console.log(`Tìm thấy ${orphans.length} scene mồ côi. Đang xử lý...`);
|
||||
|
||||
for (const scene of orphans) {
|
||||
// [FIX] Sử dụng updateOne trực tiếp trên collection để bypass Schema validation
|
||||
// Đảm bảo dữ liệu CHẮC CHẮN được ghi xuống Database
|
||||
await Scene.collection.updateOne(
|
||||
{ _id: scene._id },
|
||||
{ $set: { tourId: scene._id } }
|
||||
);
|
||||
console.log(`- [FIXED] Scene: ${scene.name || scene.title} (ID: ${scene._id}) -> Đã trở thành Tour Gốc`);
|
||||
}
|
||||
|
||||
console.log('\n--- HOÀN TẤT SỬA LỖI DỮ LIỆU ---');
|
||||
mongoose.connection.close();
|
||||
} catch (err) {
|
||||
console.error('Lỗi thực thi:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
fixOrphans();
|
||||
@@ -68,8 +68,10 @@ const migrateTourIds = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Bước 3: Xử lý các cảnh mồ côi hoặc vòng lặp kín (tự trỏ về chính mình làm gốc)
|
||||
const orphanScenes = await Scene.find({ tourId: { $exists: false } });
|
||||
// Bước 3: Xử lý các cảnh mồ côi, lỗi tourId null/rỗng hoặc vòng lặp kín
|
||||
const orphanScenes = await Scene.find({
|
||||
$or: [{ tourId: { $exists: false } }, { tourId: null }, { tourId: "" }]
|
||||
});
|
||||
let orphanCount = 0;
|
||||
for (const scene of orphanScenes) {
|
||||
await Scene.updateOne({ _id: scene._id }, { $set: { tourId: scene._id } });
|
||||
|
||||
@@ -16,51 +16,21 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
||||
const rootScene = await Scene.findById(rootSceneId);
|
||||
if (!rootScene) return { deletedCount: 0 };
|
||||
const tourId = rootScene.tourId ? rootScene.tourId.toString() : null;
|
||||
const tourIdStr = tourId || rootSceneId.toString();
|
||||
|
||||
// [Discovery BFS] Tìm kiếm các cảnh dựa trên liên kết thực tế để xử lý dữ liệu lỗi (Broken Root)
|
||||
let queue = [rootSceneId.toString()];
|
||||
let scenesToDelete = [rootSceneId.toString()];
|
||||
const visited = new Set(scenesToDelete);
|
||||
// [BIÊN GIỚI TOUR] Xác định danh sách cần xóa
|
||||
const isRoot = tourId && tourId === rootSceneId.toString();
|
||||
let scenesToDelete = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const parentId = queue.shift();
|
||||
const childHotspots = await Hotspot.find({
|
||||
parent_scene_id: parentId,
|
||||
is_auto_return: { $ne: true }
|
||||
}).populate('target_scene_id', 'tourId');
|
||||
|
||||
for (const hs of childHotspots) {
|
||||
if (hs.target_scene_id && typeof hs.target_scene_id === 'object') {
|
||||
const targetScene = hs.target_scene_id;
|
||||
const targetIdStr = targetScene._id.toString();
|
||||
const targetTourId = targetScene.tourId ? targetScene.tourId.toString() : null;
|
||||
|
||||
// [Biên giới Tour] Chỉ xóa nếu:
|
||||
// 1. Cùng tourId
|
||||
// 2. Hoặc là Broken Root (tourId tự trỏ về chính nó nhưng lại được liên kết ở đây)
|
||||
// 3. Hoặc là Orphan (không có tourId)
|
||||
const isSameTour = targetTourId === tourIdStr;
|
||||
const isBrokenRoot = targetTourId === targetIdStr;
|
||||
const isOrphan = !targetTourId;
|
||||
|
||||
if (!visited.has(targetIdStr) && (isSameTour || isBrokenRoot || isOrphan)) {
|
||||
visited.add(targetIdStr);
|
||||
scenesToDelete.push(targetIdStr);
|
||||
queue.push(targetIdStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRoot) {
|
||||
// Nếu xóa gốc: Xóa mọi thứ thuộc tourId này (Bao gồm con đẻ, loại trừ liên kết)
|
||||
const tourScenes = await Scene.find({ tourId: rootScene.tourId }).select('_id');
|
||||
scenesToDelete = tourScenes.map(s => s._id.toString());
|
||||
} else {
|
||||
// Nếu xóa cảnh con lẻ: Chỉ xóa đúng nó
|
||||
scenesToDelete = [rootSceneId.toString()];
|
||||
}
|
||||
|
||||
// 1. Dọn dẹp Hotspots (Cả link đi và link trỏ ĐẾN các scene sắp xóa)
|
||||
const hotspotCleanup = await Hotspot.deleteMany({
|
||||
$or: [
|
||||
{ parent_scene_id: { $in: scenesToDelete } },
|
||||
{ target_scene_id: { $in: scenesToDelete } }
|
||||
]
|
||||
});
|
||||
|
||||
// 1. Thu thập Asset ID
|
||||
// 2. Thu thập tất cả Asset ID liên quan
|
||||
const scenes = await Scene.find({ _id: { $in: scenesToDelete } });
|
||||
const assetIds = scenes.map(s => s.assetId).filter(id => id);
|
||||
@@ -71,20 +41,22 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
||||
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
|
||||
}));
|
||||
|
||||
// 3. Xóa bản ghi trong Database
|
||||
// 4. Dọn dẹp Hotspots (Cả link đi từ cảnh bị xóa và link từ các tour khác trỏ ĐẾN cảnh này)
|
||||
const hotspotCleanup = await Hotspot.deleteMany({
|
||||
$or: [
|
||||
{ parent_scene_id: { $in: scenesToDelete } },
|
||||
{ target_scene_id: { $in: scenesToDelete } }
|
||||
]
|
||||
});
|
||||
|
||||
// 5. Xóa bản ghi trong Database
|
||||
const assetCleanup = await Asset.deleteMany({ _id: { $in: assetIds } });
|
||||
const sceneCleanup = await Scene.deleteMany({ _id: { $in: scenesToDelete } });
|
||||
|
||||
// Chuẩn bị nội dung thông báo cho log
|
||||
const tourName = rootScene.name || rootScene.title || 'Chưa đặt tên';
|
||||
const childCount = scenesToDelete.length > 0 ? scenesToDelete.length - 1 : 0;
|
||||
|
||||
// Xác định xem đây là xóa cả Tour hay xóa lẻ
|
||||
const isRootAction = tourIdStr === rootSceneId.toString();
|
||||
|
||||
// Ghi log hoạt động xóa chi tiết để dễ dàng truy vết và kiểm tra tính toàn vẹn
|
||||
await logActivity('CASCADE_DELETE_SCENE', {
|
||||
message: isRootAction ? `Xóa trọn bộ Tour [${tourName}] và ${childCount} cảnh con` : `Xóa cảnh lẻ [${tourName}] khỏi Tour`,
|
||||
message: isRoot ? `Xóa trọn bộ Tour [${tourName}] và ${childCount} cảnh con` : `Xóa cảnh lẻ [${tourName}] khỏi Tour`,
|
||||
deletedScenesCount: scenesToDelete.length,
|
||||
cleanedHotspotsCount: hotspotCleanup.deletedCount
|
||||
}, performer ? performer.toString() : 'System');
|
||||
@@ -104,53 +76,22 @@ const propagateScenePrivacy = async (rootSceneId, privacyData, performer = 'Syst
|
||||
if (!rootScene) return;
|
||||
|
||||
const tourId = rootScene.tourId || rootScene._id;
|
||||
const tourIdStr = tourId.toString();
|
||||
|
||||
// 1. Tìm tất cả cảnh con cần cập nhật bằng BFS để đảm bảo tính "tự chữa lành" cho tourId
|
||||
let queue = [rootSceneId.toString()];
|
||||
let scenesToUpdate = [rootSceneId.toString()];
|
||||
const visited = new Set(scenesToUpdate);
|
||||
|
||||
while (queue.length > 0) {
|
||||
const parentId = queue.shift();
|
||||
const childHotspots = await Hotspot.find({
|
||||
parent_scene_id: parentId,
|
||||
is_auto_return: { $ne: true }
|
||||
}).populate('target_scene_id', 'tourId');
|
||||
|
||||
for (const hs of childHotspots) {
|
||||
if (hs.target_scene_id && typeof hs.target_scene_id === 'object') {
|
||||
const targetScene = hs.target_scene_id;
|
||||
const targetIdStr = targetScene._id.toString();
|
||||
const targetTourId = targetScene.tourId ? targetScene.tourId.toString() : null;
|
||||
|
||||
// Chấp nhận cập nhật nếu là cùng tour hoặc là broken root (tự kế thừa lại tourId đúng)
|
||||
const isBrokenRoot = targetTourId === targetIdStr;
|
||||
const isSameTour = targetTourId === tourIdStr;
|
||||
|
||||
if (!visited.has(targetIdStr) && (isSameTour || isBrokenRoot || !targetTourId)) {
|
||||
visited.add(targetIdStr);
|
||||
scenesToUpdate.push(targetIdStr);
|
||||
queue.push(targetIdStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData;
|
||||
|
||||
// 2. Chuẩn bị dữ liệu cập nhật (Luôn đồng bộ tourId để sửa lỗi dữ liệu)
|
||||
const updateFields = { privacy, tourId };
|
||||
// 2. Chuẩn bị dữ liệu cập nhật (Chỉ cập nhật Privacy, giữ nguyên tourId)
|
||||
const updateFields = { privacy };
|
||||
const unsets = {};
|
||||
if (privacy === 'shared') { if (shareToken) updateFields.shareToken = shareToken; else unsets.shareToken = 1; updateFields.shareTokenExpires = shareTokenExpires || undefined; updateFields.sharedWith = []; updateFields.sharedEmails = []; } else { unsets.shareToken = 1; unsets.shareTokenExpires = 1; if (privacy !== 'member') { updateFields.sharedWith = []; updateFields.sharedEmails = []; } }
|
||||
|
||||
const updateQuery = { $set: updateFields };
|
||||
if (Object.keys(unsets).length > 0) updateQuery.$unset = unsets;
|
||||
|
||||
// 3. Cập nhật dựa trên danh sách ID đã tìm được qua BFS
|
||||
await Scene.updateMany({ _id: { $in: scenesToUpdate } }, updateQuery);
|
||||
// [BẢO MẬT TUYỆT ĐỐI] Chỉ cập nhật cho các cảnh mang đúng tourId này (Con đẻ).
|
||||
// Các cảnh liên kết chéo mang tourId khác nên sẽ được bảo vệ an toàn.
|
||||
const result = await Scene.updateMany({ tourId: tourId }, updateQuery);
|
||||
|
||||
await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy }, performer ? performer.toString() : 'System');
|
||||
await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy, affectedCount: result.modifiedCount }, performer ? performer.toString() : 'System');
|
||||
};
|
||||
|
||||
module.exports = { deleteSceneCascade, propagateScenePrivacy };
|
||||
Reference in New Issue
Block a user