Sử dụng antigravity cli để sửa lỗi người dùng public không nhìn thấy tour chia sẻ
This commit is contained in:
@@ -137,14 +137,15 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
|||||||
|
|
||||||
if (!tour) return res.status(404).json({ message: 'Tour không tồn tại.' });
|
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 tourCreatedById = tour.createdBy?._id || tour.createdBy;
|
||||||
|
const isOwner = req.user && req.user._id && tourCreatedById && tourCreatedById.toString() === req.user._id.toString();
|
||||||
const isAdmin = req.user && req.user.role === 'admin';
|
const isAdmin = req.user && req.user.role === 'admin';
|
||||||
const isTokenValid = tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
const isTokenValid = tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
||||||
const userEmail = req.user ? req.user.email : null;
|
const userEmail = req.user ? req.user.email : null;
|
||||||
|
|
||||||
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
||||||
(tour.privacy === 'shared' && req.query.token === tour.shareToken && isTokenValid) ||
|
(tour.privacy === 'shared' && req.query.token === tour.shareToken && isTokenValid) ||
|
||||||
(tour.privacy === 'member' && req.user && (
|
(tour.privacy === 'member' && req.user && req.user._id && (
|
||||||
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
||||||
(userEmail && tour.sharedEmails.includes(userEmail))
|
(userEmail && tour.sharedEmails.includes(userEmail))
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
|
|||||||
|
|
||||||
let hasAccess = isAdmin ||
|
let hasAccess = isAdmin ||
|
||||||
scene.privacy === 'public' ||
|
scene.privacy === 'public' ||
|
||||||
|
(tour && tour.privacy === 'public') ||
|
||||||
(scene.privacy === 'member' && userIdStr && (scene.sharedWith.some(id => id.toString() === userIdStr) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
(scene.privacy === 'member' && userIdStr && (scene.sharedWith.some(id => id.toString() === userIdStr) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||||
isOwner ||
|
isOwner ||
|
||||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) ||
|
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) ||
|
||||||
|
|||||||
@@ -111,10 +111,14 @@ router.get('/', optionalAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { token } = req.query;
|
const { token } = req.query;
|
||||||
|
|
||||||
|
// [FIX] Lấy danh sách ID của các Tour đang ở chế độ công khai
|
||||||
|
const publicTours = await Tour.find({ privacy: 'public' }).select('_id');
|
||||||
|
const publicTourIds = publicTours.map(t => t._id);
|
||||||
|
|
||||||
// Quyền cơ bản: Công khai hoặc là chủ sở hữu/thành viên được chia sẻ
|
// Quyền cơ bản: Công khai hoặc là chủ sở hữu/thành viên được chia sẻ
|
||||||
let baseQuery = req.user && req.user.role !== 'guest'
|
let baseQuery = req.user && req.user.role !== 'guest'
|
||||||
? { $or: [{ privacy: 'public' }, { createdBy: req.user._id }, { sharedWith: req.user._id }, { sharedEmails: req.user.email }] }
|
? { $or: [{ privacy: 'public' }, { tourId: { $in: publicTourIds } }, { createdBy: req.user._id }, { sharedWith: req.user._id }, { sharedEmails: req.user.email }] }
|
||||||
: { privacy: 'public' };
|
: { $or: [{ privacy: 'public' }, { tourId: { $in: publicTourIds } }] };
|
||||||
|
|
||||||
let finalQuery = baseQuery;
|
let finalQuery = baseQuery;
|
||||||
|
|
||||||
@@ -130,7 +134,11 @@ router.get('/', optionalAuth, async (req, res) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const scenes = await Scene.find(finalQuery).populate('createdBy', 'username').lean();
|
console.log(`[SceneRoutes] GET /api/scenes - Final Query for user ${req.user?._id || 'Guest'}:`, JSON.stringify(finalQuery));
|
||||||
|
const scenes = await Scene.find(finalQuery)
|
||||||
|
.populate('createdBy', 'username')
|
||||||
|
.populate('tourId') // Nạp thông tin Tour để Frontend nhận diện
|
||||||
|
.lean();
|
||||||
res.json(scenes);
|
res.json(scenes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: error.message });
|
res.status(500).json({ message: error.message });
|
||||||
@@ -150,7 +158,7 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
|||||||
const tour = scene.tourId; // tourId is populated
|
const tour = scene.tourId; // tourId is populated
|
||||||
if (!tour) return res.status(404).json({ message: 'Tour liên kết không tồn tại.' });
|
if (!tour) return res.status(404).json({ message: 'Tour liên kết không tồn tại.' });
|
||||||
|
|
||||||
const isOwner = req.user && tour.createdBy?.toString() === req.user._id.toString();
|
const isOwner = req.user && req.user._id && tour.createdBy?.toString() === req.user._id.toString();
|
||||||
const isAdmin = req.user && req.user.role === 'admin';
|
const isAdmin = req.user && req.user.role === 'admin';
|
||||||
|
|
||||||
const isSceneTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
const isSceneTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||||
@@ -160,7 +168,7 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
|||||||
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
||||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) || // Access via scene's token
|
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) || // Access via scene's token
|
||||||
(tour.privacy === 'shared' && req.query.token === tour.shareToken && isTourTokenValid) || // Access via tour's token
|
(tour.privacy === 'shared' && req.query.token === tour.shareToken && isTourTokenValid) || // Access via tour's token
|
||||||
(tour.privacy === 'member' && req.user && ( // Access for members
|
(tour.privacy === 'member' && req.user && req.user._id && ( // Access for members
|
||||||
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
||||||
(userEmail && tour.sharedEmails.includes(userEmail))
|
(userEmail && tour.sharedEmails.includes(userEmail))
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ describe('TourController - updateTourCenter', () => {
|
|||||||
// Trung bình: lat (10+20+30)/3 = 20, lng (20+40+60)/3 = 40
|
// Trung bình: lat (10+20+30)/3 = 20, lng (20+40+60)/3 = 40
|
||||||
expect(Tour.findByIdAndUpdate).toHaveBeenCalledWith(tourId, {
|
expect(Tour.findByIdAndUpdate).toHaveBeenCalledWith(tourId, {
|
||||||
location: { lat: 20.0, lng: 40.0 }
|
location: { lat: 20.0, lng: 40.0 }
|
||||||
|
}, {
|
||||||
|
returnDocument: 'after'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,6 +58,8 @@ describe('TourController - updateTourCenter', () => {
|
|||||||
// Chỉ tính 2 cảnh hợp lệ: lat (10+20)/2 = 15, lng (20+40)/2 = 30
|
// Chỉ tính 2 cảnh hợp lệ: lat (10+20)/2 = 15, lng (20+40)/2 = 30
|
||||||
expect(Tour.findByIdAndUpdate).toHaveBeenCalledWith(tourId, {
|
expect(Tour.findByIdAndUpdate).toHaveBeenCalledWith(tourId, {
|
||||||
location: { lat: 15.0, lng: 30.0 }
|
location: { lat: 15.0, lng: 30.0 }
|
||||||
|
}, {
|
||||||
|
returnDocument: 'after'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+53
-10
@@ -954,6 +954,7 @@ async function loadScenes(urlToken = null) {
|
|||||||
if (!response.ok) throw new Error('Failed to load scenes');
|
if (!response.ok) throw new Error('Failed to load scenes');
|
||||||
const scenes = await response.json();
|
const scenes = await response.json();
|
||||||
|
|
||||||
|
console.log(`[Frontend] loadScenes received ${scenes.length} scenes. User role: ${localStorage.getItem('role') || 'Guest'}`);
|
||||||
console.log(`[Data] Nhận được ${scenes.length} scenes từ server`);
|
console.log(`[Data] Nhận được ${scenes.length} scenes từ server`);
|
||||||
if (!Array.isArray(scenes)) return;
|
if (!Array.isArray(scenes)) return;
|
||||||
|
|
||||||
@@ -971,13 +972,16 @@ async function loadScenes(urlToken = null) {
|
|||||||
const lngNum = Number(scene.gps?.lng ?? scene.lng);
|
const lngNum = Number(scene.gps?.lng ?? scene.lng);
|
||||||
|
|
||||||
if (isNaN(latNum) || isNaN(lngNum)) {
|
if (isNaN(latNum) || isNaN(lngNum)) {
|
||||||
console.error(`Bỏ qua Scene "${scene.name || scene.title}" do tọa độ lỗi:`, scene);
|
console.warn(`[Frontend] Bỏ qua Scene "${scene.name || scene.title}" (ID: ${scene._id}) do tọa độ lỗi:`, scene);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Logic lọc Ảnh mẹ: Sửa lỗi typo coordKey (dùng latNum 2 lầ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)}`;
|
const coordKey = `${latNum.toFixed(6)},${lngNum.toFixed(6)}`;
|
||||||
if (seenCoordinates.has(coordKey)) return;
|
if (seenCoordinates.has(coordKey)) {
|
||||||
|
console.log(`[Frontend] Bỏ qua Scene "${scene.name || scene.title}" (ID: ${scene._id}) do trùng tọa độ (hotspot con).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
seenCoordinates.add(coordKey);
|
seenCoordinates.add(coordKey);
|
||||||
|
|
||||||
// 3. Truy cập Asset an toàn
|
// 3. Truy cập Asset an toàn
|
||||||
@@ -985,6 +989,7 @@ async function loadScenes(urlToken = null) {
|
|||||||
if (!assetId) return; // Bỏ qua nếu không có ảnh liên kết
|
if (!assetId) return; // Bỏ qua nếu không có ảnh liên kết
|
||||||
|
|
||||||
const sceneName = scene.name || scene.title || "Untitled Scene";
|
const sceneName = scene.name || scene.title || "Untitled Scene";
|
||||||
|
console.log(`[Frontend] Đang thêm marker cho Scene: ${sceneName} (ID: ${scene._id}, Privacy: ${scene.privacy})`);
|
||||||
|
|
||||||
const isProcessing = scene.status === 'processing';
|
const isProcessing = scene.status === 'processing';
|
||||||
if (isProcessing) foundProcessing++;
|
if (isProcessing) foundProcessing++;
|
||||||
@@ -1053,7 +1058,7 @@ async function loadScenes(urlToken = null) {
|
|||||||
const ownerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
const ownerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
||||||
|
|
||||||
// Phân quyền: Admin hoặc Chủ sở hữu Scene
|
// Phân quyền: Admin hoặc Chủ sở hữu Scene
|
||||||
const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu';
|
const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu' || userRole === 'moderator';
|
||||||
const isOwner = currentUserId && ownerId && ownerId.toString() === currentUserId.toString();
|
const isOwner = currentUserId && ownerId && ownerId.toString() === currentUserId.toString();
|
||||||
|
|
||||||
if (isAdmin || isOwner) {
|
if (isAdmin || isOwner) {
|
||||||
@@ -1099,8 +1104,13 @@ async function handleEditDeleteScene(scene) {
|
|||||||
const editPrivacyBtn = document.getElementById('btn-edit-privacy-action');
|
const editPrivacyBtn = document.getElementById('btn-edit-privacy-action');
|
||||||
const deleteBtn = document.getElementById('btn-delete-action');
|
const deleteBtn = document.getElementById('btn-delete-action');
|
||||||
const shareBtn = document.getElementById('btn-share-action');
|
const shareBtn = document.getElementById('btn-share-action');
|
||||||
|
const desc = document.getElementById('action-modal-desc');
|
||||||
|
|
||||||
|
const tour = scene.tourId; // Tour đã được populate từ backend
|
||||||
|
|
||||||
|
title.innerText = tour ? `Tour: ${tour.name}` : `Scene: ${scene.title}`;
|
||||||
|
desc.innerText = tour ? "Bạn muốn thực hiện thao tác gì với Tour này?" : "Bạn muốn thực hiện thao tác gì với scene này?";
|
||||||
|
|
||||||
title.innerText = `Scene: ${scene.title}`;
|
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
// Hành động Lấy link chia sẻ trực tiếp
|
// Hành động Lấy link chia sẻ trực tiếp
|
||||||
@@ -1113,22 +1123,38 @@ async function handleEditDeleteScene(scene) {
|
|||||||
editPrivacyBtn.onclick = () => {
|
editPrivacyBtn.onclick = () => {
|
||||||
returnToDashboardAfterEdit = false;
|
returnToDashboardAfterEdit = false;
|
||||||
closeActionModal();
|
closeActionModal();
|
||||||
// Sử dụng thuộc tính isChildScene từ backend để quyết định quyền chỉnh sửa
|
if (tour) {
|
||||||
openEditMetadataModal(scene, scene.isChildScene);
|
openEditTourModal(tour);
|
||||||
|
} else {
|
||||||
|
openEditMetadataModal(scene, scene.isChildScene);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gán sự kiện cho nút Sửa
|
// Cập nhật nhãn và sự kiện cho nút Sửa
|
||||||
|
editBtn.innerHTML = tour ? '<span class="icon">✏️</span> Sửa thông tin Tour' : '<span class="icon">✏️</span> Chế độ sửa scene';
|
||||||
editBtn.onclick = () => {
|
editBtn.onclick = () => {
|
||||||
returnToDashboardAfterEdit = false;
|
returnToDashboardAfterEdit = false;
|
||||||
closeActionModal();
|
closeActionModal();
|
||||||
openEditMetadataModal(scene, scene.isChildScene);
|
if (tour) {
|
||||||
|
openEditTourModal(tour);
|
||||||
|
} else {
|
||||||
|
openEditMetadataModal(scene, scene.isChildScene);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gán sự kiện cho nút Xóa
|
// Cập nhật nhãn và sự kiện cho nút Xóa
|
||||||
|
deleteBtn.innerHTML = tour ? '<span class="icon">🗑️</span> Xóa vĩnh viễn Tour' : '<span class="icon">🗑️</span> Xóa vĩnh viễn';
|
||||||
deleteBtn.onclick = () => {
|
deleteBtn.onclick = () => {
|
||||||
returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map
|
returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map
|
||||||
closeActionModal();
|
closeActionModal();
|
||||||
deleteScene(scene._id);
|
if (tour) {
|
||||||
|
// Tái sử dụng logic xóa tour từ dashboard
|
||||||
|
if (confirm(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ cảnh bên trong?`)) {
|
||||||
|
confirmDeleteTourFromMap(tour._id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deleteScene(scene._id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,6 +1165,23 @@ function closeActionModal() {
|
|||||||
document.getElementById('action-choice-modal').style.display = 'none';
|
document.getElementById('action-choice-modal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xóa Tour trực tiếp từ Map
|
||||||
|
*/
|
||||||
|
async function confirmDeleteTourFromMap(tourId) {
|
||||||
|
const token = localStorage.getItem('jwt');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/tours/${tourId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
showNotification("Đã xóa Tour thành công", "success");
|
||||||
|
loadScenes(); // Tải lại bản đồ
|
||||||
|
}
|
||||||
|
} catch (e) { showNotification("Lỗi xóa tour", "error"); }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mở modal xác nhận xóa scene
|
* Mở modal xác nhận xóa scene
|
||||||
*/
|
*/
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 MiB |
Reference in New Issue
Block a user