Lỗi có thể di chuyển tù sharedlink sang public nhưng không có hotspot quay lại
This commit is contained in:
@@ -142,13 +142,29 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
||||
const isAdmin = req.user && req.user.role === 'admin';
|
||||
const isTokenValid = tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
const token = req.query.token;
|
||||
|
||||
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
||||
(tour.privacy === 'shared' && req.query.token === tour.shareToken && isTokenValid) ||
|
||||
(tour.privacy === 'member' && req.user && req.user._id && (
|
||||
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
||||
(userEmail && tour.sharedEmails.includes(userEmail))
|
||||
));
|
||||
// [Security] Check permissions based on tour privacy
|
||||
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin;
|
||||
|
||||
// Shared link access - check token validity
|
||||
if (!hasAccess && tour.privacy === 'shared' && token && isTokenValid) {
|
||||
hasAccess = token === tour.shareToken;
|
||||
}
|
||||
|
||||
// Member access - check if user is in sharedWith or sharedEmails
|
||||
if (!hasAccess && tour.privacy === 'member' && req.user && req.user._id && req.user.role !== 'guest') {
|
||||
hasAccess = true;
|
||||
}
|
||||
|
||||
// Fallback for specific shared members
|
||||
if (!hasAccess && tour.privacy === 'member' && req.user && req.user._id) {
|
||||
hasAccess = tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
||||
(userEmail && tour.sharedEmails.some(email => email.toLowerCase() === userEmail.toLowerCase()));
|
||||
}
|
||||
|
||||
// Private tours - only owner and admin
|
||||
// (hasAccess already set above if owner/admin)
|
||||
|
||||
if (!hasAccess) return res.status(403).json({ message: 'Bạn không có quyền truy cập Tour này.' });
|
||||
|
||||
@@ -159,16 +175,18 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// @route GET /api/tours
|
||||
// @desc Lấy danh sách Tour công khai hoặc của chính mình
|
||||
// @desc Lấy danh sách Tour công khai, member, shared (với token), hoặc của chính mình
|
||||
// @access Public/Private
|
||||
router.get('/', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
const { token } = req.query;
|
||||
let query = { privacy: 'public' };
|
||||
|
||||
if (req.user && req.user.role !== 'guest') {
|
||||
query = {
|
||||
$or: [
|
||||
{ privacy: 'public' },
|
||||
{ privacy: 'member' },
|
||||
{ createdBy: req.user._id },
|
||||
{ privacy: 'member', sharedWith: req.user._id },
|
||||
{ privacy: 'member', sharedEmails: req.user.email }
|
||||
@@ -176,6 +194,24 @@ router.get('/', optionalAuth, async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
// [Task 4.1] Support shared tours via token for guests
|
||||
if (token) {
|
||||
const tourWithToken = await Tour.findOne({
|
||||
shareToken: token,
|
||||
privacy: 'shared',
|
||||
$or: [{ shareTokenExpires: null }, { shareTokenExpires: { $gt: new Date() } }]
|
||||
}).select('_id');
|
||||
|
||||
if (tourWithToken) {
|
||||
query = {
|
||||
$or: [
|
||||
query,
|
||||
{ _id: tourWithToken._id }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tours = await Tour.find(query)
|
||||
.populate('createdBy', 'username')
|
||||
.populate({
|
||||
|
||||
@@ -39,6 +39,11 @@ const sceneSchema = new mongoose.Schema({
|
||||
enum: ['processing', 'completed', 'failed'],
|
||||
default: 'processing'
|
||||
},
|
||||
privacy: {
|
||||
type: String,
|
||||
enum: ['public', 'private', 'member', 'shared'],
|
||||
default: 'private'
|
||||
},
|
||||
shareToken: String,
|
||||
shareTokenExpires: Date,
|
||||
sharedWith: [{
|
||||
|
||||
@@ -24,6 +24,8 @@ const uploadDir = process.env.UPLOAD_DIR
|
||||
*/
|
||||
router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res) => {
|
||||
try {
|
||||
console.log(`[AssetView] Đang yêu cầu hiển thị Asset: ${req.params.assetId}. Token: ${req.query.token ? 'Có' : 'Không'}`);
|
||||
|
||||
const asset = await Asset.findById(req.params.assetId);
|
||||
if (!asset) return res.status(404).json({ message: 'Asset not found' });
|
||||
|
||||
@@ -88,7 +90,10 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccess) return res.status(403).json({ message: 'Bạn không được phép di chuyển đến cảnh này' });
|
||||
if (!hasAccess) {
|
||||
console.warn(`[AssetView Denied] Không có quyền xem ảnh cho Asset ${asset._id}`);
|
||||
return res.status(403).json({ message: 'Bạn không được phép di chuyển đến cảnh này' });
|
||||
}
|
||||
}
|
||||
|
||||
// Kiểm tra file vật lý (Giai đoạn 2 - Async)
|
||||
|
||||
@@ -2,15 +2,47 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const Hotspot = require('../models/Hotspot');
|
||||
const Scene = require('../models/Scene');
|
||||
const { protect } = require('../middlewares/authMiddleware');
|
||||
const { protect, optionalAuth } = require('../middlewares/authMiddleware');
|
||||
const { calculateReverseYaw } = require('../utils/hotspotHelper');
|
||||
const Tour = require('../models/Tour');
|
||||
|
||||
/**
|
||||
* @route GET /api/hotspots/:scene_id
|
||||
* @desc Lấy toàn bộ danh sách hotspot của một cảnh
|
||||
*/
|
||||
router.get('/:scene_id', async (req, res) => {
|
||||
router.get('/:scene_id', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
// [SECURITY] Kiểm tra quyền xem hotspots dựa trên quyền truy cập cảnh cha
|
||||
const scene = await Scene.findById(req.params.scene_id).populate('tourId');
|
||||
if (!scene) return res.status(404).json({ message: 'Scene not found' });
|
||||
|
||||
const tour = scene.tourId;
|
||||
const isOwner = req.user && req.user._id && tour?.createdBy?.toString() === req.user._id.toString();
|
||||
const isAdmin = req.user && req.user.role === 'admin';
|
||||
const isSceneTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
const isTourTokenValid = tour?.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
||||
|
||||
let hasAccess = (tour?.privacy === 'public') || (scene.privacy === 'public') || isOwner || isAdmin ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isSceneTokenValid) ||
|
||||
(tour?.privacy === 'shared' && req.query.token === tour.shareToken && isTourTokenValid) ||
|
||||
(tour?.privacy === 'member' && req.user && req.user._id && req.user.role !== 'guest');
|
||||
|
||||
// Bridge Access cho Hotspots
|
||||
if (!hasAccess && req.query.token) {
|
||||
const potentialParents = await Hotspot.find({ target_scene_id: scene._id }).distinct('parent_scene_id');
|
||||
const authorizedParentExists = await Scene.exists({
|
||||
_id: { $in: potentialParents },
|
||||
shareToken: req.query.token,
|
||||
$or: [{ shareTokenExpires: null }, { shareTokenExpires: { $gt: new Date() } }]
|
||||
});
|
||||
if (authorizedParentExists) hasAccess = true;
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
console.warn(`[Backend-Hotspots-Denied] Scene: ${req.params.scene_id}`);
|
||||
return res.status(403).json({ message: 'Bạn không có quyền xem các điểm điều hướng của cảnh này.' });
|
||||
}
|
||||
|
||||
const hotspots = await Hotspot.find({ parent_scene_id: req.params.scene_id })
|
||||
.populate({
|
||||
path: 'target_scene_id',
|
||||
|
||||
@@ -117,7 +117,16 @@ router.get('/', optionalAuth, async (req, res) => {
|
||||
|
||||
// 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'
|
||||
? { $or: [{ privacy: 'public' }, { tourId: { $in: publicTourIds } }, { createdBy: req.user._id }, { sharedWith: req.user._id }, { sharedEmails: req.user.email }] }
|
||||
? {
|
||||
$or: [
|
||||
{ privacy: 'public' },
|
||||
{ privacy: 'member' },
|
||||
{ tourId: { $in: publicTourIds } },
|
||||
{ createdBy: req.user._id },
|
||||
{ sharedWith: req.user._id },
|
||||
{ sharedEmails: req.user.email }
|
||||
]
|
||||
}
|
||||
: { $or: [{ privacy: 'public' }, { tourId: { $in: publicTourIds } }] };
|
||||
|
||||
let finalQuery = baseQuery;
|
||||
@@ -148,6 +157,8 @@ router.get('/', optionalAuth, async (req, res) => {
|
||||
// @route GET /api/scenes/:id
|
||||
router.get('/:id', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
console.log(`[Backend-Scene] Yêu cầu chi tiết: ${req.params.id}. User: ${req.user?._id || 'Guest'}, QueryToken: ${req.query.token || 'N/A'}`);
|
||||
|
||||
const scene = await Scene.findById(req.params.id)
|
||||
.populate('createdBy', 'username')
|
||||
.populate('assetId')
|
||||
@@ -165,18 +176,23 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
||||
const isTourTokenValid = tour.shareToken && (!tour.shareTokenExpires || new Date() < tour.shareTokenExpires);
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
|
||||
let hasAccess = tour.privacy === 'public' || isOwner || isAdmin ||
|
||||
// [FIX] Cho phép truy cập nếu bản thân Scene CÔNG KHAI hoặc Tour CÔNG KHAI
|
||||
let hasAccess = tour.privacy === 'public' || scene.privacy === 'public' || isOwner || isAdmin ||
|
||||
(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 === 'member' && req.user && req.user._id && ( // Access for members
|
||||
(tour.privacy === 'member' && req.user && req.user._id && req.user.role !== 'guest') || // Access for any logged-in member
|
||||
(tour.privacy === 'member' && req.user && req.user._id && ( // Specific shared members (legacy support)
|
||||
tour.sharedWith.some(u => u.toString() === req.user._id.toString()) ||
|
||||
(userEmail && tour.sharedEmails.includes(userEmail))
|
||||
));
|
||||
|
||||
if (req.query.token) {
|
||||
console.log(`[Backend-Auth] Token: ${req.query.token}. Match Scene: ${req.query.token === scene.shareToken}, Match Tour: ${req.query.token === tour.shareToken}, Access: ${hasAccess}`);
|
||||
}
|
||||
|
||||
// [BRIDGE ACCESS LOGIC]
|
||||
// Nếu chưa có quyền, kiểm tra xem người dùng có đến từ một cảnh hợp lệ thuộc Tour khác không
|
||||
if (!hasAccess && req.query.token) {
|
||||
// Tìm tất cả các cảnh (parent) có hotspot trỏ đến cảnh hiện tại
|
||||
const potentialParents = await Hotspot.find({ target_scene_id: scene._id }).distinct('parent_scene_id');
|
||||
if (potentialParents.length > 0) {
|
||||
// Kiểm tra xem có cảnh cha nào sở hữu shareToken này và còn hạn không
|
||||
@@ -185,11 +201,17 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
||||
shareToken: req.query.token,
|
||||
$or: [{ shareTokenExpires: null }, { shareTokenExpires: { $gt: new Date() } }]
|
||||
});
|
||||
if (authorizedParentExists) hasAccess = true;
|
||||
if (authorizedParentExists) {
|
||||
hasAccess = true;
|
||||
console.log(`[Backend-Bridge] Quyền được chấp thuận qua Scene cha.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccess) return res.status(403).json({ message: 'Bạn không được phép di chuyển đến cảnh này' });
|
||||
if (!hasAccess) {
|
||||
console.warn(`[Backend-Denied] Scene: ${scene._id}, TourPrivacy: ${tour.privacy}, ScenePrivacy: ${scene.privacy}`);
|
||||
return res.status(403).json({ message: 'Bạn không có quyền truy cập cảnh này.' });
|
||||
}
|
||||
|
||||
// Increment view count if not owner/admin and not a bot
|
||||
if (!isOwner && !isAdmin && !req.headers['user-agent']?.match(/bot|crawl|spider/i)) {
|
||||
|
||||
Reference in New Issue
Block a user