Refactor giai đoạn 1: test các tính năng vừa thay đổi như tour, scene...
This commit is contained in:
@@ -13,13 +13,17 @@ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
|
||||
// Import các sub-routers
|
||||
const adminRoutes = require('./adminRoutes');
|
||||
const sceneRoutes = require('./sceneRoutes');
|
||||
const userRoutes = require('./userRoutes');
|
||||
const tourRoutes = require('../middlewares/TourController'); // Đường dẫn thực tế hiện tại
|
||||
const authRoutes = require('./authRoutes');
|
||||
const userRoutes = require('./userRoutes');
|
||||
const hotspotRoutes = require('./hotspotRoutes');
|
||||
const assetRoutes = require('./assetRoutes');
|
||||
|
||||
// Các module chưa tách hết (có thể tách tiếp ở Giai đoạn sau)
|
||||
// Ở đây tôi gắn các route còn lại trực tiếp để không làm gián đoạn hệ thống
|
||||
router.use('/admin', adminRoutes);
|
||||
router.use('/auth', authRoutes); // Tích hợp API Đăng ký/Đăng nhập
|
||||
router.use('/tours', tourRoutes); // Thêm các route cho Tour
|
||||
router.use('/scenes', sceneRoutes);
|
||||
router.use('/users', userRoutes);
|
||||
router.use('/me', userRoutes); // Frontend gọi /api/me/profile, sẽ trỏ vào userRoutes
|
||||
|
||||
@@ -3,6 +3,7 @@ const router = express.Router();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const Asset = require('../models/Asset');
|
||||
const Scene = require('../models/Scene');
|
||||
@@ -26,20 +27,51 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
|
||||
const asset = await Asset.findById(req.params.assetId);
|
||||
if (!asset) return res.status(404).json({ message: 'Asset not found' });
|
||||
|
||||
// [FIX] Luôn kiểm tra JWT từ query string ngay cả khi optionalAuth đã chạy
|
||||
let user = req.user;
|
||||
const isGuest = !user || user.role === 'guest';
|
||||
if (isGuest && req.query.token) {
|
||||
try {
|
||||
const decoded = jwt.verify(req.query.token, process.env.JWT_SECRET || 'your_jwt_secret');
|
||||
if (decoded && decoded.id) {
|
||||
const User = require('../models/User');
|
||||
const authenticatedUser = await User.findById(decoded.id);
|
||||
if (authenticatedUser) user = authenticatedUser;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
const isAdmin = user && (user.role === 'admin' || user.role === 'moderator');
|
||||
const userIdStr = user && user._id ? user._id.toString() : null;
|
||||
const userEmail = user ? user.email : null;
|
||||
|
||||
// Kiểm tra quyền truy cập dựa trên Privacy của Scene liên kết
|
||||
const scene = await Scene.findOne({ assetId: asset._id });
|
||||
const scene = await Scene.findOne({ assetId: asset._id }).populate('tourId');
|
||||
if (!scene) {
|
||||
// Asset mồ côi, chỉ chủ sở hữu được xem
|
||||
if (!req.user || req.user._id.toString() !== asset.uploadedBy.toString()) {
|
||||
if (!isAdmin && (!userIdStr || userIdStr !== asset.uploadedBy.toString())) {
|
||||
return res.status(403).json({ message: 'Bạn không được phép di chuyển đến cảnh này' });
|
||||
}
|
||||
} else {
|
||||
const isTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
let hasAccess = scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user && (scene.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||
(req.user && scene.createdBy.toString() === req.user._id.toString()) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
|
||||
const isSceneTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
const tour = scene.tourId;
|
||||
const isTourTokenValid = tour && tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
||||
|
||||
// Chuẩn hóa ID người tạo để so sánh
|
||||
const sceneOwnerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
||||
const isOwner = userIdStr && sceneOwnerId && sceneOwnerId.toString() === userIdStr;
|
||||
|
||||
let hasAccess = isAdmin ||
|
||||
scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && userIdStr && (scene.sharedWith.some(id => id.toString() === userIdStr) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||
isOwner ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) ||
|
||||
(tour && tour.privacy === 'shared' && req.query.token === tour.shareToken && isTourTokenValid);
|
||||
|
||||
if (scene.status === 'processing' && !hasAccess) {
|
||||
return res.status(403).json({ message: 'Ảnh đang được xử lý và bạn không có quyền xem tạm thời' });
|
||||
}
|
||||
|
||||
// [BRIDGE ACCESS LOGIC]
|
||||
// Áp dụng tương tự cho Asset để đảm bảo hiển thị được ảnh khi di chuyển liên kết chéo
|
||||
@@ -131,7 +163,7 @@ router.get('/assets/view_avatar/:filename', async (req, res) => {
|
||||
*/
|
||||
router.get('/me/assets', protect, async (req, res) => {
|
||||
try {
|
||||
const query = (req.user.role === 'admin' || req.user.role === 'Chủ sở hữu') ? {} : { uploadedBy: req.user._id };
|
||||
const query = (req.user.role === 'admin') ? {} : { uploadedBy: req.user._id };
|
||||
|
||||
const assets = await Asset.aggregate([
|
||||
{ $match: query },
|
||||
|
||||
@@ -38,9 +38,19 @@ router.post('/create', protect, async (req, res) => {
|
||||
return res.status(403).json({ message: 'Không có quyền tạo hotspot cho scene này' });
|
||||
}
|
||||
|
||||
// [NEW LOGIC] Xử lý liên kết chéo giữa các Tour
|
||||
let target_tour_id = undefined;
|
||||
if (target_scene_id) {
|
||||
const targetScene = await Scene.findById(target_scene_id);
|
||||
if (targetScene && targetScene.tourId && parentScene.tourId && targetScene.tourId.toString() !== parentScene.tourId.toString()) {
|
||||
target_tour_id = targetScene.tourId;
|
||||
}
|
||||
}
|
||||
|
||||
const hotspot = new Hotspot({
|
||||
parent_scene_id,
|
||||
target_scene_id,
|
||||
target_tour_id,
|
||||
title,
|
||||
description,
|
||||
coordinates: {
|
||||
@@ -87,7 +97,7 @@ router.post('/create', protect, async (req, res) => {
|
||||
*/
|
||||
router.put('/update/:id', protect, async (req, res) => {
|
||||
try {
|
||||
const { title, description, coordinates } = req.body;
|
||||
const { title, description, coordinates, target_scene_id } = req.body;
|
||||
const hotspot = await Hotspot.findById(req.params.id);
|
||||
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
|
||||
|
||||
@@ -96,6 +106,20 @@ router.put('/update/:id', protect, async (req, res) => {
|
||||
return res.status(403).json({ message: 'Không có quyền cập nhật' });
|
||||
}
|
||||
|
||||
// Cập nhật target_scene_id và tính toán lại target_tour_id nếu có thay đổi
|
||||
if (target_scene_id) {
|
||||
const targetScene = await Scene.findById(target_scene_id);
|
||||
if (targetScene) {
|
||||
hotspot.target_scene_id = target_scene_id;
|
||||
// Kiểm tra liên kết chéo
|
||||
if (targetScene.tourId && parentScene.tourId && targetScene.tourId.toString() !== parentScene.tourId.toString()) {
|
||||
hotspot.target_tour_id = targetScene.tourId;
|
||||
} else {
|
||||
hotspot.target_tour_id = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (title) hotspot.title = title;
|
||||
if (description) hotspot.description = description;
|
||||
if (coordinates) hotspot.coordinates = coordinates;
|
||||
|
||||
@@ -23,14 +23,25 @@ const imageWorker = new Worker('image-processing', async (job) => {
|
||||
// 3. Chèn GPS Metadata
|
||||
await injectGPSCoordinates(processedFilePath, latitude, longitude);
|
||||
|
||||
// 4. Cập nhật đường dẫn file thực tế vào Database
|
||||
// Lúc này ảnh đã sẵn sàng để phục vụ (8K)
|
||||
await Asset.findByIdAndUpdate(assetId, { filePath: processedFilePath });
|
||||
await Scene.findByIdAndUpdate(sceneId, {
|
||||
// 4. Cập nhật đồng thời cả Asset và Scene
|
||||
await Asset.findByIdAndUpdate(assetId, {
|
||||
filePath: processedFilePath
|
||||
}, { returnDocument: 'after' });
|
||||
|
||||
const scene = await Scene.findByIdAndUpdate(sceneId, {
|
||||
scene_url: processedFilePath,
|
||||
status: 'completed' // Xử lý xong
|
||||
}, {
|
||||
// Thay thế 'new: true' bằng 'returnDocument: after' để tránh cảnh báo deprecation
|
||||
returnDocument: 'after'
|
||||
});
|
||||
|
||||
// 4.1 Tự động tính toán lại vị trí trung tâm của Tour sau khi ảnh đã được xử lý và chèn GPS thành công
|
||||
if (scene && scene.tourId) {
|
||||
const tourController = require('../middlewares/TourController');
|
||||
if (tourController.updateTourCenter) await tourController.updateTourCenter(scene.tourId);
|
||||
}
|
||||
|
||||
// 5. Dọn dẹp file tạm
|
||||
await fs.promises.unlink(tempFilePath).catch(() => {});
|
||||
|
||||
|
||||
+121
-69
@@ -6,6 +6,7 @@ const crypto = require('crypto');
|
||||
const multer = require('multer');
|
||||
|
||||
const Scene = require('../models/Scene');
|
||||
const Tour = require('../models/Tour');
|
||||
const Asset = require('../models/Asset');
|
||||
const Hotspot = require('../models/Hotspot');
|
||||
const { protect, optionalAuth } = require('../middlewares/authMiddleware');
|
||||
@@ -37,40 +38,17 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
|
||||
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] 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
|
||||
// [QUY TRÌNH MỚI] Bắt buộc tourId và kế thừa từ Tour model
|
||||
if (!tourId || tourId === 'null' || tourId === 'undefined') {
|
||||
return res.status(400).json({ message: 'tourId là bắt buộc khi tạo cảnh mới.' });
|
||||
}
|
||||
|
||||
try { if (sharedWithUsers) finalSharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
||||
const tour = await Tour.findById(tourId);
|
||||
if (!tour) return res.status(404).json({ message: 'Tour không tồn tại hoặc đã bị xóa.' });
|
||||
|
||||
// [BẢO MẬT] Xác thực tourId nếu được cung cấp
|
||||
if (cleanedTourId) {
|
||||
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');
|
||||
}
|
||||
// [SECURITY] Chỉ chủ sở hữu Tour hoặc Admin mới được thêm cảnh
|
||||
if (tour.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: 'Bạn không có quyền thêm cảnh vào Tour này.' });
|
||||
}
|
||||
|
||||
const latitude = Number(lat) || 0;
|
||||
@@ -93,18 +71,30 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
|
||||
scene_url: tempFilePath,
|
||||
gps: { lat: latitude, lng: longitude },
|
||||
createdBy: req.user._id,
|
||||
privacy: finalPrivacy,
|
||||
shareToken: finalShareToken,
|
||||
shareTokenExpires: finalExpires,
|
||||
sharedWith: finalSharedWith,
|
||||
sharedEmails: finalSharedEmails,
|
||||
privacy: tour.privacy || 'private',
|
||||
status: 'processing',
|
||||
tourId: assignedTourId
|
||||
tourId: tour._id,
|
||||
shareToken: tour.shareToken,
|
||||
shareTokenExpires: tour.shareTokenExpires,
|
||||
sharedWith: tour.sharedWith,
|
||||
sharedEmails: tour.sharedEmails
|
||||
});
|
||||
// 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;
|
||||
|
||||
await scene.save();
|
||||
|
||||
// Cập nhật Tour: Thêm scene vào danh sách và gán rootSceneId nếu là cảnh đầu tiên
|
||||
tour.scenes.push(scene._id);
|
||||
if (!tour.rootSceneId) {
|
||||
tour.rootSceneId = scene._id;
|
||||
}
|
||||
await tour.save();
|
||||
|
||||
// Tự động tính toán lại vị trí trung tâm của Tour khi thêm cảnh mới
|
||||
if (latitude !== 0 || longitude !== 0) {
|
||||
const tourController = require('../middlewares/TourController');
|
||||
if (tourController.updateTourCenter) await tourController.updateTourCenter(tour._id);
|
||||
}
|
||||
|
||||
await imageQueue.add('process-panorama', {
|
||||
tempFilePath, processedFilePath, latitude, longitude, assetId: asset._id, sceneId: scene._id
|
||||
});
|
||||
@@ -119,11 +109,28 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
|
||||
// @route GET /api/scenes
|
||||
router.get('/', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
let query = req.user
|
||||
const { token } = req.query;
|
||||
|
||||
// Quyền cơ bản: Công khai hoặc là chủ sở hữu/thành viên được chia sẻ
|
||||
let baseQuery = req.user && req.user.role !== 'guest'
|
||||
? { $or: [{ privacy: 'public' }, { createdBy: req.user._id }, { sharedWith: req.user._id }, { sharedEmails: req.user.email }] }
|
||||
: { privacy: 'public' };
|
||||
|
||||
const scenes = await Scene.find(query).populate('createdBy', 'username').lean();
|
||||
let finalQuery = baseQuery;
|
||||
|
||||
// Nếu có token từ URL (Guest truy cập link shared), cho phép lấy các scene thuộc Tour/Scene mang token đó
|
||||
if (token) {
|
||||
const tourWithToken = await Tour.findOne({ shareToken: token }).select('_id');
|
||||
finalQuery = {
|
||||
$or: [
|
||||
baseQuery,
|
||||
{ shareToken: token },
|
||||
{ tourId: tourWithToken ? tourWithToken._id : null }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const scenes = await Scene.find(finalQuery).populate('createdBy', 'username').lean();
|
||||
res.json(scenes);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
@@ -133,15 +140,30 @@ router.get('/', optionalAuth, async (req, res) => {
|
||||
// @route GET /api/scenes/:id
|
||||
router.get('/:id', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
const scene = await Scene.findById(req.params.id).populate('createdBy', 'username').populate('assetId');
|
||||
const scene = await Scene.findById(req.params.id)
|
||||
.populate('createdBy', 'username')
|
||||
.populate('assetId')
|
||||
.populate('tourId');
|
||||
|
||||
if (!scene) return res.status(404).json({ message: 'Scene not found' });
|
||||
|
||||
const isTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
const tour = scene.tourId; // tourId is populated
|
||||
if (!tour) return res.status(404).json({ message: 'Tour liên kết không tồn tại.' });
|
||||
|
||||
const isOwner = req.user && tour.createdBy?.toString() === req.user._id.toString();
|
||||
const isAdmin = req.user && req.user.role === 'admin';
|
||||
|
||||
const isSceneTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
const isTourTokenValid = tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
let hasAccess = scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user && (scene.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||
(req.user && scene.createdBy._id.toString() === req.user._id.toString()) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
|
||||
|
||||
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) || // Access via scene's token
|
||||
(tour.privacy === 'shared' && req.query.token === tour.shareToken && isTourTokenValid) || // Access via tour's token
|
||||
(tour.privacy === 'member' && req.user && ( // Access for members
|
||||
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
||||
(userEmail && tour.sharedEmails.includes(userEmail))
|
||||
));
|
||||
|
||||
// [BRIDGE ACCESS LOGIC]
|
||||
// Nếu chưa có quyền, kiểm tra xem người dùng có đến từ một cảnh hợp lệ thuộc Tour khác không
|
||||
@@ -161,9 +183,20 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
||||
|
||||
if (!hasAccess) return res.status(403).json({ message: 'Bạn không được phép di chuyển đến cảnh này' });
|
||||
|
||||
// [BẢO MẬT] Một cảnh là cảnh con nếu nó thuộc về một tour và tourId khác với ID chính nó
|
||||
const isChild = scene.tourId && scene.tourId.toString() !== scene._id.toString();
|
||||
res.json({ ...scene.toObject(), isChildScene: !!isChild });
|
||||
// Increment view count if not owner/admin and not a bot
|
||||
if (!isOwner && !isAdmin && !req.headers['user-agent']?.match(/bot|crawl|spider/i)) {
|
||||
scene.views = (scene.views || 0) + 1;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const viewEntry = scene.viewHistory.find(entry => new Date(entry.date).setHours(0,0,0,0) === today.getTime());
|
||||
if (viewEntry) {
|
||||
viewEntry.count++;
|
||||
} else {
|
||||
scene.viewHistory.push({ date: today, count: 1 });
|
||||
}
|
||||
await scene.save();
|
||||
}
|
||||
res.json(scene);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
@@ -179,12 +212,10 @@ 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
|
||||
// Dựa vào tourId để xác định quan hệ cha-con chính xác, tránh bị nhầm bởi liên kết chéo (cross-link)
|
||||
const isChild = scene.tourId && scene.tourId.toString() !== scene._id.toString();
|
||||
if (isChild && privacy && privacy !== scene.privacy) {
|
||||
// [BẢO MẬT] Chặn thay đổi Privacy trực tiếp trên Scene. Phải thông qua Tour.
|
||||
if (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ộ."
|
||||
message: "Quyền riêng tư phải được quản lý tập trung tại cấp độ Tour."
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,19 +279,10 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
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) {
|
||||
// [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);
|
||||
// Cập nhật lại vị trí trung tâm của Tour nếu tọa độ của Scene này thay đổi
|
||||
if (lat || lng) {
|
||||
const tourController = require('../middlewares/TourController');
|
||||
if (tourController.updateTourCenter) await tourController.updateTourCenter(scene.tourId);
|
||||
}
|
||||
|
||||
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 });
|
||||
@@ -280,8 +302,38 @@ router.delete('/:id', protect, async (req, res) => {
|
||||
return res.status(403).json({ message: 'Forbidden' });
|
||||
}
|
||||
|
||||
let tourId = rootScene.tourId;
|
||||
|
||||
const { deletedCount } = await deleteSceneCascade(rootSceneId, req.user._id);
|
||||
|
||||
// --- NEW LOGIC TO UPDATE PARENT TOUR ---
|
||||
if (tourId) {
|
||||
const tour = await Tour.findById(tourId);
|
||||
if (tour) {
|
||||
// Remove the deleted scene from the tour's scenes array
|
||||
tour.scenes = tour.scenes.filter(sId => sId.toString() !== rootSceneId.toString());
|
||||
|
||||
// If the deleted scene was the rootSceneId, find a new root or set to null
|
||||
if (tour.rootSceneId && tour.rootSceneId.toString() === rootSceneId.toString()) {
|
||||
tour.rootSceneId = tour.scenes.length > 0 ? tour.scenes[0] : null;
|
||||
}
|
||||
|
||||
// [KIỂM TRA CHÍNH XÁC] Đếm số lượng scene thực tế còn lại trong database của Tour này
|
||||
const actualRemainingScenes = await Scene.countDocuments({ tourId: tour._id });
|
||||
|
||||
if (actualRemainingScenes === 0) {
|
||||
await Tour.findByIdAndDelete(tour._id);
|
||||
return res.json({
|
||||
message: `Đã xóa Tour "${tour.name}" vì không còn cảnh nào bên trong.`,
|
||||
tourDeleted: true
|
||||
});
|
||||
} else {
|
||||
await tour.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- END NEW LOGIC ---
|
||||
|
||||
res.json({
|
||||
message: deletedCount > 1
|
||||
? `Đã xóa scene cha và ${deletedCount - 1} scene con liên quan.`
|
||||
|
||||
Reference in New Issue
Block a user