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;