diff --git a/backend/models/User.js b/backend/models/User.js index 55df42e..cc3e685 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -2,6 +2,18 @@ const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const userSchema = new mongoose.Schema({ + fullName: { + type: String, + required: true, + trim: true + }, + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true + }, username: { type: String, required: true, @@ -14,8 +26,12 @@ const userSchema = new mongoose.Schema({ }, role: { type: String, - enum: ['Chủ sở hữu', 'Thành viên'], - default: 'Thành viên' + enum: ['admin', 'moderator', 'editor', 'user'], + default: 'user' + }, + agreedToRules: { + type: Boolean, + required: true } }, { timestamps: true diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 149b0ec..18b2854 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -569,7 +569,7 @@ router.delete('/scenes/:id', protect, async (req, res) => { } // Kiểm tra quyền: Người tạo hoặc Admin - const isAdmin = req.user.role === 'Chủ sở hữu' || req.user.role === 'admin'; + const isAdmin = req.user.role === 'admin'; const isOwner = rootScene.createdBy.toString() === req.user._id.toString(); if (!isAdmin && !isOwner) { @@ -689,7 +689,7 @@ router.get('/me/scenes', protect, async (req, res) => { router.get('/me/assets', protect, async (req, res) => { try { // Sử dụng Aggregation để lấy Asset kèm thông tin Scene và Parent Scene - const query = req.user.role === 'Chủ sở hữu' ? {} : { uploadedBy: req.user._id }; + const query = (req.user.role === 'admin' || req.user.role === 'Chủ sở hữu') ? {} : { uploadedBy: req.user._id }; const assets = await Asset.aggregate([ { $match: query }, @@ -744,7 +744,7 @@ router.delete('/assets/:id', protect, async (req, res) => { // Kiểm tra quyền: Người upload hoặc Admin (Chủ sở hữu) const isOwner = asset.uploadedBy && asset.uploadedBy.toString() === req.user._id.toString(); - const isAdmin = req.user.role === 'Chủ sở hữu' || req.user.role === 'admin'; + const isAdmin = req.user.role === 'admin' || req.user.role === 'Chủ sở hữu'; if (!isOwner && !isAdmin) { return res.status(403).json({ message: 'Bạn không có quyền xóa tập tin này' }); @@ -783,17 +783,67 @@ router.delete('/assets/:id', protect, async (req, res) => { * @access Private (Admin) */ router.get('/admin/users', protect, async (req, res) => { - if (req.user.role !== 'Chủ sở hữu') { + if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') { return res.status(403).json({ message: 'Bạn không có quyền truy cập quản trị' }); } try { - const users = await User.find({}).select('-password').sort({ createdAt: -1 }); + const users = await User.find({}).sort({ createdAt: -1 }); res.json(users); } catch (error) { res.status(500).json({ message: error.message }); } }); +/** + * @route PUT /api/admin/users/:id + * @desc Admin cập nhật thông tin người dùng (Quyền, Mật khẩu, Email, Họ tên) + */ +router.put('/admin/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: 'Người dùng không tồn tại' }); + + // Cập nhật thông tin cơ bản + if (fullName) user.fullName = fullName; + if (email) user.email = email; + if (role) user.role = role; + + // Nếu có nhập mật khẩu mới thì cập nhật (Middleware pre-save sẽ tự hash) + if (password && password.trim() !== '') { + user.password = password; + } + + await user.save(); + res.json({ message: 'Cập nhật người dùng thành công' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route DELETE /api/admin/users/:id + * @desc Admin xóa vĩnh viễn người dùng + */ +router.delete('/admin/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) return res.status(404).json({ message: 'Người dùng không tồn tại' }); + + if (user.role === 'admin' && user._id.toString() === req.user._id.toString()) { + return res.status(400).json({ message: 'Bạn không thể tự xóa chính mình' }); + } + + // Lưu ý: Trong thực tế bạn có thể muốn xóa cả các Scene của user này + await User.findByIdAndDelete(req.params.id); + res.json({ message: 'Đã xóa người dùng vĩnh viễn' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + /** * @route GET/PUT /api/system/settings * @desc Quản lý thiết lập hệ thống @@ -810,7 +860,7 @@ router.get('/system/settings', optionalAuth, async (req, res) => { }); router.put('/system/settings', protect, async (req, res) => { - if (req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' }); + if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' }); try { const settings = await Setting.findOneAndUpdate({}, req.body, { new: true, upsert: true }); res.json(settings); @@ -826,6 +876,10 @@ router.put('/system/settings', protect, async (req, res) => { */ router.post('/maintenance/reset-all', protect, async (req, res) => { try { + if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') { + return res.status(403).json({ message: 'Chỉ Admin tối cao mới có quyền thực hiện thao tác này' }); + } + // 1. Xóa toàn bộ dữ liệu trong Database await Scene.deleteMany({}); await Asset.deleteMany({}); diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index fcb2772..b4dee02 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -11,26 +11,35 @@ const router = express.Router(); */ router.post('/register', async (req, res) => { try { - const { username, password, role } = req.body; + const { fullName, email, username, password, agreedToRules } = req.body; - // Check if user already exists - const userExists = await User.findOne({ username }); + // Kiểm tra thông tin bắt buộc + if (!fullName || !email || !username || !password || agreedToRules === undefined) { + return res.status(400).json({ message: 'Vui lòng cung cấp đầy đủ thông tin đăng ký' }); + } + + // Kiểm tra xem username hoặc email đã tồn tại chưa + const userExists = await User.findOne({ $or: [{ username }, { email }] }); if (userExists) { - return res.status(400).json({ message: 'User already exists' }); + const field = userExists.username === username ? 'Tên đăng nhập' : 'Email'; + return res.status(400).json({ message: `${field} đã được sử dụng` }); } // Check if this is the very first user registering const userCount = await User.countDocuments(); - let finalRole = 'Thành viên'; + let finalRole = 'user'; if (userCount === 0) { // First user to register in the system gets the supreme admin role - finalRole = 'Chủ sở hữu'; + finalRole = 'admin'; } const user = new User({ + fullName, + email, username, password, + agreedToRules, role: finalRole }); diff --git a/frontend/css/style.css b/frontend/css/style.css index 7e79aed..bcd761c 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -197,6 +197,8 @@ html, body { z-index: 1600; /* Above top-bar */ border: 1px solid rgba(255, 255, 255, 0.1); min-width: 180px; /* Đảm bảo không quá nhỏ */ + max-height: 550px; + overflow-y: auto; } #user-dropdown.show { @@ -210,6 +212,54 @@ html, body { /* Nếu đây là tiêu đề "Welcome", nó sẽ bị ẩn bởi quy tắc bên dưới */ } +.auth-tabs { + display: flex; + padding: 0 10px 10px; + margin-bottom: 15px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.auth-tab-btn { + flex: 1; + background: none; + border: none; + color: #888; + cursor: pointer; + padding: 8px 0; + font-size: 14px; + transition: all 0.2s; +} + +.auth-tab-btn.active { + color: #fff; + border-bottom: 2px solid #007bff; +} + +.auth-error { + color: #ff4d4d; + font-size: 12px; + margin: 0 20px 10px 20px; + display: none; +} + +.auth-submit-btn { + margin: 0 20px 15px; + width: calc(100% - 40px) !important; + background: #007bff !important; + color: white !important; + padding: 10px !important; +} + +.rules-checkbox { + display: flex; + align-items: center; + gap: 8px; + color: #ccc; + font-size: 12px; + margin: 5px 20px 15px; + cursor: pointer; +} + #user-dropdown input { width: calc(100% - 40px); margin: 0 20px 10px 20px; @@ -837,6 +887,36 @@ html, body { border: 1px solid rgba(255, 255, 255, 0.1); } +/* --- Admin User Management Table --- */ +.admin-table { + width: 100%; + border-collapse: collapse; + margin-top: 15px; + font-size: 14px; +} + +.admin-table th, .admin-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.admin-table th { + color: #888; + font-weight: 600; + text-transform: uppercase; + font-size: 12px; +} + +.admin-table input, .admin-table select { + background: rgba(255, 255, 255, 0.05) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + color: #fff !important; + padding: 5px !important; + border-radius: 4px; + width: 100%; +} + /* --- Privacy Settings Enhancements --- */ .privacy-settings-btn { background: rgba(255, 255, 255, 0.1); diff --git a/frontend/index.html b/frontend/index.html index 3be9881..9b57467 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -30,13 +30,31 @@