20260607 - login, add scene, add hotspot

This commit is contained in:
2026-06-07 21:31:31 +07:00
parent 10d2e07297
commit 5ba6e37039
29 changed files with 1064 additions and 73 deletions
+163 -17
View File
@@ -69,25 +69,29 @@ router.post('/scenes', protect, upload.single('panorama'), async (req, res) => {
const processedFileName = `processed_${req.file.filename}.jpg`;
const processedFilePath = path.join(uploadDir, processedFileName);
// 1. Process and resize image to 8K JPEG (8192x4096)
await resizeTo8K(tempFilePath, processedFilePath);
// 2. Analyze EXIF GPS from original file
// Lấy tọa độ GPS gốc từ ảnh vừa upload trước khi nén/xử lý
const originalGPS = await getGPSCoordinates(tempFilePath);
// 3. Inject Map Lat/Lng coordinates into processed 8K file binary EXIF
await injectGPSCoordinates(processedFilePath, latitude, longitude);
// 4. Remove original temp file
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath);
}
// BACKGROUND PROCESSING: Thực hiện song song không chặn response
setImmediate(async () => {
try {
// 1. Resize to 8K
await resizeTo8K(tempFilePath, processedFilePath);
// 2. Inject GPS
await injectGPSCoordinates(processedFilePath, latitude, longitude);
// 3. Cleanup temp file
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
console.log(`Background processing finished for: ${processedFileName}`);
} catch (err) {
console.error(`Background Image processing failed: ${err.message}`);
}
});
// 5. Save Asset to DB
const asset = new Asset({
filePath: processedFilePath,
uploadedBy: req.user._id,
coordinates: originalGPS ? { lat: originalGPS.lat, lng: originalGPS.lng } : undefined
coordinates: originalGPS ? { lat: originalGPS.lat, lng: originalGPS.lng } : { lat: latitude, lng: longitude }
});
await asset.save();
@@ -120,7 +124,7 @@ router.post('/scenes', protect, upload.single('panorama'), async (req, res) => {
});
await scene.save();
res.status(201).json({
res.status(202).json({
message: 'Scene created successfully',
scene
});
@@ -181,7 +185,7 @@ router.get('/scenes', optionalAuth, async (req, res) => {
*/
router.get('/scenes/:id', optionalAuth, async (req, res) => {
try {
const scene = await Scene.findById(req.id || req.params.id)
const scene = await Scene.findById(req.params.id)
.populate('owner', 'username')
.populate('assetId');
@@ -206,12 +210,68 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
}
});
/**
* @route POST /api/scenes/:id/hotspots
* @desc Add a new hotspot to a scene
* @access Private (Owner only)
*/
router.post('/scenes/:id/hotspots', protect, async (req, res) => {
try {
const { hotspotId, pitch, yaw, text, description, targetSceneId } = req.body;
const scene = await Scene.findById(req.params.id);
if (!scene) {
return res.status(404).json({ message: 'Scene not found' });
}
// Chỉ chủ sở hữu mới có quyền chỉnh sửa hotspots
if (scene.owner.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Access denied: Only the owner can add hotspots' });
}
if (hotspotId) {
// CẬP NHẬT HOTSPOT HIỆN CÓ
const hs = scene.hotspots.id(hotspotId);
if (!hs) return res.status(404).json({ message: 'Hotspot not found' });
hs.pitch = parseFloat(pitch) ?? hs.pitch;
hs.yaw = parseFloat(yaw) ?? hs.yaw;
hs.text = text ?? hs.text;
hs.description = description ?? hs.description;
hs.targetSceneId = targetSceneId ?? hs.targetSceneId;
} else {
// THÊM MỚI HOTSPOT
const newHotspot = {
pitch: parseFloat(pitch),
yaw: parseFloat(yaw),
text: text || '',
description: description || '',
targetSceneId: targetSceneId || undefined
};
if (isNaN(newHotspot.pitch) || isNaN(newHotspot.yaw)) {
return res.status(400).json({ message: 'Invalid coordinates' });
}
scene.hotspots.push(newHotspot);
}
await scene.save();
res.status(201).json({
message: 'Hotspot added successfully',
hotspots: scene.hotspots
});
} catch (error) {
res.status(500).json({ message: error.message });
}
});
/**
* @route GET /api/assets/view/:assetId
* @desc Securely stream panorama images (prevents direct link / unauthorized downloads)
* @access Public / Private + Referer Verification + Token validation
*/
router.get('/assets/view/:assetId', verifyReferer, setNoCacheHeaders, optionalAuth, async (req, res) => {
router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res) => {
try {
const asset = await Asset.findById(req.params.assetId);
if (!asset) {
@@ -242,12 +302,98 @@ router.get('/assets/view/:assetId', verifyReferer, setNoCacheHeaders, optionalAu
return res.status(404).json({ message: 'Physical file not found on disk' });
}
// Stream file securely
res.sendFile(path.resolve(asset.filePath));
const resolvedPath = path.resolve(asset.filePath);
// Sử dụng res.sendFile để tối ưu hóa việc truyền tải file lớn và hỗ trợ Caching (ETag)
res.sendFile(resolvedPath, {
maxAge: 2592000000, // 30 ngày (tính bằng ms)
lastModified: true,
headers: {
'Content-Type': 'image/jpeg',
'Content-Disposition': 'inline; filename="panorama.jpg"',
'Cache-Control': 'public, max-age=2592000, immutable' // Buộc trình duyệt lấy từ cache mà không cần hỏi lại server
}
});
} catch (error) {
res.status(500).json({ message: error.message });
}
});
/**
* @route PUT /api/scenes/:id
* @desc Update an existing scene
* @access Private (Owner only)
*/
router.put('/scenes/:id', protect, upload.single('panorama'), async (req, res) => {
try {
const { title, privacy, sharedWithUsers, lat, lng } = req.body;
const scene = await Scene.findById(req.params.id);
if (!scene || scene.owner.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Not authorized' });
}
// Update basic info
scene.title = title || scene.title;
scene.privacy = privacy || scene.privacy;
scene.lat = lat ? parseFloat(lat) : scene.lat;
scene.lng = lng ? parseFloat(lng) : scene.lng;
if (privacy === 'shared' && !scene.shareToken) {
scene.shareToken = crypto.randomBytes(24).toString('hex');
}
// Update image if new one is uploaded
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.lat, scene.lng);
const asset = new Asset({
filePath: processedFilePath,
uploadedBy: req.user._id
});
await asset.save();
scene.assetId = asset._id;
if (fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
}
await scene.save();
res.json({ message: 'Scene updated', scene });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
/**
* @route DELETE /api/scenes/:id
* @desc Delete a scene and its assets
* @access Private (Owner only)
*/
router.delete('/scenes/:id', protect, async (req, res) => {
try {
const scene = await Scene.findById(req.params.id);
if (!scene || scene.owner.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Not authorized' });
}
// Delete physical file if exists
const asset = await Asset.findById(scene.assetId);
if (asset && fs.existsSync(asset.filePath)) {
fs.unlinkSync(asset.filePath);
}
await Asset.findByIdAndDelete(scene.assetId);
await Scene.findByIdAndDelete(req.params.id);
res.json({ message: 'Scene deleted successfully' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;