diff --git a/backend/middlewares/TourController.js b/backend/middlewares/TourController.js index 40873eb..60a53aa 100644 --- a/backend/middlewares/TourController.js +++ b/backend/middlewares/TourController.js @@ -137,14 +137,15 @@ router.get('/:id', optionalAuth, async (req, res) => { if (!tour) return res.status(404).json({ message: 'Tour không tồn tại.' }); - const isOwner = req.user && tour.createdBy._id.toString() === req.user._id.toString(); + const tourCreatedById = tour.createdBy?._id || tour.createdBy; + const isOwner = req.user && req.user._id && tourCreatedById && tourCreatedById.toString() === req.user._id.toString(); const isAdmin = req.user && req.user.role === 'admin'; const isTokenValid = tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires); const userEmail = req.user ? req.user.email : null; let hasAccess = tour.privacy === 'public' || isOwner || isAdmin || (tour.privacy === 'shared' && req.query.token === tour.shareToken && isTokenValid) || - (tour.privacy === 'member' && req.user && ( + (tour.privacy === 'member' && req.user && req.user._id && ( tour.sharedWith.some(u => u.toString() === req.user._id.toString()) || (userEmail && tour.sharedEmails.includes(userEmail)) )); diff --git a/backend/routes/assetRoutes.js b/backend/routes/assetRoutes.js index 5bcf253..a0db97c 100644 --- a/backend/routes/assetRoutes.js +++ b/backend/routes/assetRoutes.js @@ -64,6 +64,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res let hasAccess = isAdmin || scene.privacy === 'public' || + (tour && tour.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) || diff --git a/backend/routes/sceneRoutes.js b/backend/routes/sceneRoutes.js index 522b5ac..9640a4c 100644 --- a/backend/routes/sceneRoutes.js +++ b/backend/routes/sceneRoutes.js @@ -111,10 +111,14 @@ router.get('/', optionalAuth, async (req, res) => { try { const { token } = req.query; + // [FIX] Lấy danh sách ID của các Tour đang ở chế độ công khai + const publicTours = await Tour.find({ privacy: 'public' }).select('_id'); + const publicTourIds = publicTours.map(t => t._id); + // 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' }; + ? { $or: [{ privacy: 'public' }, { tourId: { $in: publicTourIds } }, { createdBy: req.user._id }, { sharedWith: req.user._id }, { sharedEmails: req.user.email }] } + : { $or: [{ privacy: 'public' }, { tourId: { $in: publicTourIds } }] }; let finalQuery = baseQuery; @@ -130,7 +134,11 @@ router.get('/', optionalAuth, async (req, res) => { }; } - const scenes = await Scene.find(finalQuery).populate('createdBy', 'username').lean(); + console.log(`[SceneRoutes] GET /api/scenes - Final Query for user ${req.user?._id || 'Guest'}:`, JSON.stringify(finalQuery)); + const scenes = await Scene.find(finalQuery) + .populate('createdBy', 'username') + .populate('tourId') // Nạp thông tin Tour để Frontend nhận diện + .lean(); res.json(scenes); } catch (error) { res.status(500).json({ message: error.message }); @@ -150,7 +158,7 @@ router.get('/:id', optionalAuth, async (req, res) => { 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 isOwner = req.user && req.user._id && 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); @@ -160,7 +168,7 @@ router.get('/:id', optionalAuth, async (req, res) => { 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.privacy === 'member' && req.user && req.user._id && ( // Access for members tour.sharedWith.some(u => u.toString() === req.user._id.toString()) || (userEmail && tour.sharedEmails.includes(userEmail)) )); diff --git a/backend/tests/tourCenter.test.js b/backend/tests/tourCenter.test.js index 2ab89e5..012eeb9 100644 --- a/backend/tests/tourCenter.test.js +++ b/backend/tests/tourCenter.test.js @@ -35,6 +35,8 @@ describe('TourController - updateTourCenter', () => { // Trung bình: lat (10+20+30)/3 = 20, lng (20+40+60)/3 = 40 expect(Tour.findByIdAndUpdate).toHaveBeenCalledWith(tourId, { location: { lat: 20.0, lng: 40.0 } + }, { + returnDocument: 'after' }); }); @@ -56,6 +58,8 @@ describe('TourController - updateTourCenter', () => { // Chỉ tính 2 cảnh hợp lệ: lat (10+20)/2 = 15, lng (20+40)/2 = 30 expect(Tour.findByIdAndUpdate).toHaveBeenCalledWith(tourId, { location: { lat: 15.0, lng: 30.0 } + }, { + returnDocument: 'after' }); }); diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index ec57ad4..a335e19 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -954,6 +954,7 @@ async function loadScenes(urlToken = null) { if (!response.ok) throw new Error('Failed to load scenes'); const scenes = await response.json(); + console.log(`[Frontend] loadScenes received ${scenes.length} scenes. User role: ${localStorage.getItem('role') || 'Guest'}`); console.log(`[Data] Nhận được ${scenes.length} scenes từ server`); if (!Array.isArray(scenes)) return; @@ -971,13 +972,16 @@ async function loadScenes(urlToken = null) { const lngNum = Number(scene.gps?.lng ?? scene.lng); if (isNaN(latNum) || isNaN(lngNum)) { - console.error(`Bỏ qua Scene "${scene.name || scene.title}" do tọa độ lỗi:`, scene); + console.warn(`[Frontend] Bỏ qua Scene "${scene.name || scene.title}" (ID: ${scene._id}) do tọa độ lỗi:`, scene); return; } // 2. Logic lọc Ảnh mẹ: Sửa lỗi typo coordKey (dùng latNum 2 lần) const coordKey = `${latNum.toFixed(6)},${lngNum.toFixed(6)}`; - if (seenCoordinates.has(coordKey)) return; + if (seenCoordinates.has(coordKey)) { + console.log(`[Frontend] Bỏ qua Scene "${scene.name || scene.title}" (ID: ${scene._id}) do trùng tọa độ (hotspot con).`); + return; + } seenCoordinates.add(coordKey); // 3. Truy cập Asset an toàn @@ -985,6 +989,7 @@ async function loadScenes(urlToken = null) { if (!assetId) return; // Bỏ qua nếu không có ảnh liên kết const sceneName = scene.name || scene.title || "Untitled Scene"; + console.log(`[Frontend] Đang thêm marker cho Scene: ${sceneName} (ID: ${scene._id}, Privacy: ${scene.privacy})`); const isProcessing = scene.status === 'processing'; if (isProcessing) foundProcessing++; @@ -1053,7 +1058,7 @@ async function loadScenes(urlToken = null) { const ownerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner; // Phân quyền: Admin hoặc Chủ sở hữu Scene - const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu'; + const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu' || userRole === 'moderator'; const isOwner = currentUserId && ownerId && ownerId.toString() === currentUserId.toString(); if (isAdmin || isOwner) { @@ -1099,8 +1104,13 @@ async function handleEditDeleteScene(scene) { const editPrivacyBtn = document.getElementById('btn-edit-privacy-action'); const deleteBtn = document.getElementById('btn-delete-action'); const shareBtn = document.getElementById('btn-share-action'); + const desc = document.getElementById('action-modal-desc'); + + const tour = scene.tourId; // Tour đã được populate từ backend + + title.innerText = tour ? `Tour: ${tour.name}` : `Scene: ${scene.title}`; + desc.innerText = tour ? "Bạn muốn thực hiện thao tác gì với Tour này?" : "Bạn muốn thực hiện thao tác gì với scene này?"; - title.innerText = `Scene: ${scene.title}`; modal.style.display = 'flex'; // Hành động Lấy link chia sẻ trực tiếp @@ -1113,22 +1123,38 @@ async function handleEditDeleteScene(scene) { editPrivacyBtn.onclick = () => { returnToDashboardAfterEdit = false; closeActionModal(); - // Sử dụng thuộc tính isChildScene từ backend để quyết định quyền chỉnh sửa - openEditMetadataModal(scene, scene.isChildScene); + if (tour) { + openEditTourModal(tour); + } else { + openEditMetadataModal(scene, scene.isChildScene); + } }; - // Gán sự kiện cho nút Sửa + // Cập nhật nhãn và sự kiện cho nút Sửa + editBtn.innerHTML = tour ? '✏️ Sửa thông tin Tour' : '✏️ Chế độ sửa scene'; editBtn.onclick = () => { returnToDashboardAfterEdit = false; closeActionModal(); - openEditMetadataModal(scene, scene.isChildScene); + if (tour) { + openEditTourModal(tour); + } else { + openEditMetadataModal(scene, scene.isChildScene); + } }; - // Gán sự kiện cho nút Xóa + // Cập nhật nhãn và sự kiện cho nút Xóa + deleteBtn.innerHTML = tour ? '🗑️ Xóa vĩnh viễn Tour' : '🗑️ Xóa vĩnh viễn'; deleteBtn.onclick = () => { returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map closeActionModal(); - deleteScene(scene._id); + if (tour) { + // Tái sử dụng logic xóa tour từ dashboard + if (confirm(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ cảnh bên trong?`)) { + confirmDeleteTourFromMap(tour._id); + } + } else { + deleteScene(scene._id); + } }; } @@ -1139,6 +1165,23 @@ function closeActionModal() { document.getElementById('action-choice-modal').style.display = 'none'; } +/** + * Xóa Tour trực tiếp từ Map + */ +async function confirmDeleteTourFromMap(tourId) { + const token = localStorage.getItem('jwt'); + try { + const res = await fetch(`${API_BASE_URL}/tours/${tourId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (res.ok) { + showNotification("Đã xóa Tour thành công", "success"); + loadScenes(); // Tải lại bản đồ + } + } catch (e) { showNotification("Lỗi xóa tour", "error"); } +} + /** * Mở modal xác nhận xóa scene */ diff --git a/uploads/processed_1781103810966_7acb1aea.jpg.jpg b/uploads/processed_1781103810966_7acb1aea.jpg.jpg new file mode 100644 index 0000000..29e6c31 Binary files /dev/null and b/uploads/processed_1781103810966_7acb1aea.jpg.jpg differ diff --git a/uploads/processed_1781103856812_7d988385.jpg.jpg b/uploads/processed_1781103856812_7d988385.jpg.jpg new file mode 100644 index 0000000..e43981f Binary files /dev/null and b/uploads/processed_1781103856812_7d988385.jpg.jpg differ