diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 9596c69..f3412e5 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -78,8 +78,9 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => { return res.status(400).json({ message: 'Please upload a panorama image' }); } - const latitude = Number(lat); - const longitude = Number(lng); + // Đảm bảo ép kiểu Number tuyệt đối trước khi lưu DB + const latitude = Number(lat) || 0; + const longitude = Number(lng) || 0; if (isNaN(latitude) || isNaN(longitude)) { // Cleanup uploaded file on validation error @@ -136,7 +137,6 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => { // 7. Save Scene to DB const scene = new Scene({ name: title, - assetId: asset._id, scene_url: processedFilePath, // Lưu đường dẫn ảnh trực tiếp gps: { lat: latitude, @@ -260,9 +260,7 @@ router.post('/hotspots/create', protect, async (req, res) => { const { parent_scene_id, target_scene_id, title, description, coordinates } = req.body; const parentScene = await Scene.findById(parent_scene_id); - // Phân quyền: Admin hoặc Người tạo Scene - const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString()); - if (!parentScene || !isAuthorized) { + if (!parentScene || parentScene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Không có quyền tạo hotspot cho scene này' }); } @@ -272,8 +270,8 @@ router.post('/hotspots/create', protect, async (req, res) => { title, description, coordinates: { - yaw: Number(coordinates.yaw), - pitch: Number(coordinates.pitch) + yaw: Number(coordinates?.yaw) || 0, + pitch: Number(coordinates?.pitch) || 0 }, is_auto_return: false }); @@ -283,14 +281,10 @@ router.post('/hotspots/create', protect, async (req, res) => { const targetScene = await Scene.findById(target_scene_id); if (targetScene) { const reverseYaw = coordinates.yaw > 0 ? coordinates.yaw - 180 : coordinates.yaw + 180; - - // Fallback đa tầng cho tiêu đề quay lại - const backLabel = title || parentScene.name || parentScene.title || 'cảnh trước'; - const reverseHotspot = new Hotspot({ parent_scene_id: target_scene_id, target_scene_id: parent_scene_id, - title: `Quay lại ${backLabel}`, + title: `Quay lại ${parentScene.name}`, coordinates: { yaw: reverseYaw, pitch: 0 }, is_auto_return: true }); @@ -315,20 +309,13 @@ router.put('/hotspots/update/:id', protect, async (req, res) => { if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' }); const parentScene = await Scene.findById(hotspot.parent_scene_id); - // Phân quyền Admin hoặc Owner - const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString()); - if (!isAuthorized) { + if (parentScene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Không có quyền cập nhật' }); } if (title) hotspot.title = title; if (description) hotspot.description = description; - if (coordinates) { - hotspot.coordinates = { - yaw: Number(coordinates.yaw), - pitch: Number(coordinates.pitch) - }; - } + if (coordinates) hotspot.coordinates = coordinates; await hotspot.save(); res.json(hotspot); @@ -347,9 +334,7 @@ router.delete('/hotspots/delete/:id', protect, async (req, res) => { if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' }); const parentScene = await Scene.findById(hotspot.parent_scene_id); - // Phân quyền Admin hoặc Owner - const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString()); - if (!isAuthorized) { + if (parentScene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Không có quyền xóa' }); } @@ -432,17 +417,46 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => { const { title, privacy, sharedWithUsers, lat, lng } = req.body; const scene = await Scene.findById(req.params.id); - // Phân quyền Admin hoặc Owner - const isAuthorized = req.user.role === 'Chủ sở hữu' || (scene && scene.createdBy.toString() === req.user._id.toString()); - if (!scene || !isAuthorized) { + if (!scene || scene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Not authorized' }); } + const oldPrivacy = scene.privacy; + // Update basic info scene.name = title || scene.name; scene.privacy = privacy || scene.privacy; - if (lat) scene.gps.lat = Number(lat); - if (lng) scene.gps.lng = Number(lng); + if (lat) scene.gps.lat = parseFloat(lat); + if (lng) scene.gps.lng = parseFloat(lng); + + // LOGIC ĐỒNG BỘ QUYỀN RIÊNG TƯ (CASCADING PRIVACY) + // Nếu quyền chia sẻ thay đổi, cập nhật toàn bộ các Scene con liên kết trực tiếp + if (privacy && privacy !== oldPrivacy) { + try { + const linkedHotspots = await Hotspot.find({ parent_scene_id: scene._id }); + const targetSceneIds = linkedHotspots + .map(h => h.target_scene_id) + .filter(id => id && id.toString() !== scene._id.toString()); + + if (targetSceneIds.length > 0) { + for (const targetId of targetSceneIds) { + const updateData = { privacy: privacy }; + + // Nếu chuyển sang 'shared', đảm bảo scene con cũng có token riêng + if (privacy === 'shared') { + const target = await Scene.findById(targetId); + if (target && !target.shareToken) { + updateData.shareToken = crypto.randomBytes(24).toString('hex'); + } + } + await Scene.updateOne({ _id: targetId }, { $set: updateData }); + } + console.log(`[Privacy Sync] Cascaded ${privacy} status to ${targetSceneIds.length} linked scenes.`); + } + } catch (err) { + console.error("Lỗi khi đồng bộ quyền riêng tư cho các scene con:", err.message); + } + } if (privacy === 'shared' && !scene.shareToken) { scene.shareToken = crypto.randomBytes(24).toString('hex'); @@ -481,9 +495,7 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => { router.delete('/scenes/:id', protect, async (req, res) => { try { const scene = await Scene.findById(req.params.id); - // Phân quyền Admin hoặc Owner - const isAuthorized = req.user.role === 'Chủ sở hữu' || (scene && scene.createdBy.toString() === req.user._id.toString()); - if (!scene || !isAuthorized) { + if (!scene || scene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Not authorized' }); } diff --git a/backend/uploads/processed_1780894157770_b06d7c6e.JPG.jpg b/backend/uploads/processed_1780894157770_b06d7c6e.JPG.jpg deleted file mode 100644 index 552e6da..0000000 Binary files a/backend/uploads/processed_1780894157770_b06d7c6e.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780894179644_5d0cb801.JPG.jpg b/backend/uploads/processed_1780894179644_5d0cb801.JPG.jpg deleted file mode 100644 index fdb1a86..0000000 Binary files a/backend/uploads/processed_1780894179644_5d0cb801.JPG.jpg and /dev/null differ diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index 3128a8b..d640334 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -107,10 +107,9 @@ function initMap() { return; } - // Chỉ Admin (Chủ sở hữu) mới có quyền tạo Scene mới trực tiếp trên map qua contextmenu - const userRole = localStorage.getItem('role'); - const isAdmin = userRole === 'Chủ sở hữu' || userRole === 'admin'; - if (!isAdmin) return; + // Cho phép bất kỳ người dùng nào đã đăng nhập tạo Scene mới trên bản đồ + const token = localStorage.getItem('jwt'); + if (!token) return; const { lat, lng } = e.latlng; openCreateSceneModal(lat, lng); @@ -370,20 +369,25 @@ async function loadScenes() { // Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ scenes.forEach((scene) => { - // Ép kiểu tọa độ về Number để tránh lỗi render bản đồ - const latNum = Number(scene.gps?.lat || scene.lat); - const lngNum = Number(scene.gps?.lng || scene.lng); + // 1. Kiểm tra tọa độ an toàn - Ngăn chặn treo map do NaN + const latNum = Number(scene.gps?.lat ?? scene.lat); + const lngNum = Number(scene.gps?.lng ?? scene.lng); - if (isNaN(latNum) || isNaN(lngNum)) return; + if (isNaN(latNum) || isNaN(lngNum)) { + console.error(`Bỏ qua Scene "${scene.name || scene.title}" do tọa độ lỗi:`, scene); + return; + } - // Logic lọc Ảnh mẹ: Mỗi tọa độ GPS chỉ tạo duy nhất 1 Marker đại diện + // 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; // Bỏ qua nếu tọa độ này đã có Marker + if (seenCoordinates.has(coordKey)) return; seenCoordinates.add(coordKey); - // Kiểm tra an toàn dữ liệu từ MongoDB trước khi truy cập + // 3. Truy cập Asset an toàn const assetId = scene.assetId?._id || scene.assetId; - const sceneName = scene.name || scene.title; + if (!assetId) return; // Bỏ qua nếu không có ảnh liên kết + + const sceneName = scene.name || scene.title || "Untitled Scene"; let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`; if (token) thumbUrl += `?token=${token}`;