Sửa chế độ chia sẻ privacy trực tiếp khi nhấn chuột phải lên scene

This commit is contained in:
2026-06-08 19:56:46 +07:00
parent a2263b9005
commit d1aa2209a7
5 changed files with 564 additions and 16 deletions
+11
View File
@@ -9,6 +9,10 @@ const sceneSchema = new mongoose.Schema({
scene_url: {
type: String
},
description: {
type: String,
trim: true
},
assetId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Asset',
@@ -33,10 +37,17 @@ const sceneSchema = new mongoose.Schema({
unique: true,
sparse: true
},
shareTokenExpires: {
type: Date
},
sharedWith: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
sharedEmails: [{
type: String,
trim: true
}],
}, {
timestamps: true
});
+73 -8
View File
@@ -165,6 +165,33 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
}
});
/**
* @route GET /api/users/search
* @desc Tìm kiếm người dùng theo username hoặc email để chia sẻ
* @access Private
*/
router.get('/users/search', protect, async (req, res) => {
const query = req.query.q;
if (!query || query.length < 2) return res.json([]);
try {
const users = await User.find({
$and: [
{ _id: { $ne: req.user._id } }, // Không tìm chính mình
{
$or: [
{ username: { $regex: query, $options: 'i' } },
{ email: { $regex: query, $options: 'i' } }
]
}
]
}).select('username email').limit(10);
res.json(users);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
/**
* @route GET /api/scenes
* @desc Get all accessible scenes for the map (respecting privacy rules)
@@ -183,7 +210,8 @@ router.get('/scenes', optionalAuth, async (req, res) => {
{ privacy: 'member' },
{ privacy: 'shared' }, // shareToken will be required to fetch panorama, but coordinates show on map
{ createdBy: req.user._id },
{ sharedWith: req.user._id }
{ sharedWith: req.user._id },
{ sharedEmails: req.user.email }
]
};
} else {
@@ -223,12 +251,17 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
return res.status(404).json({ message: 'Scene not found' });
}
// Kiểm tra Token hết hạn
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.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()) ||
(req.user && scene.sharedWith.includes(req.user._id)) ||
(scene.privacy === 'shared' && req.query.token === scene.shareToken);
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
if (!hasAccess) {
return res.status(403).json({ message: 'Access denied to this scene' });
@@ -382,12 +415,17 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
return res.status(403).json({ message: 'Access denied' });
}
} else {
// Kiểm tra Token hết hạn
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.privacy === 'member' && req.user && (scene.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
(req.user && scene.createdBy.toString() === req.user._id.toString()) ||
(req.user && scene.sharedWith.includes(req.user._id)) ||
(scene.privacy === 'shared' && req.query.token === scene.shareToken);
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
if (!hasAccess) {
return res.status(403).json({ message: 'Access denied: You do not have permission to view this asset' });
@@ -423,7 +461,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
*/
router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
try {
const { title, privacy, sharedWithUsers, lat, lng } = req.body;
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()) {
@@ -434,10 +472,27 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
// Update basic info
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);
// Cập nhật danh sách chia sẻ
if (sharedWithUsers) {
try {
scene.sharedWith = JSON.parse(sharedWithUsers);
} catch (e) {
console.error("Lỗi parse sharedWithUsers:", e);
}
}
if (sharedEmails) {
try {
scene.sharedEmails = JSON.parse(sharedEmails);
} catch (e) {
console.error("Lỗi parse sharedEmails:", e);
}
}
// 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) {
@@ -467,8 +522,18 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
}
}
if (privacy === 'shared' && !scene.shareToken) {
scene.shareToken = crypto.randomBytes(24).toString('hex');
if (privacy === 'shared') {
if (!scene.shareToken) {
scene.shareToken = crypto.randomBytes(24).toString('hex');
}
// Thiết lập ngày hết hạn nếu có truyền lên
if (shareExpireDays && shareExpireDays !== 'never') {
const expires = new Date();
expires.setDate(expires.getDate() + parseInt(shareExpireDays));
scene.shareTokenExpires = expires;
} else if (shareExpireDays === 'never') {
scene.shareTokenExpires = null;
}
}
// Update image if new one is uploaded