Xóa scene con mà không xóa scene cha

This commit is contained in:
2026-06-09 21:26:47 +07:00
parent d39d3b3d53
commit 67825b04cc
22 changed files with 1185 additions and 1486 deletions
+125
View File
@@ -0,0 +1,125 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const AdmZip = require('adm-zip');
const multer = require('multer');
const User = require('../models/User');
const Asset = require('../models/Asset');
const Scene = require('../models/Scene');
const Hotspot = require('../models/Hotspot');
const Setting = require('../models/Setting');
const { protect } = require('../middlewares/authMiddleware');
const { logActivity } = require('../utils/logger');
const uploadDir = process.env.UPLOAD_DIR ? path.resolve(process.env.UPLOAD_DIR) : path.join(__dirname, '../uploads');
const tempDir = path.join(uploadDir, 'temp');
const upload = multer({ dest: tempDir });
// Helper: Dọn dẹp dữ liệu mồ côi
const runOrphanedCleanup = async (performer = 'System') => {
const validUserIds = await User.distinct('_id');
const orphanedScenes = await Scene.find({ createdBy: { $nin: validUserIds } });
const orphanedSceneIds = orphanedScenes.map(s => s._id);
if (orphanedSceneIds.length > 0) {
await Hotspot.deleteMany({ $or: [{ parent_scene_id: { $in: orphanedSceneIds } }, { target_scene_id: { $in: orphanedSceneIds } }] });
await Scene.deleteMany({ _id: { $in: orphanedSceneIds } });
}
const usedAssetIds = await Scene.distinct('assetId');
const safeDate = new Date(Date.now() - 2 * 3600 * 1000);
const orphanedAssets = await Asset.find({ $or: [{ uploadedBy: { $nin: validUserIds } }, { $and: [{ _id: { $nin: usedAssetIds } }, { createdAt: { $lt: safeDate } }] }] });
for (const asset of orphanedAssets) {
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
await Asset.findByIdAndDelete(asset._id);
}
await logActivity('SYSTEM_ORPHAN_CLEANUP', { scenesDeleted: orphanedSceneIds.length, assetsDeleted: orphanedAssets.length }, performer);
return { scenesDeleted: orphanedSceneIds.length, assetsDeleted: orphanedAssets.length };
};
// @route POST /api/admin/backup
router.post('/backup', protect, async (req, res) => {
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' });
try {
const zip = new AdmZip();
const dbData = {
users: await User.find().lean(),
assets: await Asset.find().lean(),
scenes: await Scene.find().lean(),
hotspots: await Hotspot.find().lean(),
settings: await Setting.find().lean()
};
zip.addFile("database.json", Buffer.from(JSON.stringify(dbData, null, 2), "utf8"));
if (fs.existsSync(uploadDir)) zip.addLocalFolder(uploadDir, "uploads");
res.set({ 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="backup.zip"' });
res.send(zip.toBuffer());
} catch (error) { res.status(500).json({ message: error.message }); }
});
// @route GET /api/admin/maintenance/stray-files
router.get('/maintenance/stray-files', protect, async (req, res) => {
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' });
try {
const entries = await fs.promises.readdir(uploadDir, { withFileTypes: true });
const assets = await Asset.find().select('filePath').lean();
const dbFileNames = new Set(assets.map(a => path.basename(a.filePath)));
const strayFiles = entries.filter(e => e.isFile() && !e.name.startsWith('.') && !dbFileNames.has(e.name)).map(e => e.name);
res.json({ count: strayFiles.length, files: strayFiles });
} catch (error) { res.status(500).json({ message: error.message }); }
});
// @route POST /api/admin/maintenance/cleanup
router.post('/maintenance/cleanup', protect, async (req, res) => {
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' });
try {
const report = await runOrphanedCleanup(req.user.username);
res.json({ message: 'Cleanup completed', report });
} catch (error) { res.status(500).json({ message: error.message }); }
});
// @route GET /api/admin/users
router.get('/users', protect, async (req, res) => {
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' });
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User.find().sort({ createdAt: -1 }).skip(skip).limit(limit).select('-password');
const total = await User.countDocuments();
res.json({ users, totalPages: Math.ceil(total / limit), currentPage: page });
} catch (error) { res.status(500).json({ message: error.message }); }
});
// @route PUT /api/admin/users/:id
router.put('/users/:id', protect, async (req, res) => {
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' });
try {
const { fullName, email, role, password } = req.body;
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'User not found' });
if (fullName) user.fullName = fullName;
if (email) user.email = email;
if (role && user.role !== 'admin') user.role = role;
if (password) user.password = password;
await user.save();
res.json({ message: 'User updated' });
} catch (error) { res.status(500).json({ message: error.message }); }
});
// @route DELETE /api/admin/users/:id
router.delete('/users/:id', protect, async (req, res) => {
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' });
try {
const user = await User.findById(req.params.id);
if (!user || user.role === 'admin') return res.status(400).json({ message: 'Invalid request' });
await User.findByIdAndDelete(req.params.id);
await logActivity('USER_PERMANENT_DELETE', { userId: user._id, username: user.username }, req.user.username);
await runOrphanedCleanup(req.user.username);
res.json({ message: 'User deleted' });
} catch (error) { res.status(500).json({ message: error.message }); }
});
module.exports = router;