Files

191 lines
7.9 KiB
JavaScript

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;