const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const multer = require('multer'); const Scene = require('../models/Scene'); const Asset = require('../models/Asset'); const Hotspot = require('../models/Hotspot'); const { protect, optionalAuth } = require('../middlewares/authMiddleware'); const { checkQuota } = require('../middlewares/quotaMiddleware'); const { resizeTo8K } = require('../utils/imageHelper'); const { injectGPSCoordinates } = require('../utils/exifHelper'); const { imageQueue } = require('./imageQueue'); const { deleteSceneCascade } = require('../utils/sceneHelper'); const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads'); const tempDir = path.join(uploadDir, 'temp'); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, tempDir), filename: (req, file, cb) => cb(null, `${Date.now()}_${crypto.randomBytes(4).toString('hex')}${path.extname(file.originalname)}`) }); const upload = multer({ storage }); const uploadSinglePanorama = (req, res, next) => { upload.single('panorama')(req, res, (err) => { if (err) return res.status(400).json({ message: err.message }); next(); }); }; // @route POST /api/scenes router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) => { try { const { title, lat, lng, privacy, sharedWithUsers } = req.body; if (!req.file) return res.status(400).json({ message: 'Please upload a panorama image' }); const latitude = Number(lat) || 0; const longitude = Number(lng) || 0; const tempFilePath = req.file.path; const processedFileName = `processed_${req.file.filename}.jpg`; const processedFilePath = path.join(uploadDir, processedFileName); const asset = new Asset({ filePath: tempFilePath, fileSize: req.file.size, uploadedBy: req.user._id, coordinates: { lat: latitude, lng: longitude } }); await asset.save(); let shareToken = privacy === 'shared' ? crypto.randomBytes(24).toString('hex') : undefined; let parsedSharedWith = []; try { if (sharedWithUsers) parsedSharedWith = JSON.parse(sharedWithUsers); } catch (e) {} const scene = new Scene({ name: title, assetId: asset._id, scene_url: tempFilePath, gps: { lat: latitude, lng: longitude }, createdBy: req.user._id, privacy: privacy || 'private', shareToken, sharedWith: parsedSharedWith, status: 'processing' }); await scene.save(); await imageQueue.add('process-panorama', { tempFilePath, processedFilePath, latitude, longitude, assetId: asset._id, sceneId: scene._id }); res.status(201).json({ message: 'Scene đã được tạo! Ảnh đang được xử lý 8K ngầm...', scene }); } catch (error) { if (req.file) await fs.promises.unlink(req.file.path).catch(() => {}); res.status(500).json({ message: error.message }); } }); // @route GET /api/scenes router.get('/', optionalAuth, async (req, res) => { try { let query = req.user ? { $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(); res.json(scenes); } catch (error) { res.status(500).json({ message: error.message }); } }); // @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'); if (!scene) return res.status(404).json({ message: 'Scene not found' }); const isTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires); const userEmail = req.user ? req.user.email : null; const 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); if (!hasAccess) return res.status(403).json({ message: 'Access denied' }); const isChildScene = await Hotspot.exists({ target_scene_id: scene._id }); res.json({ ...scene.toObject(), isChildScene: !!isChildScene }); } catch (error) { res.status(500).json({ message: error.message }); } }); // @route PUT /api/scenes/:id router.put('/:id', protect, uploadSinglePanorama, async (req, res) => { try { const { title, description, privacy, sharedWithUsers, sharedEmails, shareExpireDays, lat, lng } = req.body; const scene = await Scene.findById(req.params.id); if (!scene || scene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Not authorized' }); } const oldPrivacy = scene.privacy; scene.name = title || scene.name; scene.description = description !== undefined ? description : scene.description; scene.privacy = privacy || scene.privacy; if (lat) scene.gps.lat = parseFloat(lat); if (lng) scene.gps.lng = parseFloat(lng); if (sharedWithUsers) try { scene.sharedWith = JSON.parse(sharedWithUsers); } catch (e) {} if (sharedEmails) try { scene.sharedEmails = JSON.parse(sharedEmails); } catch (e) {} if (privacy === 'shared') { if (!scene.shareToken) scene.shareToken = crypto.randomBytes(24).toString('hex'); if (shareExpireDays && shareExpireDays !== 'never') { const expires = new Date(); expires.setDate(expires.getDate() + parseInt(shareExpireDays)); scene.shareTokenExpires = expires; } else if (shareExpireDays === 'never') { scene.shareTokenExpires = null; } } if (req.file) { const processedFileName = `processed_${req.file.filename}.jpg`; const processedFilePath = path.join(uploadDir, processedFileName); await resizeTo8K(req.file.path, processedFilePath); await injectGPSCoordinates(processedFilePath, scene.gps.lat, scene.gps.lng); const asset = new Asset({ filePath: processedFilePath, uploadedBy: req.user._id }); await asset.save(); scene.assetId = asset._id; await fs.promises.unlink(req.file.path).catch(() => {}); } await scene.save(); res.json({ message: 'Scene updated', scene }); } catch (error) { res.status(500).json({ message: error.message }); } }); // @route DELETE /api/scenes/:id router.delete('/:id', protect, async (req, res) => { try { const rootSceneId = req.params.id; const rootScene = await Scene.findById(rootSceneId); if (!rootScene) return res.status(404).json({ message: 'Scene không tồn tại' }); if (req.user.role !== 'admin' && rootScene.createdBy.toString() !== req.user._id.toString()) { return res.status(403).json({ message: 'Forbidden' }); } const { deletedCount } = await deleteSceneCascade(rootSceneId, req.user.username); res.json({ message: deletedCount > 1 ? `Đã xóa scene cha và ${deletedCount - 1} scene con liên quan.` : 'Đã xóa scene thành công.' }); } catch (error) { res.status(500).json({ message: error.message }); } }); module.exports = router;