courseworx/backend/routes/users.js

391 lines
No EOL
12 KiB
JavaScript

const express = require('express');
const { body, validationResult } = require('express-validator');
const { User } = require('../models');
const { auth, requireSuperAdmin, requireTrainer } = require('../middleware/auth');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const csv = require('csv-parser');
const bcrypt = require('bcryptjs');
const router = express.Router();
// Multer storage for slider images
const sliderImageStorage = multer.diskStorage({
destination: function (req, file, cb) {
const dir = path.join(__dirname, '../uploads/slider');
fs.mkdirSync(dir, { recursive: true });
cb(null, dir);
},
filename: function (req, file, cb) {
cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
}
});
const uploadSliderImage = multer({
storage: sliderImageStorage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
},
limits: { fileSize: 5 * 1024 * 1024 }
});
// Utility to remove BOM from a file (in-place)
function removeBOMFromFile(filePath) {
const fs = require('fs');
const data = fs.readFileSync(filePath);
// UTF-8 BOM is 0xEF,0xBB,0xBF
if (data[0] === 0xEF && data[1] === 0xBB && data[2] === 0xBF) {
fs.writeFileSync(filePath, data.slice(3));
}
}
// @route GET /api/users
// @desc Get all users (Super Admin only)
// @access Private (Super Admin)
router.get('/', auth, requireSuperAdmin, async (req, res) => {
try {
const { role, page = 1, limit = 10, search } = req.query;
const offset = (page - 1) * limit;
const whereClause = {};
if (role) whereClause.role = role;
if (search) {
whereClause[require('sequelize').Op.or] = [
{ firstName: { [require('sequelize').Op.iLike]: `%${search}%` } },
{ lastName: { [require('sequelize').Op.iLike]: `%${search}%` } },
{ email: { [require('sequelize').Op.iLike]: `%${search}%` } }
];
}
const { count, rows: users } = await User.findAndCountAll({
where: whereClause,
attributes: { exclude: ['password'] },
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
users,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(count / limit),
totalItems: count,
itemsPerPage: parseInt(limit)
}
});
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// @route GET /api/users/:id
// @desc Get user by ID (Super Admin only)
// @access Private (Super Admin)
router.get('/:id', auth, requireSuperAdmin, async (req, res) => {
try {
const user = await User.findByPk(req.params.id, {
attributes: { exclude: ['password'] }
});
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
res.json({ user });
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// @route PUT /api/users/:id
// @desc Update user (Super Admin only)
// @access Private (Super Admin)
router.put('/:id', [
auth,
requireSuperAdmin,
body('firstName').optional().isLength({ min: 2, max: 50 }),
body('lastName').optional().isLength({ min: 2, max: 50 }),
body('email').optional().isEmail().normalizeEmail(),
body('role').optional().isIn(['super_admin', 'trainer', 'trainee']),
body('phone').optional().isMobilePhone(),
body('isActive').optional().isBoolean()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
const { firstName, lastName, email, role, phone, isActive } = req.body;
const updateData = {};
if (firstName) updateData.firstName = firstName;
if (lastName) updateData.lastName = lastName;
if (email) updateData.email = email;
if (role) updateData.role = role;
if (phone !== undefined) updateData.phone = phone;
if (isActive !== undefined) updateData.isActive = isActive;
await user.update(updateData);
res.json({
message: 'User updated successfully.',
user: {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
role: user.role,
phone: user.phone,
isActive: user.isActive
}
});
} catch (error) {
console.error('Update user error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// @route PUT /api/users/:id/password
// @desc Change user password (Super Admin only)
// @access Private (Super Admin)
router.put('/:id/password', [
auth,
requireSuperAdmin,
body('password').isLength({ min: 6 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
const { password } = req.body;
await user.update({ password });
res.json({
message: 'User password updated successfully.'
});
} catch (error) {
console.error('Update user password error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// @route DELETE /api/users/:id
// @desc Delete user (Super Admin only)
// @access Private (Super Admin)
router.delete('/:id', auth, requireSuperAdmin, async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
// Prevent deleting super admin accounts
if (user.role === 'super_admin') {
return res.status(403).json({ error: 'Cannot delete super admin accounts.' });
}
await user.destroy();
res.json({ message: 'User deleted successfully.' });
} catch (error) {
console.error('Delete user error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// @route GET /api/users/stats/overview
// @desc Get user statistics (Super Admin only)
// @access Private (Super Admin)
router.get('/stats/overview', auth, requireSuperAdmin, async (req, res) => {
try {
console.log('User stats endpoint called by user:', req.user.id, req.user.role);
const totalUsers = await User.count();
const activeUsers = await User.count({ where: { isActive: true } });
const trainers = await User.count({ where: { role: 'trainer' } });
const trainees = await User.count({ where: { role: 'trainee' } });
console.log('User stats calculated:', { totalUsers, activeUsers, trainers, trainees });
res.json({
stats: {
totalUsers,
activeUsers,
inactiveUsers: totalUsers - activeUsers,
trainers,
trainees,
superAdmins: totalUsers - trainers - trainees
}
});
} catch (error) {
console.error('Get user stats error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// Configure multer for file uploads
const upload = multer({
dest: 'uploads/',
fileFilter: (req, file, cb) => {
if (file.mimetype === 'text/csv' || file.originalname.endsWith('.csv')) {
cb(null, true);
} else {
cb(new Error('Only CSV files are allowed'), false);
}
},
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
}
});
// @route POST /api/users/import
// @desc Import users from CSV (Super Admin or Trainer)
// @access Private
router.post('/import', [
auth,
requireTrainer,
upload.single('file')
], async (req, res) => {
// Remove BOM if present
if (req.file && req.file.path) {
removeBOMFromFile(req.file.path);
}
try {
if (!req.file) {
return res.status(400).json({ error: 'No CSV file uploaded.' });
}
const results = [];
const errors = [];
let created = 0;
let skipped = 0;
// Parse CSV file
fs.createReadStream(req.file.path)
.pipe(csv())
.on('data', (data) => {
results.push(data);
})
.on('end', async () => {
try {
// Process each row
for (let i = 0; i < results.length; i++) {
const row = results[i];
const rowNumber = i + 2; // +2 because CSV has header and arrays are 0-indexed
try {
// Validate required fields
if (!row.firstName || !row.lastName || !row.email) {
errors.push(`Row ${rowNumber}: Missing required fields (firstName, lastName, email)`);
continue;
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(row.email)) {
errors.push(`Row ${rowNumber}: Invalid email format`);
continue;
}
// Check if user already exists
const existingUser = await User.findOne({ where: { email: row.email.toLowerCase() } });
if (existingUser) {
skipped++;
continue;
}
// Hash default password
const defaultPassword = req.body.defaultPassword || 'changeme123';
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
// Create user
await User.create({
firstName: row.firstName.trim(),
lastName: row.lastName.trim(),
email: row.email.toLowerCase().trim(),
phone: row.phone ? row.phone.trim() : null,
password: hashedPassword,
role: 'trainee',
isActive: true,
requiresPasswordChange: true // Flag for first login password change
});
created++;
} catch (error) {
errors.push(`Row ${rowNumber}: ${error.message}`);
}
}
// Clean up uploaded file
fs.unlinkSync(req.file.path);
res.json({
message: 'Import completed successfully.',
created,
skipped,
errors: errors.length,
errorDetails: errors
});
} catch (error) {
// Clean up uploaded file
if (fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path);
}
throw error;
}
})
.on('error', (error) => {
// Clean up uploaded file
if (fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path);
}
console.error('CSV parsing error:', error);
res.status(500).json({ error: 'Error parsing CSV file.' });
});
} catch (error) {
console.error('Import users error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
// @route POST /api/slider/image
// @desc Upload slider image (Super Admin only)
// @access Private
router.post('/slider/image', [auth, requireSuperAdmin, uploadSliderImage.single('image')], async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image file uploaded.' });
}
res.json({
message: 'Slider image uploaded successfully.',
imageUrl: `/uploads/slider/${req.file.filename}`
});
} catch (error) {
console.error('Upload slider image error:', error);
res.status(500).json({ error: 'Server error.' });
}
});
module.exports = router;