Tạo liên kết chia sẻ lên social
This commit is contained in:
@@ -5,6 +5,14 @@ const { URL } = require('url');
|
|||||||
* from the official app domain (SYSTEM_HOST). Blocks direct URL access.
|
* from the official app domain (SYSTEM_HOST). Blocks direct URL access.
|
||||||
*/
|
*/
|
||||||
const verifyReferer = (req, res, next) => {
|
const verifyReferer = (req, res, next) => {
|
||||||
|
// Cho phép các Bot của mạng xã hội truy cập để lấy ảnh thumbnail
|
||||||
|
const userAgent = req.headers['user-agent'] || '';
|
||||||
|
const isSocialBot = /facebookexternalhit|Facebot|ZaloBot|Twitterbot|Slackbot|LinkedInBot|Embedly/i.test(userAgent);
|
||||||
|
|
||||||
|
if (isSocialBot) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const referer = req.headers.referer;
|
const referer = req.headers.referer;
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
const systemHost = process.env.SYSTEM_HOST || 'http://localhost:5000';
|
const systemHost = process.env.SYSTEM_HOST || 'http://localhost:5000';
|
||||||
|
|||||||
@@ -229,6 +229,68 @@ router.get('/scenes', optionalAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/share/:sceneId
|
||||||
|
* @desc Endpoint tạo trang trung gian hỗ trợ hiển thị ảnh thumbnail trên Facebook/Zalo (Open Graph)
|
||||||
|
*/
|
||||||
|
router.get('/share/:sceneId', optionalAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const scene = await Scene.findById(req.params.sceneId).populate('assetId');
|
||||||
|
if (!scene) return res.status(404).send('Không tìm thấy Scene');
|
||||||
|
|
||||||
|
// Kiểm tra quyền truy cập (sử dụng logic đồng bộ với các route khác)
|
||||||
|
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.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||||
|
(req.user && scene.createdBy.toString() === req.user._id.toString()) ||
|
||||||
|
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
|
||||||
|
|
||||||
|
if (!hasAccess) return res.status(403).send('Bạn không có quyền xem liên kết này hoặc liên kết đã hết hạn');
|
||||||
|
|
||||||
|
// Xây dựng các thông số Open Graph
|
||||||
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||||
|
const host = req.get('host');
|
||||||
|
const siteUrl = `${protocol}://${host}`;
|
||||||
|
const assetId = scene.assetId?._id || scene.assetId;
|
||||||
|
const thumbUrl = `${siteUrl}/api/assets/view/${assetId}${req.query.token ? '?token=' + req.query.token : ''}`;
|
||||||
|
|
||||||
|
// Trả về HTML chứa Meta Tags và Script chuyển hướng
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>${scene.name}</title>
|
||||||
|
<meta property="og:title" content="${scene.name}" />
|
||||||
|
<meta property="og:description" content="${scene.description || 'Khám phá tour 3D thực tế ảo sinh động'}" />
|
||||||
|
<meta property="og:image" content="${thumbUrl}" />
|
||||||
|
<meta property="og:image:secure_url" content="${thumbUrl}" />
|
||||||
|
<meta property="og:image:type" content="image/jpeg" />
|
||||||
|
<meta property="og:url" content="${siteUrl}${req.originalUrl}" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="${scene.name}" />
|
||||||
|
<meta name="twitter:description" content="${scene.description || 'Khám phá tour 3D thực tế ảo sinh động'}" />
|
||||||
|
<meta name="twitter:image" content="${thumbUrl}" />
|
||||||
|
<script>
|
||||||
|
// Tự động chuyển hướng người dùng về ứng dụng chính (SPA) kèm tham số
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const token = urlParams.get('token');
|
||||||
|
window.location.href = "/?sceneId=${scene._id}" + (token ? "&token=" + token : "");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body style="background:#000; color:#fff; display:flex; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
|
||||||
|
<p>Đang mở tour 3D của bạn...</p>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
res.send(html);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route GET /api/scenes/:id
|
* @route GET /api/scenes/:id
|
||||||
* @desc Get single scene detail (respecting privacy rules)
|
* @desc Get single scene detail (respecting privacy rules)
|
||||||
|
|||||||
+13
-3
@@ -293,7 +293,7 @@ html, body {
|
|||||||
|
|
||||||
#user-dropdown .btn-group button:hover, #user-dropdown button:hover {
|
#user-dropdown .btn-group button:hover, #user-dropdown button:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1); /* Hiệu ứng hover nhẹ */
|
background-color: rgba(255, 255, 255, 0.1); /* Hiệu ứng hover nhẹ */
|
||||||
color: #fff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user-dropdown button:last-child {
|
#user-dropdown button:last-child {
|
||||||
@@ -880,6 +880,16 @@ html, body {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tùy chỉnh màu sắc cho danh sách lựa chọn (dropdown options) trong modal tối */
|
||||||
|
#edit-scene-metadata-modal select option {
|
||||||
|
background-color: #000; /* Nền đen cho các item */
|
||||||
|
color: #fff; /* Chữ trắng */
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-scene-metadata-modal select option:hover {
|
||||||
|
background-color: #555; /* Nền xám khi di chuột qua (tùy trình duyệt hỗ trợ) */
|
||||||
|
}
|
||||||
|
|
||||||
#edit-mini-map {
|
#edit-mini-map {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -911,7 +921,7 @@ html, body {
|
|||||||
.admin-table input, .admin-table select {
|
.admin-table input, .admin-table select {
|
||||||
background: rgba(255, 255, 255, 0.05) !important;
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||||
color: #fff !important;
|
color: #ffffff !important;
|
||||||
padding: 5px !important;
|
padding: 5px !important;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -921,7 +931,7 @@ html, body {
|
|||||||
.privacy-settings-btn {
|
.privacy-settings-btn {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
color: white;
|
color: rgb(255, 255, 255);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
+32
-7
@@ -717,8 +717,13 @@ async function loadScenes() {
|
|||||||
|
|
||||||
// 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';
|
||||||
if (isAdmin || (currentUserId && ownerId && ownerId.toString() === currentUserId.toString())) {
|
const isOwner = currentUserId && ownerId && ownerId.toString() === currentUserId.toString();
|
||||||
|
|
||||||
|
if (isAdmin || isOwner) {
|
||||||
handleEditDeleteScene(scene);
|
handleEditDeleteScene(scene);
|
||||||
|
} else if (scene.privacy === 'public') {
|
||||||
|
// Cho phép bất kỳ ai (kể cả khách) lấy link chia sẻ của scene công khai
|
||||||
|
showShareLink(scene);
|
||||||
} else {
|
} else {
|
||||||
showNotification("Bạn không có quyền chỉnh sửa scene này.", 'warning');
|
showNotification("Bạn không có quyền chỉnh sửa scene này.", 'warning');
|
||||||
}
|
}
|
||||||
@@ -917,6 +922,12 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
|
|||||||
// Initialize 3D Viewer with secure, referer-protected image stream
|
// Initialize 3D Viewer with secure, referer-protected image stream
|
||||||
initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId, initialPitch, initialYaw);
|
initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId, initialPitch, initialYaw);
|
||||||
|
|
||||||
|
// Sau khi mở thành công từ URL trực tiếp, xóa tham số để làm sạch thanh địa chỉ (URL chuyên nghiệp)
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.has('sceneId')) {
|
||||||
|
window.history.replaceState({}, document.title, "/");
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (typeof closeViewer === 'function') closeViewer();
|
if (typeof closeViewer === 'function') closeViewer();
|
||||||
|
|
||||||
@@ -929,7 +940,7 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
|
|||||||
if (urlParams.has('sceneId')) {
|
if (urlParams.has('sceneId')) {
|
||||||
showNotification("Bạn không có quyền truy cập hoặc liên kết chia sẻ đã hết hạn. Quay về bản đồ công cộng.", 'error');
|
showNotification("Bạn không có quyền truy cập hoặc liên kết chia sẻ đã hết hạn. Quay về bản đồ công cộng.", 'error');
|
||||||
// Xóa toàn bộ tham số URL và tải lại trang để làm mới trạng thái (về trang chủ dành cho khách)
|
// Xóa toàn bộ tham số URL và tải lại trang để làm mới trạng thái (về trang chủ dành cho khách)
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
window.history.replaceState({}, document.title, "/");
|
||||||
location.reload();
|
location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1763,14 +1774,28 @@ window.openPrivacySettingsModal = function() {
|
|||||||
if (privacy === 'member') {
|
if (privacy === 'member') {
|
||||||
renderSharedList();
|
renderSharedList();
|
||||||
document.getElementById('share-member-modal').style.display = 'flex';
|
document.getElementById('share-member-modal').style.display = 'flex';
|
||||||
} else if (privacy === 'shared') {
|
} else if (privacy === 'shared' || privacy === 'public') {
|
||||||
const baseUrl = window.location.origin + window.location.pathname;
|
showShareLink(currentEditingScene);
|
||||||
const token = currentEditingScene.shareToken || 'đang_tạo_mới...';
|
|
||||||
document.getElementById('shared-link-input').value = `${baseUrl}?sceneId=${currentEditingScene._id}&token=${token}`;
|
|
||||||
document.getElementById('share-link-modal').style.display = 'flex';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hiển thị modal lấy link chia sẻ (dùng cho cả khách và chủ sở hữu)
|
||||||
|
* @param {Object} scene - Đối tượng Scene cần lấy link
|
||||||
|
*/
|
||||||
|
window.showShareLink = function(scene) {
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
// Lưu lại scene đang tương tác để các logic phụ trợ hoạt động đồng bộ
|
||||||
|
currentEditingScene = scene;
|
||||||
|
|
||||||
|
// Trỏ link vào endpoint /api/share/ để hỗ trợ Open Graph (ảnh thumbnail Facebook/Zalo)
|
||||||
|
const baseUrl = window.location.origin + '/api/share/';
|
||||||
|
const token = scene.shareToken || '';
|
||||||
|
document.getElementById('shared-link-input').value = `${baseUrl}${scene._id}${token ? '?token=' + token : ''}`;
|
||||||
|
document.getElementById('share-link-modal').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tìm kiếm người dùng để chia sẻ
|
* Tìm kiếm người dùng để chia sẻ
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user