sửa lỗi privacy cho nhiều scene con, không cho phép ghi hoặc nhận sai
@@ -52,7 +52,7 @@ Tài liệu này tổng hợp toàn bộ cấu trúc hệ thống phục vụ qu
|
|||||||
|
|
||||||
## 2. Danh sách API Endpoints
|
## 2. Danh sách API Endpoints
|
||||||
|
|
||||||
### Quản trị hệ thống (Admin Only)
|
### Quản trị hệ thống (AdminController.js & SystemController.js)
|
||||||
- `POST /api/admin/backup`: Xuất toàn bộ DB và Uploads (Zip)
|
- `POST /api/admin/backup`: Xuất toàn bộ DB và Uploads (Zip)
|
||||||
- `POST /api/admin/restore`: Khôi phục hệ thống từ file Zip
|
- `POST /api/admin/restore`: Khôi phục hệ thống từ file Zip
|
||||||
- `GET /api/admin/maintenance/stray-files`: Tìm file rác (không có trong DB)
|
- `GET /api/admin/maintenance/stray-files`: Tìm file rác (không có trong DB)
|
||||||
@@ -60,8 +60,10 @@ Tài liệu này tổng hợp toàn bộ cấu trúc hệ thống phục vụ qu
|
|||||||
- `GET /api/admin/users`: Quản lý danh sách người dùng (Phân trang)
|
- `GET /api/admin/users`: Quản lý danh sách người dùng (Phân trang)
|
||||||
- `PUT /api/admin/users/:id`: Cập nhật User (Quyền, Password...)
|
- `PUT /api/admin/users/:id`: Cập nhật User (Quyền, Password...)
|
||||||
- `DELETE /api/admin/users/:id`: Xóa User và dọn dẹp data liên quan
|
- `DELETE /api/admin/users/:id`: Xóa User và dọn dẹp data liên quan
|
||||||
|
- `GET /api/system/settings`: Lấy cấu hình (Timezone, Lang)
|
||||||
|
- `PUT /api/system/settings`: Cập nhật cấu hình hệ thống
|
||||||
|
|
||||||
### Cảnh 3D (Scenes)
|
### Cảnh 3D (SceneController.js)
|
||||||
- `POST /api/scenes`: Tạo mới (Upload ảnh, Resize 8K, Inject GPS)
|
- `POST /api/scenes`: Tạo mới (Upload ảnh, Resize 8K, Inject GPS)
|
||||||
- `GET /api/scenes`: Lấy danh sách hiển thị trên bản đồ (Theo quyền truy cập)
|
- `GET /api/scenes`: Lấy danh sách hiển thị trên bản đồ (Theo quyền truy cập)
|
||||||
- `GET /api/scenes/:id`: Chi tiết một cảnh
|
- `GET /api/scenes/:id`: Chi tiết một cảnh
|
||||||
@@ -69,31 +71,26 @@ Tài liệu này tổng hợp toàn bộ cấu trúc hệ thống phục vụ qu
|
|||||||
- `DELETE /api/scenes/:id`: Xóa Scene và các scene con liên kết (BFS)
|
- `DELETE /api/scenes/:id`: Xóa Scene và các scene con liên kết (BFS)
|
||||||
- `GET /api/share/:sceneId`: Trang trung gian hỗ trợ Open Graph (FB/Zalo)
|
- `GET /api/share/:sceneId`: Trang trung gian hỗ trợ Open Graph (FB/Zalo)
|
||||||
|
|
||||||
### Điểm điều hướng (Hotspots)
|
### Điểm điều hướng (HotspotController.js)
|
||||||
- `GET /api/hotspots/:scene_id`: Lấy danh sách điểm tương tác của cảnh
|
- `GET /api/hotspots/:scene_id`: Lấy danh sách điểm tương tác của cảnh
|
||||||
- `POST /api/hotspots/create`: Tạo mới (Hỗ trợ tự động tạo link ngược)
|
- `POST /api/hotspots/create`: Tạo mới (Hỗ trợ tự động tạo link ngược)
|
||||||
- `PUT /api/hotspots/update/:id`: Cập nhật vị trí/tiêu đề
|
- `PUT /api/hotspots/update/:id`: Cập nhật vị trí/tiêu đề
|
||||||
- `DELETE /api/hotspots/delete/:id`: Xóa hotspot
|
- `DELETE /api/hotspots/delete/:id`: Xóa hotspot
|
||||||
|
|
||||||
### Tài sản & Media (Assets)
|
### Tài sản & Media (AssetController.js)
|
||||||
- `GET /api/assets/view/:assetId`: Stream ảnh panorama (Có Referer & Token Verification)
|
- `GET /api/assets/view/:assetId`: Stream ảnh panorama (Có Referer & Token Verification)
|
||||||
- `GET /api/assets/view_avatar/:filename`: Stream ảnh đại diện
|
- `GET /api/assets/view_avatar/:filename`: Stream ảnh đại diện
|
||||||
- `GET /api/me/assets`: Kho ảnh của tôi
|
- `GET /api/me/assets`: Kho ảnh của tôi
|
||||||
- `DELETE /api/assets/:id`: Xóa file vật lý và bản ghi
|
- `DELETE /api/assets/:id`: Xóa file vật lý và bản ghi
|
||||||
- `GET /api/me/assets/top-large`: Thống kê file chiếm dung lượng lớn
|
- `GET /api/me/assets/top-large`: Thống kê file chiếm dung lượng lớn
|
||||||
|
|
||||||
### Người dùng & Hồ sơ (User Profile)
|
### Người dùng & Xác thực (AuthController.js & UserController.js)
|
||||||
- `POST /api/auth/register`: Đăng ký tài khoản
|
- `POST /api/auth/register`: Đăng ký tài khoản
|
||||||
- `POST /api/auth/login`: Đăng nhập (Trả về JWT)
|
- `POST /api/auth/login`: Đăng nhập (Trả về JWT)
|
||||||
- `GET /api/me/profile`: Thông tin cá nhân & Quota lưu trữ
|
- `GET /api/me/profile`: Thông tin cá nhân & Quota lưu trữ
|
||||||
- `PUT /api/me/profile`: Cập nhật hồ sơ & Avatar
|
- `PUT /api/me/profile`: Cập nhật hồ sơ & Avatar
|
||||||
- `GET /api/users/search`: Tìm kiếm người dùng để chia sẻ
|
- `GET /api/users/search`: Tìm kiếm người dùng để chia sẻ
|
||||||
|
|
||||||
### Hệ thống (System)
|
|
||||||
- `GET /api/system/settings`: Lấy cấu hình (Timezone, Lang)
|
|
||||||
- `PUT /api/system/settings`: Cập nhật cấu hình hệ thống
|
|
||||||
- `POST /api/maintenance/reset-all`: Xóa sạch dữ liệu (Dev only)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Trung tâm Xử lý Hình ảnh (Backend Worker)
|
## 3. Trung tâm Xử lý Hình ảnh (Backend Worker)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const { checkQuota } = require('../middlewares/quotaMiddleware');
|
|||||||
const { resizeTo8K } = require('../utils/imageHelper');
|
const { resizeTo8K } = require('../utils/imageHelper');
|
||||||
const { injectGPSCoordinates } = require('../utils/exifHelper');
|
const { injectGPSCoordinates } = require('../utils/exifHelper');
|
||||||
const { imageQueue } = require('./imageQueue');
|
const { imageQueue } = require('./imageQueue');
|
||||||
const { deleteSceneCascade } = require('../utils/sceneHelper');
|
const { deleteSceneCascade, propagateScenePrivacy } = require('../utils/sceneHelper');
|
||||||
|
|
||||||
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads');
|
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads');
|
||||||
const tempDir = path.join(uploadDir, 'temp');
|
const tempDir = path.join(uploadDir, 'temp');
|
||||||
@@ -108,7 +108,11 @@ router.get('/:id', optionalAuth, async (req, res) => {
|
|||||||
|
|
||||||
if (!hasAccess) return res.status(403).json({ message: 'Access denied' });
|
if (!hasAccess) return res.status(403).json({ message: 'Access denied' });
|
||||||
|
|
||||||
const isChildScene = await Hotspot.exists({ target_scene_id: scene._id });
|
// Một cảnh chỉ được coi là cảnh con nếu có hotspot đi tới (không phải link quay lại) trỏ đến nó
|
||||||
|
const isChildScene = await Hotspot.exists({
|
||||||
|
target_scene_id: scene._id,
|
||||||
|
is_auto_return: { $ne: true }
|
||||||
|
});
|
||||||
res.json({ ...scene.toObject(), isChildScene: !!isChildScene });
|
res.json({ ...scene.toObject(), isChildScene: !!isChildScene });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: error.message });
|
res.status(500).json({ message: error.message });
|
||||||
@@ -125,6 +129,18 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
return res.status(403).json({ message: 'Not authorized' });
|
return res.status(403).json({ message: 'Not authorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [BẢO MẬT] Kiểm tra nếu là cảnh con thì chặn thay đổi Privacy
|
||||||
|
// Chỉ chặn nếu cảnh này là đích đến của một luồng điều hướng đi xuôi
|
||||||
|
const isChild = await Hotspot.exists({
|
||||||
|
target_scene_id: req.params.id,
|
||||||
|
is_auto_return: { $ne: true }
|
||||||
|
});
|
||||||
|
if (isChild && privacy && privacy !== scene.privacy) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: "Cảnh này thuộc một tour. Vui lòng thay đổi quyền riêng tư tại Cảnh gốc để đồng bộ."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const oldPrivacy = scene.privacy;
|
const oldPrivacy = scene.privacy;
|
||||||
scene.name = title || scene.name;
|
scene.name = title || scene.name;
|
||||||
scene.description = description !== undefined ? description : scene.description;
|
scene.description = description !== undefined ? description : scene.description;
|
||||||
@@ -132,10 +148,29 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
if (lat) scene.gps.lat = parseFloat(lat);
|
if (lat) scene.gps.lat = parseFloat(lat);
|
||||||
if (lng) scene.gps.lng = parseFloat(lng);
|
if (lng) scene.gps.lng = parseFloat(lng);
|
||||||
|
|
||||||
if (sharedWithUsers) try { scene.sharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
// [BẢO MẬT] Chỉ duy trì shareToken ở chế độ 'shared'.
|
||||||
if (sharedEmails) try { scene.sharedEmails = JSON.parse(sharedEmails); } catch (e) {}
|
// Gán undefined để Mongoose xóa trường này khỏi DB khi save.
|
||||||
|
if (scene.privacy !== 'shared') {
|
||||||
|
scene.shareToken = undefined; // Mongoose sẽ xóa field này khỏi document
|
||||||
|
scene.shareTokenExpires = undefined; // Mướng sẽ xóa field này khỏi document
|
||||||
|
// Nếu không phải 'member', xóa luôn danh sách chia sẻ người dùng
|
||||||
|
if (scene.privacy !== 'member') {
|
||||||
|
scene.sharedWith = [];
|
||||||
|
scene.sharedEmails = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (privacy === 'shared') {
|
if (scene.privacy !== 'private') {
|
||||||
|
// Cập nhật danh sách chia sẻ nếu không phải chế độ Private
|
||||||
|
if (sharedWithUsers) {
|
||||||
|
try { scene.sharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
|
||||||
|
}
|
||||||
|
if (sharedEmails) {
|
||||||
|
try { scene.sharedEmails = JSON.parse(sharedEmails); } catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.privacy === 'shared') {
|
||||||
if (!scene.shareToken) scene.shareToken = crypto.randomBytes(24).toString('hex');
|
if (!scene.shareToken) scene.shareToken = crypto.randomBytes(24).toString('hex');
|
||||||
if (shareExpireDays && shareExpireDays !== 'never') {
|
if (shareExpireDays && shareExpireDays !== 'never') {
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
@@ -159,7 +194,13 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await scene.save();
|
await scene.save();
|
||||||
res.json({ message: 'Scene updated', scene });
|
|
||||||
|
// [CASCADING] Lan truyền Privacy xuống các cảnh con nếu đây là cảnh cha
|
||||||
|
if (!isChild) {
|
||||||
|
await propagateScenePrivacy(scene._id, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Cập nhật thành công và đã đồng bộ quyền riêng tư cho các cảnh liên quan.', scene });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: error.message });
|
res.status(500).json({ message: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,4 +78,76 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
|
|||||||
return { deletedCount: scenesToDelete.length };
|
return { deletedCount: scenesToDelete.length };
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { deleteSceneCascade };
|
/**
|
||||||
|
* Lan truyền thiết lập quyền riêng tư từ Scene cha xuống TOÀN BỘ các Scene con trong Tour (Đệ quy/BFS).
|
||||||
|
* Đảm bảo tính nhất quán của toàn bộ Tour khi thay đổi quyền truy cập.
|
||||||
|
* @param {string} parentSceneId - ID của Scene cha vừa được cập nhật
|
||||||
|
* @param {Object} privacyData - Dữ liệu quyền riêng tư từ Scene cha (privacy, tokens, v.v.)
|
||||||
|
*/
|
||||||
|
const propagateScenePrivacy = async (parentSceneId, privacyData) => {
|
||||||
|
const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData;
|
||||||
|
|
||||||
|
// 1. Tìm tất cả các scene con ở mọi cấp độ bằng thuật toán BFS
|
||||||
|
let queue = [parentSceneId.toString()];
|
||||||
|
let allChildIds = [];
|
||||||
|
const visited = new Set(queue);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentId = queue.shift();
|
||||||
|
// Tìm các hotspots xuất phát từ scene hiện tại (bỏ qua link quay lại để tránh vòng lặp)
|
||||||
|
const hotspots = await Hotspot.find({
|
||||||
|
parent_scene_id: currentId,
|
||||||
|
is_auto_return: { $ne: true }
|
||||||
|
}).select('target_scene_id');
|
||||||
|
|
||||||
|
for (const hs of hotspots) {
|
||||||
|
if (hs.target_scene_id) {
|
||||||
|
const targetIdStr = hs.target_scene_id.toString();
|
||||||
|
if (!visited.has(targetIdStr)) {
|
||||||
|
visited.add(targetIdStr);
|
||||||
|
allChildIds.push(targetIdStr);
|
||||||
|
queue.push(targetIdStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChildIds.length === 0) return;
|
||||||
|
|
||||||
|
// 2. Chuẩn bị dữ liệu cập nhật đồng bộ cho toàn bộ chuỗi
|
||||||
|
const updateFields = { privacy };
|
||||||
|
const updateQuery = { $set: updateFields };
|
||||||
|
|
||||||
|
if (privacy === 'shared') {
|
||||||
|
// Chỉ gán nếu có giá trị, nếu không thì xóa hẳn để tránh lỗi Duplicate Key Null
|
||||||
|
if (shareToken) {
|
||||||
|
updateFields.shareToken = shareToken;
|
||||||
|
} else {
|
||||||
|
if (!updateQuery.$unset) updateQuery.$unset = {};
|
||||||
|
updateQuery.$unset.shareToken = 1;
|
||||||
|
}
|
||||||
|
updateFields.shareTokenExpires = shareTokenExpires || undefined;
|
||||||
|
updateFields.sharedWith = sharedWith || [];
|
||||||
|
updateFields.sharedEmails = sharedEmails || [];
|
||||||
|
} else {
|
||||||
|
// [BẢO MẬT] Xóa hoàn toàn token cho mọi chế độ không phải 'shared' để tránh lỗi Duplicate Key Null
|
||||||
|
if (!updateQuery.$unset) updateQuery.$unset = {};
|
||||||
|
updateQuery.$unset.shareToken = 1;
|
||||||
|
updateQuery.$unset.shareTokenExpires = 1;
|
||||||
|
// Nếu là private hoặc public, xóa luôn danh sách thành viên được chia sẻ
|
||||||
|
if (privacy !== 'member') {
|
||||||
|
updateFields.sharedWith = [];
|
||||||
|
updateFields.sharedEmails = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Cập nhật hàng loạt cho tất cả các scene con được tìm thấy
|
||||||
|
await Scene.updateMany(
|
||||||
|
{ _id: { $in: allChildIds } },
|
||||||
|
updateQuery
|
||||||
|
);
|
||||||
|
|
||||||
|
await logActivity('PROPAGATE_PRIVACY_DEEP', { parentSceneId, childCount: allChildIds.length, privacy }, 'System');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { deleteSceneCascade, propagateScenePrivacy };
|
||||||
@@ -1052,6 +1052,16 @@ html, body {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Trạng thái vô hiệu hóa (Disabled) cho các trường nhập liệu trong Modal sửa */
|
||||||
|
#edit-scene-metadata-modal input:disabled,
|
||||||
|
#edit-scene-metadata-modal textarea:disabled,
|
||||||
|
#edit-scene-metadata-modal select:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.02) !important;
|
||||||
|
color: #777 !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tùy chỉnh màu sắc cho danh sách lựa chọn (dropdown options) trong modal tối */
|
/* 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 {
|
#edit-scene-metadata-modal select option {
|
||||||
background-color: #000; /* Nền đen cho các item */
|
background-color: #000; /* Nền đen cho các item */
|
||||||
|
|||||||
@@ -1098,12 +1098,22 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
|
|||||||
|
|
||||||
const scene = await sceneRes.json();
|
const scene = await sceneRes.json();
|
||||||
const hotspots = await hotspotsRes.json();
|
const hotspots = await hotspotsRes.json();
|
||||||
console.log("DEBUG: Hotspots raw data from API:", hotspots);
|
|
||||||
|
|
||||||
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
||||||
|
|
||||||
// Lấy ID người tạo (createdBy) để phân quyền chuột phải trong viewer
|
// [FIX CRITICAL] Kiểm tra bảo mật Client-side:
|
||||||
|
// Nếu scene là private, chỉ cho phép chủ sở hữu xem (ngay cả khi backend vô tình trả về dữ liệu qua token cũ)
|
||||||
const sceneOwnerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
const sceneOwnerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
||||||
|
const currentUserId = localStorage.getItem('userId');
|
||||||
|
const userRole = localStorage.getItem('role');
|
||||||
|
const isOwner = currentUserId && sceneOwnerId && currentUserId.toString() === sceneOwnerId.toString();
|
||||||
|
const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu';
|
||||||
|
|
||||||
|
if (scene.privacy === 'private' && !isOwner && !isAdmin) {
|
||||||
|
throw new Error("Cảnh này đã được chủ sở hữu chuyển sang chế độ riêng tư.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("DEBUG: Hotspots raw data from API:", hotspots);
|
||||||
|
|
||||||
// Tự động focus bản đồ vào vị trí của Scene
|
// Tự động focus bản đồ vào vị trí của Scene
|
||||||
if (map) {
|
if (map) {
|
||||||
@@ -2012,6 +2022,7 @@ window.openEditFromMedia = function(scene, isChild = false) {
|
|||||||
window.openEditMetadataModal = function(scene, isChildArg = null) {
|
window.openEditMetadataModal = function(scene, isChildArg = null) {
|
||||||
// Ưu tiên isChildScene từ object scene, hoặc giá trị truyền vào thủ công
|
// Ưu tiên isChildScene từ object scene, hoặc giá trị truyền vào thủ công
|
||||||
const isChild = isChildArg !== null ? isChildArg : (!!scene.isChildScene);
|
const isChild = isChildArg !== null ? isChildArg : (!!scene.isChildScene);
|
||||||
|
const modalTitle = document.getElementById('edit-metadata-modal-title');
|
||||||
|
|
||||||
currentEditingScene = scene; // Lưu lại để dùng cho chia sẻ
|
currentEditingScene = scene; // Lưu lại để dùng cho chia sẻ
|
||||||
// Load dữ liệu chia sẻ hiện tại
|
// Load dữ liệu chia sẻ hiện tại
|
||||||
@@ -2035,10 +2046,13 @@ window.openEditMetadataModal = function(scene, isChildArg = null) {
|
|||||||
privacySelect.value = scene.privacy;
|
privacySelect.value = scene.privacy;
|
||||||
privacySelect.disabled = true;
|
privacySelect.disabled = true;
|
||||||
childInfo.style.display = 'block';
|
childInfo.style.display = 'block';
|
||||||
|
if (modalTitle) modalTitle.innerText = "Chi tiết Cảnh con (Kế thừa)";
|
||||||
|
childInfo.innerHTML = `ℹ️ Cảnh này thuộc một tour. Quyền riêng tư được quản lý bởi Cảnh gốc.`;
|
||||||
} else {
|
} else {
|
||||||
privacySelect.value = scene.privacy;
|
privacySelect.value = scene.privacy;
|
||||||
privacySelect.disabled = false;
|
privacySelect.disabled = false;
|
||||||
childInfo.style.display = 'none';
|
childInfo.style.display = 'none';
|
||||||
|
if (modalTitle) modalTitle.innerText = "Sửa 3D Scene (Cảnh gốc)";
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditPrivacyChange(); // Cập nhật hiển thị nút bánh răng
|
handleEditPrivacyChange(); // Cập nhật hiển thị nút bánh răng
|
||||||
@@ -2241,10 +2255,18 @@ async function submitEditScene(e) {
|
|||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.message);
|
if (!res.ok) throw new Error(data.message);
|
||||||
|
|
||||||
showNotification("Đã cập nhật thông tin cảnh thành công!", 'success');
|
// [FIX CRITICAL] Nếu chuyển sang Private, xóa token lưu cục bộ nếu là scene đang xem
|
||||||
|
const newPrivacy = document.getElementById('edit-modal-privacy').value;
|
||||||
|
if (newPrivacy === 'private' && localStorage.getItem('activeSceneId') === id) {
|
||||||
|
localStorage.removeItem('activeSceneToken');
|
||||||
|
localStorage.setItem('activeScenePrivacy', 'private');
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification("Cập nhật thành công! Các liên kết chia sẻ cũ đã bị vô hiệu hóa.", 'success');
|
||||||
closeEditMetadataModal();
|
closeEditMetadataModal();
|
||||||
loadScenes();
|
loadScenes();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 6.9 MiB After Width: | Height: | Size: 6.9 MiB |
|
Before Width: | Height: | Size: 5.7 MiB After Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |