const express = require('express'); const router = express.Router(); const Tour = require('../models/Tour'); const Scene = require('../models/Scene'); const { protect, optionalAuth } = require('../middlewares/authMiddleware'); const { propagateScenePrivacy } = require('../utils/sceneHelper'); const crypto = require('crypto'); // @route POST /api/tours // @desc Tạo một Tour mới (bước đầu tiên trước khi upload ảnh) // @access Private router.post('/', protect, async (req, res) => { try { const { name, description, lat, lng, privacy } = req.body; if (!name) { return res.status(400).json({ message: 'Tên Tour là bắt buộc.' }); } const newTour = new Tour({ name, description, location: { lat: Number(lat) || 0, lng: Number(lng) || 0 }, createdBy: req.user._id, privacy: privacy || 'private', scenes: [], shareToken: (privacy === 'shared') ? crypto.randomBytes(24).toString('hex') : undefined }); if (newTour.privacy === 'shared') { // Thiết lập hạn mặc định 7 ngày nếu không chỉ định const expires = new Date(); expires.setDate(expires.getDate() + 7); newTour.shareTokenExpires = expires; } await newTour.save(); res.status(201).json({ message: 'Tour đã được tạo thành công.', tour: newTour }); } catch (error) { res.status(500).json({ message: error.message }); } }); // @route PUT /api/tours/:id // @desc Cập nhật Tour và lan truyền quyền riêng tư xuống các cảnh con // @access Private (Chủ sở hữu hoặc Admin) router.put('/:id', protect, async (req, res) => { try { const { name, description, lat, lng, privacy, sharedWithUsers, sharedEmails, shareExpireDays } = req.body; const tour = await Tour.findById(req.params.id); if (!tour) return res.status(404).json({ message: 'Tour không tồn tại.' }); 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 chỉnh sửa Tour này.' }); } tour.name = name || tour.name; tour.description = description !== undefined ? description : tour.description; if (lat !== undefined) tour.location.lat = Number(lat); if (lng !== undefined) tour.location.lng = Number(lng); if (privacy) tour.privacy = privacy; // Xử lý logic Token cho chế độ 'shared' (Link-based) if (tour.privacy === 'shared') { if (!tour.shareToken) tour.shareToken = crypto.randomBytes(24).toString('hex'); if (shareExpireDays && shareExpireDays !== 'never') { const expires = new Date(); expires.setDate(expires.getDate() + parseInt(shareExpireDays)); tour.shareTokenExpires = expires; } else if (shareExpireDays === 'never') { tour.shareTokenExpires = null; } } else { tour.shareToken = undefined; tour.shareTokenExpires = undefined; } // Cập nhật danh sách thành viên được chia sẻ if (tour.privacy === 'member' || tour.privacy === 'shared') { if (sharedWithUsers) { try { tour.sharedWith = JSON.parse(sharedWithUsers); } catch (e) { } } if (sharedEmails) { try { tour.sharedEmails = JSON.parse(sharedEmails); } catch (e) { } } } else if (tour.privacy === 'private') { tour.sharedWith = []; tour.sharedEmails = []; } await tour.save(); // [CORE LOGIC] Lan truyền thiết lập mới xuống toàn bộ các Scene con trong Tour await propagateScenePrivacy(tour._id, { privacy: tour.privacy, shareToken: tour.shareToken, shareTokenExpires: tour.shareTokenExpires, sharedWith: tour.sharedWith, sharedEmails: tour.sharedEmails }, req.user._id); res.json({ message: 'Tour đã được cập nhật thành công.', tour }); } catch (error) { res.status(500).json({ message: error.message }); } }); // @route GET /api/tours/:id // @desc Lấy chi tiết Tour và danh sách các cảnh (Kiểm tra quyền truy cập) // @access Public (Xác thực thông qua Privacy/Token) router.get('/:id', optionalAuth, async (req, res) => { try { const tour = await Tour.findById(req.params.id) .populate('createdBy', 'username') .populate({ path: 'rootSceneId', select: 'assetId', // Chỉ lấy assetId của rootScene populate: { path: 'assetId', select: '_id' } // Populate assetId để lấy _id của Asset }) .populate({ path: 'scenes', select: 'name description assetId gps status privacy shareToken shareTokenExpires sharedWith sharedEmails createdBy', populate: { path: 'assetId', select: '_id' } }) .lean(); 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 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.sharedWith.some(u => u.toString() === req.user._id.toString()) || (userEmail && tour.sharedEmails.includes(userEmail)) )); if (!hasAccess) return res.status(403).json({ message: 'Bạn không có quyền truy cập Tour này.' }); res.json(tour); } catch (error) { res.status(500).json({ message: error.message }); } }); // @route GET /api/tours // @desc Lấy danh sách Tour công khai hoặc của chính mình // @access Public/Private router.get('/', optionalAuth, async (req, res) => { try { let query = { privacy: 'public' }; if (req.user && req.user.role !== 'guest') { query = { $or: [ { privacy: 'public' }, { createdBy: req.user._id }, { privacy: 'member', sharedWith: req.user._id }, { privacy: 'member', sharedEmails: req.user.email } ] }; } const tours = await Tour.find(query) .populate('createdBy', 'username') .populate({ path: 'rootSceneId', select: 'assetId', // Chỉ lấy assetId của rootScene populate: { path: 'assetId', select: '_id' } // Populate assetId để lấy _id của Asset }) .sort({ createdAt: -1 }) .lean(); res.json(tours); } catch (error) { res.status(500).json({ message: error.message }); } }); // @route DELETE /api/tours/:id // @desc Xóa Tour và xóa dây chuyền toàn bộ Scene/Asset bên trong // @access Private (Chủ sở hữu hoặc Admin) router.delete('/:id', protect, async (req, res) => { try { const tour = await Tour.findById(req.params.id); if (!tour) return res.status(404).json({ message: 'Tour không tồn tại.' }); 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 xóa Tour này.' }); } const { deleteSceneCascade } = require('../utils/sceneHelper'); const scenesInTour = await Scene.find({ tourId: tour._id }); for (const scene of scenesInTour) { await deleteSceneCascade(scene._id, req.user._id); } await Tour.findByIdAndDelete(req.params.id); res.json({ message: `Tour "${tour.name}" đã được xóa thành công.` }); } catch (error) { res.status(500).json({ message: error.message }); } }); /** * Tính toán và cập nhật tọa độ trung tâm (location) của Tour * dựa trên giá trị trung bình tọa độ GPS của tất cả các cảnh con hiện có. * @param {string} tourId - ID của Tour cần cập nhật */ const updateTourCenter = async (tourId) => { try { const scenes = await Scene.find({ tourId }).select('gps'); if (!scenes || scenes.length === 0) return; let totalLat = 0; let totalLng = 0; let validCount = 0; scenes.forEach(scene => { // Chỉ tính toán dựa trên các cảnh có tọa độ GPS hợp lệ (khác 0,0) if (scene.gps && typeof scene.gps.lat === 'number' && typeof scene.gps.lng === 'number' && (scene.gps.lat !== 0 || scene.gps.lng !== 0)) { totalLat += scene.gps.lat; totalLng += scene.gps.lng; validCount++; } }); if (validCount > 0) { await Tour.findByIdAndUpdate(tourId, { location: { lat: totalLat / validCount, lng: totalLng / validCount } }, { // Thay thế cho 'new: true' để lấy dữ liệu sau khi cập nhật returnDocument: 'after' }); } } catch (error) { console.error(`[TourController] Error updating center for tour ${tourId}:`, error.message); } }; // @route POST /api/tours/recalculate-all // @desc Admin: Tính toán lại trung tâm cho toàn bộ Tour trong hệ thống // @access Private (Admin only) router.post('/recalculate-all', protect, async (req, res) => { if (req.user.role !== 'admin') { return res.status(403).json({ message: 'Tính năng này chỉ dành cho Quản trị viên.' }); } try { const tours = await Tour.find({}); let processedCount = 0; // Thực hiện tuần tự để tránh gây áp lực quá lớn lên cơ sở dữ liệu for (const tour of tours) { await updateTourCenter(tour._id); processedCount++; } res.json({ message: `Đã hoàn thành tính toán lại trung tâm cho ${processedCount} Tour.`, processedCount }); } catch (error) { res.status(500).json({ message: error.message }); } }); router.updateTourCenter = updateTourCenter; module.exports = router;