Thay đổi ARCHITEC.md cập nhật các thông tin để chuẩn bị refactor lại dự án

This commit is contained in:
2026-06-10 17:13:56 +07:00
parent ec7a9186b6
commit 3f1b31b233
17 changed files with 326 additions and 139 deletions
+49 -16
View File
@@ -34,16 +34,43 @@ const uploadSinglePanorama = (req, res, next) => {
// @route POST /api/scenes
router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) => {
try {
const { title, lat, lng, privacy, sharedWithUsers, tourId } = req.body;
const { title, lat, lng, privacy, sharedWithUsers, sharedEmails, shareExpireDays, tourId } = req.body;
if (!req.file) return res.status(400).json({ message: 'Please upload a panorama image' });
// [BẢO MẬT] Làm sạch tourId từ client gửi lên
const cleanedTourId = (tourId && tourId !== 'null' && tourId !== '') ? tourId : undefined;
// [BẢO MẬT] Xác định quan hệ: Nếu có tourId thì là "Con đẻ", nếu không là "Gốc"
const cleanedTourId = (tourId && tourId !== 'null' && tourId !== 'undefined' && tourId !== '') ? tourId : undefined;
let finalPrivacy = privacy || 'private';
let finalSharedWith = [];
let finalSharedEmails = [];
let finalShareToken = undefined;
let finalExpires = undefined;
let assignedTourId = cleanedTourId; // Biến tạm để lưu tourId cuối cùng được gán
try { if (sharedWithUsers) finalSharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
// [BẢO MẬT] Xác thực tourId nếu được cung cấp
if (cleanedTourId) {
const tourExists = await Scene.exists({ _id: cleanedTourId });
if (!tourExists) return res.status(400).json({ message: 'Tour gốc không tồn tại hoặc đã bị xóa.' });
const rootScene = await Scene.findById(cleanedTourId);
if (!rootScene) return res.status(400).json({ message: 'Tour gốc không tồn tại hoặc đã bị xóa.' });
// [SECURITY] Chỉ cho phép gán tourId nếu người dùng hiện tại là chủ sở hữu của cảnh gốc đó
if (rootScene.createdBy.toString() !== req.user._id.toString()) {
// Nếu không phải chủ sở hữu, cảnh mới này sẽ tự làm gốc của chính nó
assignedTourId = undefined;
} else {
// [ENFORCE INHERITANCE] Cảnh con bắt buộc kế thừa toàn bộ cấu hình từ cảnh gốc
finalPrivacy = rootScene.privacy;
finalSharedWith = rootScene.sharedWith;
finalSharedEmails = rootScene.sharedEmails;
finalShareToken = rootScene.shareToken;
finalExpires = rootScene.shareTokenExpires;
}
} else {
// Nếu là cảnh gốc mới, tạo token nếu chế độ là shared
if (finalPrivacy === 'shared') {
finalShareToken = crypto.randomBytes(24).toString('hex');
}
}
const latitude = Number(lat) || 0;
@@ -60,21 +87,19 @@ router.post('/', protect, uploadSinglePanorama, checkQuota, async (req, res) =>
});
await asset.save();
let shareToken = privacy === 'shared' ? crypto.randomBytes(24).toString('hex') : undefined;
let parsedSharedWith = [];
try { if (sharedWithUsers) parsedSharedWith = JSON.parse(sharedWithUsers); } catch (e) {}
const scene = new Scene({
name: title,
assetId: asset._id,
scene_url: tempFilePath,
gps: { lat: latitude, lng: longitude },
createdBy: req.user._id,
privacy: privacy || 'private',
shareToken,
sharedWith: parsedSharedWith,
privacy: finalPrivacy,
shareToken: finalShareToken,
shareTokenExpires: finalExpires,
sharedWith: finalSharedWith,
sharedEmails: finalSharedEmails,
status: 'processing',
tourId: cleanedTourId
tourId: assignedTourId
});
// Mặc định mỗi cảnh mới khi tạo ra là cảnh gốc của chính nó
if (!scene.tourId) scene.tourId = scene._id;
@@ -219,15 +244,23 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
await fs.promises.unlink(req.file.path).catch(() => {});
}
// [FIX] Đảm bảo root scene luôn có tourId để logic lan truyền hoạt động (cho cả dữ liệu cũ)
if (!scene.tourId) scene.tourId = scene._id;
// Đảm bảo tính nhất quán: Nếu không có tourId cha, scene này tự làm gốc
if (!scene.tourId) scene.tourId = scene._id;
await scene.save();
// [BẢO MẬT] Lan truyền Privacy xuống các cảnh con nếu đây là cảnh gốc của Tour.
const isRoot = scene.tourId && scene.tourId.toString() === scene._id.toString();
if (isRoot) {
await propagateScenePrivacy(scene._id, scene, req.user._id);
// [TASK 2] Chuẩn hóa dữ liệu truyền vào helper
// Chuyển đổi sharedWith thành mảng string ID thuần túy để tránh lỗi Mongoose
await propagateScenePrivacy(scene._id, {
privacy: scene.privacy,
shareToken: scene.shareToken,
shareTokenExpires: scene.shareTokenExpires,
sharedWith: scene.sharedWith.map(id => id.toString ? id.toString() : id),
sharedEmails: scene.sharedEmails
}, req.user._id);
}
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 });