Sửa lỗi quyền sở hữu scene

This commit is contained in:
2026-06-08 12:21:22 +07:00
parent d9ed8032d3
commit 90abf2418a
4 changed files with 61 additions and 45 deletions
+45 -33
View File
@@ -78,8 +78,9 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
return res.status(400).json({ message: 'Please upload a panorama image' });
}
const latitude = Number(lat);
const longitude = Number(lng);
// Đảm bảo ép kiểu Number tuyệt đối trước khi lưu DB
const latitude = Number(lat) || 0;
const longitude = Number(lng) || 0;
if (isNaN(latitude) || isNaN(longitude)) {
// Cleanup uploaded file on validation error
@@ -136,7 +137,6 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
// 7. Save Scene to DB
const scene = new Scene({
name: title,
assetId: asset._id,
scene_url: processedFilePath, // Lưu đường dẫn ảnh trực tiếp
gps: {
lat: latitude,
@@ -260,9 +260,7 @@ router.post('/hotspots/create', protect, async (req, res) => {
const { parent_scene_id, target_scene_id, title, description, coordinates } = req.body;
const parentScene = await Scene.findById(parent_scene_id);
// Phân quyền: Admin hoặc Người tạo Scene
const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString());
if (!parentScene || !isAuthorized) {
if (!parentScene || parentScene.createdBy.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Không có quyền tạo hotspot cho scene này' });
}
@@ -272,8 +270,8 @@ router.post('/hotspots/create', protect, async (req, res) => {
title,
description,
coordinates: {
yaw: Number(coordinates.yaw),
pitch: Number(coordinates.pitch)
yaw: Number(coordinates?.yaw) || 0,
pitch: Number(coordinates?.pitch) || 0
},
is_auto_return: false
});
@@ -283,14 +281,10 @@ router.post('/hotspots/create', protect, async (req, res) => {
const targetScene = await Scene.findById(target_scene_id);
if (targetScene) {
const reverseYaw = coordinates.yaw > 0 ? coordinates.yaw - 180 : coordinates.yaw + 180;
// Fallback đa tầng cho tiêu đề quay lại
const backLabel = title || parentScene.name || parentScene.title || 'cảnh trước';
const reverseHotspot = new Hotspot({
parent_scene_id: target_scene_id,
target_scene_id: parent_scene_id,
title: `Quay lại ${backLabel}`,
title: `Quay lại ${parentScene.name}`,
coordinates: { yaw: reverseYaw, pitch: 0 },
is_auto_return: true
});
@@ -315,20 +309,13 @@ router.put('/hotspots/update/:id', protect, async (req, res) => {
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
const parentScene = await Scene.findById(hotspot.parent_scene_id);
// Phân quyền Admin hoặc Owner
const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString());
if (!isAuthorized) {
if (parentScene.createdBy.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Không có quyền cập nhật' });
}
if (title) hotspot.title = title;
if (description) hotspot.description = description;
if (coordinates) {
hotspot.coordinates = {
yaw: Number(coordinates.yaw),
pitch: Number(coordinates.pitch)
};
}
if (coordinates) hotspot.coordinates = coordinates;
await hotspot.save();
res.json(hotspot);
@@ -347,9 +334,7 @@ router.delete('/hotspots/delete/:id', protect, async (req, res) => {
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
const parentScene = await Scene.findById(hotspot.parent_scene_id);
// Phân quyền Admin hoặc Owner
const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString());
if (!isAuthorized) {
if (parentScene.createdBy.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Không có quyền xóa' });
}
@@ -432,17 +417,46 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
const { title, privacy, sharedWithUsers, lat, lng } = req.body;
const scene = await Scene.findById(req.params.id);
// Phân quyền Admin hoặc Owner
const isAuthorized = req.user.role === 'Chủ sở hữu' || (scene && scene.createdBy.toString() === req.user._id.toString());
if (!scene || !isAuthorized) {
if (!scene || scene.createdBy.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Not authorized' });
}
const oldPrivacy = scene.privacy;
// Update basic info
scene.name = title || scene.name;
scene.privacy = privacy || scene.privacy;
if (lat) scene.gps.lat = Number(lat);
if (lng) scene.gps.lng = Number(lng);
if (lat) scene.gps.lat = parseFloat(lat);
if (lng) scene.gps.lng = parseFloat(lng);
// LOGIC ĐỒNG BỘ QUYỀN RIÊNG TƯ (CASCADING PRIVACY)
// Nếu quyền chia sẻ thay đổi, cập nhật toàn bộ các Scene con liên kết trực tiếp
if (privacy && privacy !== oldPrivacy) {
try {
const linkedHotspots = await Hotspot.find({ parent_scene_id: scene._id });
const targetSceneIds = linkedHotspots
.map(h => h.target_scene_id)
.filter(id => id && id.toString() !== scene._id.toString());
if (targetSceneIds.length > 0) {
for (const targetId of targetSceneIds) {
const updateData = { privacy: privacy };
// Nếu chuyển sang 'shared', đảm bảo scene con cũng có token riêng
if (privacy === 'shared') {
const target = await Scene.findById(targetId);
if (target && !target.shareToken) {
updateData.shareToken = crypto.randomBytes(24).toString('hex');
}
}
await Scene.updateOne({ _id: targetId }, { $set: updateData });
}
console.log(`[Privacy Sync] Cascaded ${privacy} status to ${targetSceneIds.length} linked scenes.`);
}
} catch (err) {
console.error("Lỗi khi đồng bộ quyền riêng tư cho các scene con:", err.message);
}
}
if (privacy === 'shared' && !scene.shareToken) {
scene.shareToken = crypto.randomBytes(24).toString('hex');
@@ -481,9 +495,7 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
router.delete('/scenes/:id', protect, async (req, res) => {
try {
const scene = await Scene.findById(req.params.id);
// Phân quyền Admin hoặc Owner
const isAuthorized = req.user.role === 'Chủ sở hữu' || (scene && scene.createdBy.toString() === req.user._id.toString());
if (!scene || !isAuthorized) {
if (!scene || scene.createdBy.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Not authorized' });
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

+16 -12
View File
@@ -107,10 +107,9 @@ function initMap() {
return;
}
// Chỉ Admin (Chủ sở hữu) mới có quyền tạo Scene mới trực tiếp trên map qua contextmenu
const userRole = localStorage.getItem('role');
const isAdmin = userRole === 'Chủ sở hữu' || userRole === 'admin';
if (!isAdmin) return;
// Cho phép bất kỳ người dùng nào đã đăng nhập tạo Scene mới trên bản đồ
const token = localStorage.getItem('jwt');
if (!token) return;
const { lat, lng } = e.latlng;
openCreateSceneModal(lat, lng);
@@ -370,20 +369,25 @@ async function loadScenes() {
// Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ
scenes.forEach((scene) => {
// Ép kiểu tọa độ về Number để tránh lỗi render bản đồ
const latNum = Number(scene.gps?.lat || scene.lat);
const lngNum = Number(scene.gps?.lng || scene.lng);
// 1. Kiểm tra tọa độ an toàn - Ngăn chặn treo map do NaN
const latNum = Number(scene.gps?.lat ?? scene.lat);
const lngNum = Number(scene.gps?.lng ?? scene.lng);
if (isNaN(latNum) || isNaN(lngNum)) return;
if (isNaN(latNum) || isNaN(lngNum)) {
console.error(`Bỏ qua Scene "${scene.name || scene.title}" do tọa độ lỗi:`, scene);
return;
}
// Logic lọc Ảnh mẹ: Mỗi tọa độ GPS chỉ tạo duy nhất 1 Marker đại diện
// 2. Logic lọc Ảnh mẹ: Sửa lỗi typo coordKey (dùng latNum 2 lần)
const coordKey = `${latNum.toFixed(6)},${lngNum.toFixed(6)}`;
if (seenCoordinates.has(coordKey)) return; // Bỏ qua nếu tọa độ này đã có Marker
if (seenCoordinates.has(coordKey)) return;
seenCoordinates.add(coordKey);
// Kiểm tra an toàn dữ liệu từ MongoDB trước khi truy cập
// 3. Truy cập Asset an toàn
const assetId = scene.assetId?._id || scene.assetId;
const sceneName = scene.name || scene.title;
if (!assetId) return; // Bỏ qua nếu không có ảnh liên kết
const sceneName = scene.name || scene.title || "Untitled Scene";
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
if (token) thumbUrl += `?token=${token}`;