const express = require('express'); const router = express.Router(); const Hotspot = require('../models/Hotspot'); const Scene = require('../models/Scene'); const { protect, optionalAuth } = require('../middlewares/authMiddleware'); const { calculateReverseYaw } = require('../utils/hotspotHelper'); const Tour = require('../models/Tour'); /** * @route GET /api/hotspots/:scene_id * @desc Lấy toàn bộ danh sách hotspot của một cảnh */ router.get('/:scene_id', optionalAuth, async (req, res) => { try { // [SECURITY] Kiểm tra quyền xem hotspots dựa trên quyền truy cập cảnh cha const scene = await Scene.findById(req.params.scene_id).populate('tourId'); if (!scene) return res.status(404).json({ message: 'Scene not found' }); const tour = scene.tourId; 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); const isTourTokenValid = tour?.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires); let hasAccess = (tour?.privacy === 'public') || (scene.privacy === 'public') || isOwner || isAdmin || (scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) || (tour?.privacy === 'shared' && req.query.token === tour.shareToken && isTourTokenValid) || (tour?.privacy === 'member' && req.user && req.user._id && req.user.role !== 'guest'); // Bridge Access cho Hotspots if (!hasAccess && req.query.token) { const potentialParents = await Hotspot.find({ target_scene_id: scene._id }).distinct('parent_scene_id'); const authorizedParentExists = await Scene.exists({ _id: { $in: potentialParents }, shareToken: req.query.token, $or: [{ shareTokenExpires: null }, { shareTokenExpires: { $gt: new Date() } }] }); if (authorizedParentExists) hasAccess = true; } if (!hasAccess) { console.warn(`[Backend-Hotspots-Denied] Scene: ${req.params.scene_id}`); return res.status(403).json({ message: 'Bạn không có quyền xem các điểm điều hướng của cảnh này.' }); } const hotspots = await Hotspot.find({ parent_scene_id: req.params.scene_id }) .populate({ path: 'target_scene_id', select: 'name title assetId privacy shareToken', populate: { path: 'assetId', select: '_id' } }) .lean(); res.json(hotspots); } catch (error) { res.status(500).json({ message: error.message }); } }); /** * @route POST /api/hotspots/create * @desc Tạo mới Hotspot và tự động tạo liên kết quay lại */ router.post('/create', protect, async (req, res) => { try { const { parent_scene_id, target_scene_id, title, description, coordinates } = req.body; const parentScene = await Scene.findById(parent_scene_id); if (!parentScene || (parentScene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin')) { 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: { yaw: Number(coordinates?.yaw) || 0, pitch: Number(coordinates?.pitch) || 0 }, is_auto_return: false }); await hotspot.save(); // [BẢO MẬT] Logic tạo liên kết quay lại tự động nếu có scene đích if (target_scene_id) { const targetScene = await Scene.findById(target_scene_id); if (targetScene) { // [TASK 3] BẢO VỆ tourId & CHẶN VANDALISM: // Chỉ tự động tạo link quay lại (tức là ghi dữ liệu vào cảnh đích) // nếu người dùng hiện tại cũng là chủ sở hữu của cảnh đích đó. // Điều này đảm bảo: // 1. Không thay đổi cấu trúc tour của người khác khi tạo liên kết chéo (Cross-link). // 2. Tuyệt đối không can thiệp vào trường 'tourId' của targetScene. if (targetScene.createdBy.toString() === req.user._id.toString()) { const reverseYaw = calculateReverseYaw(coordinates.yaw); const reverseHotspot = new Hotspot({ parent_scene_id: target_scene_id, target_scene_id: parent_scene_id, title: `Quay lại ${parentScene.name || parentScene.title}`, coordinates: { yaw: reverseYaw, pitch: 0 }, is_auto_return: true }); await reverseHotspot.save(); } } } res.status(201).json(hotspot); } catch (error) { res.status(500).json({ message: error.message }); } }); /** * @route PUT /api/hotspots/update/:id * @desc Cập nhật thông tin/vị trí hotspot */ router.put('/update/:id', protect, async (req, res) => { try { 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' }); const parentScene = await Scene.findById(hotspot.parent_scene_id); if (parentScene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') { 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; await hotspot.save(); res.json(hotspot); } catch (error) { res.status(500).json({ message: error.message }); } }); /** * @route DELETE /api/hotspots/delete/:id * @desc Xóa hotspot và liên kết quay lại tự động nếu có */ router.delete('/delete/:id', protect, async (req, res) => { try { const hotspot = await Hotspot.findById(req.params.id); if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' }); const parentScene = await Scene.findById(hotspot.parent_scene_id); if (parentScene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') { return res.status(403).json({ message: 'Không có quyền xóa' }); } // Xóa liên kết ngược nếu đây là cặp đôi tự động tạo if (hotspot.target_scene_id) { await Hotspot.deleteOne({ parent_scene_id: hotspot.target_scene_id, target_scene_id: hotspot.parent_scene_id, is_auto_return: true }); } await Hotspot.findByIdAndDelete(req.params.id); res.json({ message: 'Hotspot deleted successfully' }); } catch (error) { res.status(500).json({ message: error.message }); } }); module.exports = router;