sửa lỗi privacy cho nhiều scene con, không cho phép ghi hoặc nhận sai
This commit is contained in:
@@ -13,7 +13,7 @@ const { checkQuota } = require('../middlewares/quotaMiddleware');
|
||||
const { resizeTo8K } = require('../utils/imageHelper');
|
||||
const { injectGPSCoordinates } = require('../utils/exifHelper');
|
||||
const { imageQueue } = require('./imageQueue');
|
||||
const { deleteSceneCascade } = require('../utils/sceneHelper');
|
||||
const { deleteSceneCascade, propagateScenePrivacy } = require('../utils/sceneHelper');
|
||||
|
||||
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads');
|
||||
const tempDir = path.join(uploadDir, 'temp');
|
||||
@@ -108,7 +108,11 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
||||
|
||||
if (!hasAccess) return res.status(403).json({ message: 'Access denied' });
|
||||
|
||||
const isChildScene = await Hotspot.exists({ target_scene_id: scene._id });
|
||||
// Một cảnh chỉ được coi là cảnh con nếu có hotspot đi tới (không phải link quay lại) trỏ đến nó
|
||||
const isChildScene = await Hotspot.exists({
|
||||
target_scene_id: scene._id,
|
||||
is_auto_return: { $ne: true }
|
||||
});
|
||||
res.json({ ...scene.toObject(), isChildScene: !!isChildScene });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
@@ -125,6 +129,18 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
return res.status(403).json({ message: 'Not authorized' });
|
||||
}
|
||||
|
||||
// [BẢO MẬT] Kiểm tra nếu là cảnh con thì chặn thay đổi Privacy
|
||||
// Chỉ chặn nếu cảnh này là đích đến của một luồng điều hướng đi xuôi
|
||||
const isChild = await Hotspot.exists({
|
||||
target_scene_id: req.params.id,
|
||||
is_auto_return: { $ne: true }
|
||||
});
|
||||
if (isChild && privacy && privacy !== scene.privacy) {
|
||||
return res.status(403).json({
|
||||
message: "Cảnh này thuộc một tour. Vui lòng thay đổi quyền riêng tư tại Cảnh gốc để đồng bộ."
|
||||
});
|
||||
}
|
||||
|
||||
const oldPrivacy = scene.privacy;
|
||||
scene.name = title || scene.name;
|
||||
scene.description = description !== undefined ? description : scene.description;
|
||||
@@ -132,10 +148,29 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
if (lat) scene.gps.lat = parseFloat(lat);
|
||||
if (lng) scene.gps.lng = parseFloat(lng);
|
||||
|
||||
if (sharedWithUsers) try { scene.sharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
||||
if (sharedEmails) try { scene.sharedEmails = JSON.parse(sharedEmails); } catch (e) {}
|
||||
// [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.
|
||||
if (scene.privacy !== 'shared') {
|
||||
scene.shareToken = undefined; // Mongoose sẽ xóa field này khỏi document
|
||||
scene.shareTokenExpires = undefined; // Mướng sẽ xóa field này khỏi document
|
||||
// Nếu không phải 'member', xóa luôn danh sách chia sẻ người dùng
|
||||
if (scene.privacy !== 'member') {
|
||||
scene.sharedWith = [];
|
||||
scene.sharedEmails = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (privacy === 'shared') {
|
||||
if (scene.privacy !== 'private') {
|
||||
// Cập nhật danh sách chia sẻ nếu không phải chế độ Private
|
||||
if (sharedWithUsers) {
|
||||
try { scene.sharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
||||
}
|
||||
if (sharedEmails) {
|
||||
try { scene.sharedEmails = JSON.parse(sharedEmails); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (scene.privacy === 'shared') {
|
||||
if (!scene.shareToken) scene.shareToken = crypto.randomBytes(24).toString('hex');
|
||||
if (shareExpireDays && shareExpireDays !== 'never') {
|
||||
const expires = new Date();
|
||||
@@ -159,7 +194,13 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
}
|
||||
|
||||
await scene.save();
|
||||
res.json({ message: 'Scene updated', scene });
|
||||
|
||||
// [CASCADING] Lan truyền Privacy xuống các cảnh con nếu đây là cảnh cha
|
||||
if (!isChild) {
|
||||
await propagateScenePrivacy(scene._id, 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 });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
|
||||
@@ -78,4 +78,76 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
||||
return { deletedCount: scenesToDelete.length };
|
||||
};
|
||||
|
||||
module.exports = { deleteSceneCascade };
|
||||
/**
|
||||
* Lan truyền thiết lập quyền riêng tư từ Scene cha xuống TOÀN BỘ các Scene con trong Tour (Đệ quy/BFS).
|
||||
* Đả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} parentSceneId - ID của Scene cha vừa được cập nhật
|
||||
* @param {Object} privacyData - Dữ liệu quyền riêng tư từ Scene cha (privacy, tokens, v.v.)
|
||||
*/
|
||||
const propagateScenePrivacy = async (parentSceneId, privacyData) => {
|
||||
const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData;
|
||||
|
||||
// 1. Tìm tất cả các scene con ở mọi cấp độ bằng thuật toán BFS
|
||||
let queue = [parentSceneId.toString()];
|
||||
let allChildIds = [];
|
||||
const visited = new Set(queue);
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift();
|
||||
// Tìm các hotspots xuất phát từ scene hiện tại (bỏ qua link quay lại để tránh vòng lặp)
|
||||
const hotspots = await Hotspot.find({
|
||||
parent_scene_id: currentId,
|
||||
is_auto_return: { $ne: true }
|
||||
}).select('target_scene_id');
|
||||
|
||||
for (const hs of hotspots) {
|
||||
if (hs.target_scene_id) {
|
||||
const targetIdStr = hs.target_scene_id.toString();
|
||||
if (!visited.has(targetIdStr)) {
|
||||
visited.add(targetIdStr);
|
||||
allChildIds.push(targetIdStr);
|
||||
queue.push(targetIdStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allChildIds.length === 0) return;
|
||||
|
||||
// 2. Chuẩn bị dữ liệu cập nhật đồng bộ cho toàn bộ chuỗi
|
||||
const updateFields = { privacy };
|
||||
const updateQuery = { $set: updateFields };
|
||||
|
||||
if (privacy === 'shared') {
|
||||
// Chỉ gán nếu có giá trị, nếu không thì xóa hẳn để tránh lỗi Duplicate Key Null
|
||||
if (shareToken) {
|
||||
updateFields.shareToken = shareToken;
|
||||
} else {
|
||||
if (!updateQuery.$unset) updateQuery.$unset = {};
|
||||
updateQuery.$unset.shareToken = 1;
|
||||
}
|
||||
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
|
||||
if (!updateQuery.$unset) updateQuery.$unset = {};
|
||||
updateQuery.$unset.shareToken = 1;
|
||||
updateQuery.$unset.shareTokenExpires = 1;
|
||||
// Nếu là private hoặc public, xóa luôn danh sách thành viên được chia sẻ
|
||||
if (privacy !== 'member') {
|
||||
updateFields.sharedWith = [];
|
||||
updateFields.sharedEmails = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Cập nhật hàng loạt cho tất cả các scene con được tìm thấy
|
||||
await Scene.updateMany(
|
||||
{ _id: { $in: allChildIds } },
|
||||
updateQuery
|
||||
);
|
||||
|
||||
await logActivity('PROPAGATE_PRIVACY_DEEP', { parentSceneId, childCount: allChildIds.length, privacy }, 'System');
|
||||
};
|
||||
|
||||
module.exports = { deleteSceneCascade, propagateScenePrivacy };
|
||||
Reference in New Issue
Block a user