Fix lỗi set privacy chéo
This commit is contained in:
@@ -183,12 +183,12 @@ router.delete('/assets/:id', protect, async (req, res) => {
|
|||||||
|
|
||||||
const linkedScene = await Scene.findOne({ assetId: asset._id });
|
const linkedScene = await Scene.findOne({ assetId: asset._id });
|
||||||
if (linkedScene) {
|
if (linkedScene) {
|
||||||
await deleteSceneCascade(linkedScene._id, req.user.username);
|
await deleteSceneCascade(linkedScene._id, req.user._id);
|
||||||
} else {
|
} else {
|
||||||
// Nếu là asset mồ côi (không gắn scene)
|
// Nếu là asset mồ côi (không gắn scene)
|
||||||
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
|
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
|
||||||
await Asset.findByIdAndDelete(req.params.id);
|
await Asset.findByIdAndDelete(req.params.id);
|
||||||
await logActivity('ORPHAN_ASSET_DELETE', { assetId: req.params.id }, req.user.username);
|
await logActivity('ORPHAN_ASSET_DELETE', { assetId: req.params.id }, req.user._id.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ message: 'Đã xóa ảnh và dữ liệu liên quan thành công' });
|
res.json({ message: 'Đã xóa ảnh và dữ liệu liên quan thành công' });
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
|
|||||||
const { title, lat, lng, privacy, sharedWithUsers, tourId } = req.body;
|
const { title, lat, lng, privacy, sharedWithUsers, tourId } = req.body;
|
||||||
if (!req.file) return res.status(400).json({ message: 'Please upload a panorama image' });
|
if (!req.file) return res.status(400).json({ message: 'Please upload a panorama image' });
|
||||||
|
|
||||||
// Xử lý tourId từ FormData an toàn
|
// [BẢO MẬT] Làm sạch tourId từ client gửi lên
|
||||||
const cleanedTourId = (tourId && tourId !== 'null' && tourId !== '') ? tourId : undefined;
|
const cleanedTourId = (tourId && tourId !== 'null' && tourId !== '') ? tourId : undefined;
|
||||||
|
|
||||||
|
// [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 latitude = Number(lat) || 0;
|
const latitude = Number(lat) || 0;
|
||||||
const longitude = Number(lng) || 0;
|
const longitude = Number(lng) || 0;
|
||||||
const tempFilePath = req.file.path;
|
const tempFilePath = req.file.path;
|
||||||
@@ -164,6 +170,10 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
if (lat) scene.gps.lat = parseFloat(lat);
|
if (lat) scene.gps.lat = parseFloat(lat);
|
||||||
if (lng) scene.gps.lng = parseFloat(lng);
|
if (lng) scene.gps.lng = parseFloat(lng);
|
||||||
|
|
||||||
|
// [BẢO MẬT] Tuyệt đối không cho phép thay đổi tourId qua API cập nhật Metadata
|
||||||
|
// Một cảnh khi đã thuộc về một Tour thì không thể bị "chuyển hộ khẩu" sang Tour khác.
|
||||||
|
// (Trường tourId không có trong danh sách bóc tách req.body ở trên)
|
||||||
|
|
||||||
// [BẢO MẬT] Chỉ duy trì shareToken ở chế độ 'shared'.
|
// [BẢO MẬT] Chỉ duy trì shareToken ở chế độ 'shared'.
|
||||||
// Gán undefined để Mongoose xóa trường này khỏi DB khi save.
|
// Gán undefined để Mongoose xóa trường này khỏi DB khi save.
|
||||||
if (scene.privacy !== 'shared') {
|
if (scene.privacy !== 'shared') {
|
||||||
@@ -213,9 +223,11 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
if (!scene.tourId) scene.tourId = scene._id;
|
if (!scene.tourId) scene.tourId = scene._id;
|
||||||
await scene.save();
|
await scene.save();
|
||||||
|
|
||||||
// [CASCADING] Lan truyền Privacy xuống các cảnh con nếu đây là cảnh cha
|
// [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.
|
||||||
if (!isChild) {
|
const isRoot = scene.tourId && scene.tourId.toString() === scene._id.toString();
|
||||||
await propagateScenePrivacy(scene.tourId || scene._id, scene);
|
|
||||||
|
if (isRoot) {
|
||||||
|
await propagateScenePrivacy(scene._id, scene, 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 });
|
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 });
|
||||||
@@ -235,7 +247,7 @@ router.delete('/:id', protect, async (req, res) => {
|
|||||||
return res.status(403).json({ message: 'Forbidden' });
|
return res.status(403).json({ message: 'Forbidden' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { deletedCount } = await deleteSceneCascade(rootSceneId, req.user.username);
|
const { deletedCount } = await deleteSceneCascade(rootSceneId, req.user._id);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: deletedCount > 1
|
message: deletedCount > 1
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const connectDB = require('../config/db');
|
||||||
|
const Scene = require('../models/Scene');
|
||||||
|
const Hotspot = require('../models/Hotspot');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script migration chuẩn hóa trường tourId cho tất cả các Scene dựa trên liên kết Hotspot thực tế.
|
||||||
|
* Đảm bảo tính nhất quán cho các tính năng Privacy Cascading và Cascade Delete.
|
||||||
|
*/
|
||||||
|
const migrateTourIds = async () => {
|
||||||
|
try {
|
||||||
|
console.log('--- Bắt đầu quy trình chuẩn hóa tourId ---');
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
// Bước 1: Xóa bỏ các giá trị tourId rác (null, rỗng) để xử lý sạch
|
||||||
|
await Scene.updateMany(
|
||||||
|
{ tourId: { $in: [null, ""] } },
|
||||||
|
{ $unset: { tourId: 1 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bước 2: Tìm các cảnh gốc (Roots)
|
||||||
|
// Cảnh gốc là cảnh không có bất kỳ hotspot đi tới nào (không tính link quay lại - is_auto_return)
|
||||||
|
const targetSceneIds = await Hotspot.find({ is_auto_return: { $ne: true } }).distinct('target_scene_id');
|
||||||
|
const rootScenes = await Scene.find({ _id: { $nin: targetSceneIds } });
|
||||||
|
|
||||||
|
console.log(`- Tìm thấy ${rootScenes.length} cảnh gốc tiềm năng.`);
|
||||||
|
|
||||||
|
const processedScenes = new Set();
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
for (const root of rootScenes) {
|
||||||
|
const rootIdStr = root._id.toString();
|
||||||
|
if (processedScenes.has(rootIdStr)) continue;
|
||||||
|
|
||||||
|
console.log(`- Đang xử lý Tour: ${root.name || root.title || root._id}`);
|
||||||
|
|
||||||
|
// Cập nhật rootId cho chính nó
|
||||||
|
await Scene.updateOne({ _id: root._id }, { $set: { tourId: root._id } });
|
||||||
|
processedScenes.add(rootIdStr);
|
||||||
|
updatedCount++;
|
||||||
|
|
||||||
|
// Duyệt BFS để gán tourId cho toàn bộ cây tour
|
||||||
|
let queue = [root._id];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const parentId = queue.shift();
|
||||||
|
|
||||||
|
const hotspots = await Hotspot.find({
|
||||||
|
parent_scene_id: parentId,
|
||||||
|
is_auto_return: { $ne: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const hs of hotspots) {
|
||||||
|
if (hs.target_scene_id) {
|
||||||
|
const childIdStr = hs.target_scene_id.toString();
|
||||||
|
if (!processedScenes.has(childIdStr)) {
|
||||||
|
await Scene.updateOne(
|
||||||
|
{ _id: hs.target_scene_id },
|
||||||
|
{ $set: { tourId: root._id } }
|
||||||
|
);
|
||||||
|
processedScenes.add(childIdStr);
|
||||||
|
updatedCount++;
|
||||||
|
queue.push(hs.target_scene_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 } });
|
||||||
|
let orphanCount = 0;
|
||||||
|
for (const scene of orphanScenes) {
|
||||||
|
await Scene.updateOne({ _id: scene._id }, { $set: { tourId: scene._id } });
|
||||||
|
orphanCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`- Đã cập nhật ${updatedCount} cảnh theo luồng tour.`);
|
||||||
|
console.log(`- Đã xử lý ${orphanCount} cảnh mồ côi/vòng lặp tự trỏ về chính mình.`);
|
||||||
|
console.log('--- Hoàn tất migration tourId! ---');
|
||||||
|
|
||||||
|
mongoose.connection.close();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Lỗi Migration:', error.message);
|
||||||
|
if (mongoose.connection) mongoose.connection.close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateTourIds();
|
||||||
@@ -16,23 +16,15 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
|||||||
const rootScene = await Scene.findById(rootSceneId);
|
const rootScene = await Scene.findById(rootSceneId);
|
||||||
if (!rootScene) return { deletedCount: 0 };
|
if (!rootScene) return { deletedCount: 0 };
|
||||||
const tourId = rootScene.tourId ? rootScene.tourId.toString() : null;
|
const tourId = rootScene.tourId ? rootScene.tourId.toString() : null;
|
||||||
|
const tourIdStr = tourId || rootSceneId.toString();
|
||||||
|
|
||||||
// BƯỚC SỬA LỖI QUAN TRỌNG: Xóa toàn bộ "điều hướng" (Hotspots) trỏ ĐẾN scene này.
|
// [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)
|
||||||
// Đây chính là lệnh "xóa điều hướng" để cô lập scene con khỏi scene cha ngay lập tức.
|
|
||||||
// Nó đảm bảo các scene cha không còn bất kỳ liên kết nào dẫn đến luồng xóa này.
|
|
||||||
await Hotspot.deleteMany({ target_scene_id: rootSceneId });
|
|
||||||
|
|
||||||
// 1. Thuật toán BFS để tìm tất cả các scene con (Xóa theo chiều xuôi)
|
|
||||||
let queue = [rootSceneId.toString()];
|
let queue = [rootSceneId.toString()];
|
||||||
let scenesToDelete = [rootSceneId.toString()];
|
let scenesToDelete = [rootSceneId.toString()];
|
||||||
const visited = new Set(scenesToDelete);
|
const visited = new Set(scenesToDelete);
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const parentId = queue.shift();
|
const parentId = queue.shift();
|
||||||
// Chỉ tìm các hotspots xuất phát từ scene hiện tại trỏ đến các scene con.
|
|
||||||
// QUAN TRỌNG: Phải loại bỏ các liên kết "Quay lại" (is_auto_return: true)
|
|
||||||
// để tránh việc thuật toán đi ngược lên cảnh cha.
|
|
||||||
// Populate target_scene_id để kiểm tra tourId
|
|
||||||
const childHotspots = await Hotspot.find({
|
const childHotspots = await Hotspot.find({
|
||||||
parent_scene_id: parentId,
|
parent_scene_id: parentId,
|
||||||
is_auto_return: { $ne: true }
|
is_auto_return: { $ne: true }
|
||||||
@@ -44,11 +36,15 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
|||||||
const targetIdStr = targetScene._id.toString();
|
const targetIdStr = targetScene._id.toString();
|
||||||
const targetTourId = targetScene.tourId ? targetScene.tourId.toString() : null;
|
const targetTourId = targetScene.tourId ? targetScene.tourId.toString() : null;
|
||||||
|
|
||||||
// [BẢO MẬT] Ngăn chặn xóa dây chuyền sang Tour khác.
|
// [Biên giới Tour] Chỉ xóa nếu:
|
||||||
// Một cảnh chỉ được xóa nếu:
|
// 1. Cùng tourId
|
||||||
// 1. Nó có cùng tourId với cảnh gốc đang bị xóa.
|
// 2. Hoặc là Broken Root (tourId tự trỏ về chính nó nhưng lại được liên kết ở đây)
|
||||||
// 2. tourId của cả hai phải tồn tại (không null) để tránh xóa nhầm dữ liệu cũ chưa migrate.
|
// 3. Hoặc là Orphan (không có tourId)
|
||||||
if (tourId && targetTourId && targetTourId === tourId && !visited.has(targetIdStr)) {
|
const isSameTour = targetTourId === tourIdStr;
|
||||||
|
const isBrokenRoot = targetTourId === targetIdStr;
|
||||||
|
const isOrphan = !targetTourId;
|
||||||
|
|
||||||
|
if (!visited.has(targetIdStr) && (isSameTour || isBrokenRoot || isOrphan)) {
|
||||||
visited.add(targetIdStr);
|
visited.add(targetIdStr);
|
||||||
scenesToDelete.push(targetIdStr);
|
scenesToDelete.push(targetIdStr);
|
||||||
queue.push(targetIdStr);
|
queue.push(targetIdStr);
|
||||||
@@ -57,6 +53,14 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Thu thập tất cả Asset ID liên quan
|
// 2. Thu thập tất cả Asset ID liên quan
|
||||||
const scenes = await Scene.find({ _id: { $in: scenesToDelete } });
|
const scenes = await Scene.find({ _id: { $in: scenesToDelete } });
|
||||||
const assetIds = scenes.map(s => s.assetId).filter(id => id);
|
const assetIds = scenes.map(s => s.assetId).filter(id => id);
|
||||||
@@ -67,27 +71,23 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
|||||||
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
|
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 4. Dọn dẹp Database
|
// 3. Xóa bản ghi trong Database
|
||||||
// Xử lý triệt để Dangling Hotspots:
|
|
||||||
// - parent_scene_id in scenesToDelete: Xóa các điểm điều hướng nằm TRONG các scene bị xóa.
|
|
||||||
// - target_scene_id in scenesToDelete: Xóa các điểm điều hướng từ CÁC SCENE KHÁC (cha hoặc hàng xóm)
|
|
||||||
// đang trỏ đến các scene bị xóa. Điều này giúp ngăn chặn lỗi "Broken Link" trong toàn hệ thống.
|
|
||||||
const hotspotCleanup = await Hotspot.deleteMany({
|
|
||||||
$or: [
|
|
||||||
{ parent_scene_id: { $in: scenesToDelete } },
|
|
||||||
{ target_scene_id: { $in: scenesToDelete } }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const assetCleanup = await Asset.deleteMany({ _id: { $in: assetIds } });
|
const assetCleanup = await Asset.deleteMany({ _id: { $in: assetIds } });
|
||||||
const sceneCleanup = await Scene.deleteMany({ _id: { $in: scenesToDelete } });
|
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
|
// 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', {
|
await logActivity('CASCADE_DELETE_SCENE', {
|
||||||
rootSceneId,
|
message: isRootAction ? `Xóa trọn bộ Tour [${tourName}] và ${childCount} cảnh con` : `Xóa cảnh lẻ [${tourName}] khỏi Tour`,
|
||||||
deletedScenesCount: scenesToDelete.length,
|
deletedScenesCount: scenesToDelete.length,
|
||||||
cleanedHotspotsCount: hotspotCleanup.deletedCount
|
cleanedHotspotsCount: hotspotCleanup.deletedCount
|
||||||
}, performer);
|
}, performer ? performer.toString() : 'System');
|
||||||
|
|
||||||
return { deletedCount: scenesToDelete.length };
|
return { deletedCount: scenesToDelete.length };
|
||||||
};
|
};
|
||||||
@@ -95,51 +95,62 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
|||||||
/**
|
/**
|
||||||
* Lan truyền thiết lập quyền riêng tư cho toàn bộ Tour dựa trên tourId.
|
* Lan truyền thiết lập quyền riêng tư cho toàn bộ Tour dựa trên tourId.
|
||||||
* Đảm bảo tính nhất quán của toàn bộ Tour khi thay đổi quyền truy cập.
|
* Đảm bảo tính nhất quán của toàn bộ Tour khi thay đổi quyền truy cập.
|
||||||
* @param {string} tourId - ID định danh của Tour (thường là ID của cảnh gốc)
|
* @param {string} rootSceneId - ID của cảnh gốc thực hiện thay đổi
|
||||||
* @param {Object} privacyData - Dữ liệu quyền riêng tư mới
|
* @param {Object} privacyData - Dữ liệu quyền riêng tư mới
|
||||||
|
* @param {string} performer - ID người thực hiện (mặc định là System)
|
||||||
*/
|
*/
|
||||||
const propagateScenePrivacy = async (tourId, privacyData) => {
|
const propagateScenePrivacy = async (rootSceneId, privacyData, performer = 'System') => {
|
||||||
if (!tourId) return;
|
const rootScene = await Scene.findById(rootSceneId);
|
||||||
|
if (!rootScene) return;
|
||||||
|
|
||||||
const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData;
|
const tourId = rootScene.tourId || rootScene._id;
|
||||||
|
const tourIdStr = tourId.toString();
|
||||||
|
|
||||||
// 2. Chuẩn bị dữ liệu cập nhật đồng bộ cho toàn bộ chuỗi
|
// 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
|
||||||
const updateFields = { privacy, tourId }; // Luôn đồng bộ tourId
|
let queue = [rootSceneId.toString()];
|
||||||
const unsets = {};
|
let scenesToUpdate = [rootSceneId.toString()];
|
||||||
|
const visited = new Set(scenesToUpdate);
|
||||||
|
|
||||||
if (privacy === 'shared') {
|
while (queue.length > 0) {
|
||||||
// Chỉ gán nếu có giá trị, nếu không thì xóa hẳn để tránh lỗi Duplicate Key Null
|
const parentId = queue.shift();
|
||||||
if (shareToken) {
|
const childHotspots = await Hotspot.find({
|
||||||
updateFields.shareToken = shareToken;
|
parent_scene_id: parentId,
|
||||||
} else {
|
is_auto_return: { $ne: true }
|
||||||
unsets.shareToken = 1;
|
}).populate('target_scene_id', 'tourId');
|
||||||
}
|
|
||||||
updateFields.shareTokenExpires = shareTokenExpires || undefined;
|
|
||||||
updateFields.sharedWith = sharedWith || [];
|
|
||||||
updateFields.sharedEmails = sharedEmails || [];
|
|
||||||
} else {
|
|
||||||
// [BẢO MẬT] Xóa hoàn toàn token cho mọi chế độ không phải 'shared' để tránh lỗi Duplicate Key Null
|
|
||||||
unsets.shareToken = 1;
|
|
||||||
unsets.shareTokenExpires = 1;
|
|
||||||
|
|
||||||
// Nếu là private hoặc public, xóa luôn danh sách thành viên được chia sẻ
|
for (const hs of childHotspots) {
|
||||||
if (privacy !== 'member') {
|
if (hs.target_scene_id && typeof hs.target_scene_id === 'object') {
|
||||||
updateFields.sharedWith = [];
|
const targetScene = hs.target_scene_id;
|
||||||
updateFields.sharedEmails = [];
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xây dựng Query cuối cùng
|
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 };
|
||||||
|
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 };
|
const updateQuery = { $set: updateFields };
|
||||||
if (Object.keys(unsets).length > 0) updateQuery.$unset = unsets;
|
if (Object.keys(unsets).length > 0) updateQuery.$unset = unsets;
|
||||||
|
|
||||||
// 3. Cập nhật hàng loạt dựa trên tourId (Nhanh và chính xác tuyệt đối)
|
// 3. Cập nhật dựa trên danh sách ID đã tìm được qua BFS
|
||||||
await Scene.updateMany(
|
await Scene.updateMany({ _id: { $in: scenesToUpdate } }, updateQuery);
|
||||||
{ tourId: tourId },
|
|
||||||
updateQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy }, 'System');
|
await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy }, performer ? performer.toString() : 'System');
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { deleteSceneCascade, propagateScenePrivacy };
|
module.exports = { deleteSceneCascade, propagateScenePrivacy };
|
||||||
@@ -1099,11 +1099,9 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
|
|||||||
const scene = await sceneRes.json();
|
const scene = await sceneRes.json();
|
||||||
const hotspots = await hotspotsRes.json();
|
const hotspots = await hotspotsRes.json();
|
||||||
|
|
||||||
// [FIX] Lưu tourId đang hoạt động sau khi đã nạp dữ liệu scene thành công
|
// [TOUR ID] Cập nhật tourId hiện tại vào localStorage để các cảnh con kế thừa đúng
|
||||||
const currentTourId = scene.tourId?._id || scene.tourId || scene._id;
|
const currentTourId = scene.tourId?._id || scene.tourId || scene._id;
|
||||||
if (!localStorage.getItem('activeTourId') || force) {
|
localStorage.setItem('activeTourId', currentTourId);
|
||||||
localStorage.setItem('activeTourId', currentTourId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
||||||
|
|
||||||
@@ -2063,7 +2061,8 @@ window.openEditMetadataModal = function(scene, isChildArg = null) {
|
|||||||
sharedUsersData = scene.sharedWith || [];
|
sharedUsersData = scene.sharedWith || [];
|
||||||
sharedEmailsData = scene.sharedEmails || [];
|
sharedEmailsData = scene.sharedEmails || [];
|
||||||
|
|
||||||
// [FIX] Cập nhật activeTourId ngay khi chỉnh sửa để đồng bộ luồng tạo tour
|
// [TOUR ID] Cập nhật activeTourId ngay khi mở modal sửa để đảm bảo
|
||||||
|
// các thao tác tạo cảnh con sau đó (nếu có) luôn mang ID của Tour này.
|
||||||
const currentTourId = scene.tourId?._id || scene.tourId || scene._id;
|
const currentTourId = scene.tourId?._id || scene.tourId || scene._id;
|
||||||
localStorage.setItem('activeTourId', currentTourId);
|
localStorage.setItem('activeTourId', currentTourId);
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 MiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 MiB After Width: | Height: | Size: 6.9 MiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
Reference in New Issue
Block a user