thay đổi quyền chia sẻ của user và quyền xem của các scene

This commit is contained in:
2026-06-09 18:18:34 +07:00
parent 18e1c3d76d
commit d243c67718
11 changed files with 204 additions and 22 deletions
+100 -22
View File
@@ -532,7 +532,56 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
return res.status(403).json({ message: 'Access denied to this scene' });
}
res.json(scene);
// Tăng số lượt xem nếu truy cập qua link chia sẻ
if (scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid) {
// Tăng tổng lượt xem
scene.views = (scene.views || 0) + 1;
// Cập nhật lịch sử lượt xem theo ngày
const today = new Date();
today.setHours(0, 0, 0, 0); // Đặt về đầu ngày để nhóm theo ngày
const existingEntry = scene.viewHistory.find(entry =>
entry.date.getTime() === today.getTime()
);
if (existingEntry) {
existingEntry.count = (existingEntry.count || 0) + 1;
} else {
scene.viewHistory.push({ date: today, count: 1 });
}
await scene.save();
}
// Kiểm tra xem scene này có phải là scene con của một hotspot nào đó không
const isChildScene = await Hotspot.exists({ target_scene_id: scene._id });
// Trả về đối tượng scene đã được chuyển đổi sang plain object để thêm thuộc tính
res.json({ ...scene.toObject(), isChildScene: !!isChildScene });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
/**
* @route GET /api/me/scenes/:id/view-stats
* @desc Lấy dữ liệu thống kê lượt xem theo thời gian của một scene
* @access Private (Owner only)
*/
router.get('/me/scenes/:id/view-stats', protect, async (req, res) => {
try {
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 xem thống kê chi tiết
if (scene.createdBy.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Bạn không có quyền xem thống kê này' });
}
res.json(scene.viewHistory.sort((a, b) => a.date - b.date)); // Sắp xếp theo ngày tăng dần
} catch (error) {
res.status(500).json({ message: error.message });
}
@@ -759,7 +808,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
* @desc Update an existing scene
* @access Private (Owner only)
*/
router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res, next) => {
try {
const { title, description, privacy, sharedWithUsers, sharedEmails, shareExpireDays, lat, lng } = req.body;
const scene = await Scene.findById(req.params.id);
@@ -768,6 +817,10 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
return res.status(403).json({ message: 'Not authorized' });
}
// Đảm bảo req.user là một đối tượng thuần túy để ngăn chặn validation/save ngầm định của Mongoose
// Đây là một biện pháp phòng ngừa nếu req.user là một Mongoose document và có middleware khác cố gắng lưu nó.
if (req.user && typeof req.user.toObject === 'function') req.user = req.user.toObject();
const oldPrivacy = scene.privacy;
// Update basic info
@@ -796,29 +849,43 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
// 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());
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');
if (targetSceneIds.length > 0) {
for (const targetId of targetSceneIds) {
const updateData = { privacy: privacy };
let newShareToken = null;
// 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) {
newShareToken = crypto.randomBytes(24).toString('hex');
updateData.shareToken = newShareToken;
// Đặt thời hạn token của scene con giống scene cha nếu có
if (scene.shareTokenExpires) {
updateData.shareTokenExpires = scene.shareTokenExpires;
}
} else if (target && target.shareToken) {
// Nếu scene con đã có token, giữ nguyên
updateData.shareToken = target.shareToken;
if (scene.shareTokenExpires) {
updateData.shareTokenExpires = scene.shareTokenExpires;
} else {
updateData.shareTokenExpires = null;
}
}
await Scene.updateOne({ _id: targetId }, { $set: updateData });
} else {
// Nếu không phải 'shared', xóa token và thời hạn của scene con
updateData.shareToken = null;
updateData.shareTokenExpires = null;
}
console.log(`[Privacy Sync] Cascaded ${privacy} status to ${targetSceneIds.length} linked scenes.`);
await Scene.updateOne({ _id: targetId }, { $set: updateData });
}
} catch (err) {
console.error("Lỗi khi đồng bộ quyền riêng tư cho các scene con:", err.message);
console.log(`[Privacy Sync] Cascaded privacy status to ${targetSceneIds.length} linked scenes.`);
}
}
@@ -857,7 +924,10 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
await scene.save();
res.json({ message: 'Scene updated', scene });
} catch (error) {
res.status(500).json({ message: error.message });
if (error.name === 'ValidationError') {
return res.status(400).json({ message: error.message });
}
next(error); // Chuyển lỗi khác cho middleware xử lý lỗi chung
}
});
@@ -1106,7 +1176,15 @@ router.get('/me/scenes', protect, async (req, res) => {
const scenes = await Scene.find({ createdBy: req.user._id })
.populate('createdBy', 'username')
.populate('assetId')
.sort({ createdAt: -1 });
.select('+views') // Đảm bảo trường 'views' được chọn nếu nó bị ẩn theo mặc định trong schema
.sort({ createdAt: -1 })
.lean(); // Sử dụng .lean() để tăng hiệu suất khi thêm thuộc tính tùy chỉnh
// Kiểm tra xem mỗi scene có phải là scene con hay không
for (let i = 0; i < scenes.length; i++) {
const isChild = await Hotspot.exists({ target_scene_id: scenes[i]._id });
scenes[i].isChildScene = !!isChild;
}
res.json(scenes);
} catch (error) {
res.status(500).json({ message: error.message });
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB