MAJOR FEATURE: Complete Course Content Management System NEW FEATURES: - Enhanced Trainer Dashboard with clickable cards - New Trainer Courses Page (/trainer/courses) with filtering and management - New Trainer Students Page (/trainer/students) with enrollment management - Backend API endpoints for trainer-specific data - Enhanced API services with React Query integration TECHNICAL IMPROVEMENTS: - Created TrainerCourses.js and TrainerStudents.js components - Updated Dashboard.js with enhanced navigation - Added new routes in App.js for trainer pages - Implemented secure trainer-specific backend endpoints - Added role-based access control and data isolation DESIGN FEATURES: - Responsive design with mobile-first approach - Beautiful UI with hover effects and transitions - Consistent styling throughout the application - Accessibility improvements with ARIA labels SECURITY: - Trainers can only access their own data - Secure API authentication required - Data isolation between different trainers PERFORMANCE: - Efficient React Query implementation - Optimized database queries - Responsive image handling BUG FIXES: - Fixed phone number login functionality - Removed temporary debug endpoints - Cleaned up authentication logging This release provides trainers with comprehensive tools to manage their courses and students, significantly improving the user experience and functionality of the CourseWorx platform.
494 lines
No EOL
14 KiB
JavaScript
494 lines
No EOL
14 KiB
JavaScript
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const bcrypt = require('bcryptjs');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { User } = require('../models');
|
|
const { auth, requireSuperAdmin } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
// Generate JWT token
|
|
const generateToken = (userId) => {
|
|
return jwt.sign({ userId }, process.env.JWT_SECRET, {
|
|
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
|
|
});
|
|
};
|
|
|
|
// @route GET /api/auth/setup-status
|
|
// @desc Check if system setup is required
|
|
// @access Public
|
|
router.get('/setup-status', async (req, res) => {
|
|
try {
|
|
const superAdminCount = await User.count({ where: { role: 'super_admin' } });
|
|
res.json({
|
|
setupRequired: superAdminCount === 0,
|
|
superAdminCount
|
|
});
|
|
} catch (error) {
|
|
console.error('Setup status check error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/auth/setup
|
|
// @desc First-time setup - Create super admin (Public)
|
|
// @access Public (only when no SA exists)
|
|
router.post('/setup', [
|
|
body('firstName').isLength({ min: 2, max: 50 }),
|
|
body('lastName').isLength({ min: 2, max: 50 }),
|
|
body('email').isEmail().normalizeEmail(),
|
|
body('password').isLength({ min: 6 }),
|
|
body('phone').notEmpty().withMessage('Phone number is required'),
|
|
body('isActive').optional().isBoolean()
|
|
], async (req, res) => {
|
|
try {
|
|
// Check if setup is already completed
|
|
const superAdminCount = await User.count({ where: { role: 'super_admin' } });
|
|
if (superAdminCount > 0) {
|
|
return res.status(400).json({ error: 'System setup already completed.' });
|
|
}
|
|
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { firstName, lastName, email, password, phone } = req.body;
|
|
|
|
// Check if user already exists (by email or phone)
|
|
const existingUser = await User.findOne({
|
|
where: {
|
|
[require('sequelize').Op.or]: [
|
|
{ email },
|
|
{ phone }
|
|
]
|
|
}
|
|
});
|
|
|
|
if (existingUser) {
|
|
if (existingUser.email === email) {
|
|
return res.status(400).json({ error: 'User with this email already exists.' });
|
|
} else {
|
|
return res.status(400).json({ error: 'User with this phone number already exists.' });
|
|
}
|
|
}
|
|
|
|
// Create the first super admin
|
|
const superAdmin = await User.create({
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
password,
|
|
role: 'super_admin',
|
|
phone,
|
|
isActive: true
|
|
});
|
|
|
|
console.log('Super Admin created during setup:', {
|
|
id: superAdmin.id,
|
|
email: superAdmin.email,
|
|
phone: superAdmin.phone,
|
|
role: superAdmin.role
|
|
});
|
|
|
|
// Generate token for immediate login
|
|
const token = generateToken(superAdmin.id);
|
|
|
|
res.status(201).json({
|
|
message: 'System setup completed successfully. Super Admin account created.',
|
|
token,
|
|
user: {
|
|
id: superAdmin.id,
|
|
firstName: superAdmin.firstName,
|
|
lastName: superAdmin.lastName,
|
|
email: superAdmin.email,
|
|
phone: superAdmin.phone,
|
|
role: superAdmin.role
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Setup error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/auth/login
|
|
// @desc Login user
|
|
// @access Public
|
|
router.post('/login', [
|
|
body('identifier').notEmpty().withMessage('Email or phone number is required'),
|
|
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 { identifier, password } = req.body;
|
|
|
|
// Find user by email or phone
|
|
const user = await User.findOne({
|
|
where: {
|
|
[require('sequelize').Op.or]: [
|
|
{ email: identifier },
|
|
{ phone: identifier }
|
|
],
|
|
isActive: true
|
|
}
|
|
});
|
|
|
|
if (!user || !user.isActive) {
|
|
return res.status(401).json({ error: 'Invalid credentials or account inactive.' });
|
|
}
|
|
|
|
const isPasswordValid = await user.comparePassword(password);
|
|
|
|
if (!isPasswordValid) {
|
|
return res.status(401).json({ error: 'Invalid credentials.' });
|
|
}
|
|
|
|
// Update last login
|
|
await user.update({ lastLogin: new Date() });
|
|
|
|
const token = generateToken(user.id);
|
|
|
|
res.json({
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
email: user.email,
|
|
phone: user.phone,
|
|
role: user.role,
|
|
avatar: user.avatar,
|
|
requiresPasswordChange: user.requiresPasswordChange
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/auth/register
|
|
// @desc Register new user (Super Admin only)
|
|
// @access Private (Super Admin)
|
|
router.post('/register', [
|
|
auth,
|
|
requireSuperAdmin,
|
|
body('firstName').isLength({ min: 2, max: 50 }),
|
|
body('lastName').isLength({ min: 2, max: 50 }),
|
|
body('email').isEmail().normalizeEmail(),
|
|
body('password').isLength({ min: 6 }),
|
|
body('role').isIn(['super_admin', 'trainer', 'trainee']),
|
|
body('phone').notEmpty().withMessage('Phone number is required')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { firstName, lastName, email, password, role, phone, isActive = true } = req.body;
|
|
|
|
// Check if user already exists
|
|
const existingUser = await User.findOne({ where: { email } });
|
|
if (existingUser) {
|
|
return res.status(400).json({ error: 'User with this email already exists.' });
|
|
}
|
|
|
|
// Prevent creating another super admin if not during setup
|
|
if (role === 'super_admin') {
|
|
const superAdminCount = await User.count({ where: { role: 'super_admin' } });
|
|
if (superAdminCount > 0) {
|
|
return res.status(400).json({ error: 'Super Admin account already exists. Cannot create another one.' });
|
|
}
|
|
}
|
|
|
|
const user = await User.create({
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
password,
|
|
role,
|
|
phone,
|
|
isActive
|
|
});
|
|
|
|
console.log('User created successfully:', {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
isActive: user.isActive
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'User created 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('Registration error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/auth/me
|
|
// @desc Get current user
|
|
// @access Private
|
|
router.get('/me', auth, async (req, res) => {
|
|
try {
|
|
res.json({
|
|
user: {
|
|
id: req.user.id,
|
|
firstName: req.user.firstName,
|
|
lastName: req.user.lastName,
|
|
email: req.user.email,
|
|
role: req.user.role,
|
|
avatar: req.user.avatar,
|
|
phone: req.user.phone,
|
|
lastLogin: req.user.lastLogin
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get user error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/auth/profile
|
|
// @desc Update user profile
|
|
// @access Private
|
|
router.put('/profile', [
|
|
auth,
|
|
body('firstName').optional().isLength({ min: 2, max: 50 }),
|
|
body('lastName').optional().isLength({ min: 2, max: 50 }),
|
|
body('phone').optional()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { firstName, lastName, phone } = req.body;
|
|
const updateData = {};
|
|
|
|
if (firstName) updateData.firstName = firstName;
|
|
if (lastName) updateData.lastName = lastName;
|
|
if (phone) updateData.phone = phone;
|
|
|
|
await req.user.update(updateData);
|
|
|
|
res.json({
|
|
message: 'Profile updated successfully.',
|
|
user: {
|
|
id: req.user.id,
|
|
firstName: req.user.firstName,
|
|
lastName: req.user.lastName,
|
|
email: req.user.email,
|
|
role: req.user.role,
|
|
avatar: req.user.avatar,
|
|
phone: req.user.phone
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Profile update error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/auth/change-password
|
|
// @desc Change user password
|
|
// @access Private
|
|
router.put('/change-password', [
|
|
auth,
|
|
body('currentPassword').isLength({ min: 6 }),
|
|
body('newPassword').isLength({ min: 6 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { currentPassword, newPassword } = req.body;
|
|
|
|
const isCurrentPasswordValid = await req.user.comparePassword(currentPassword);
|
|
if (!isCurrentPasswordValid) {
|
|
return res.status(400).json({ error: 'Current password is incorrect.' });
|
|
}
|
|
|
|
await req.user.update({ password: newPassword });
|
|
|
|
res.json({ message: 'Password changed successfully.' });
|
|
} catch (error) {
|
|
console.error('Password change error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/auth/first-password-change
|
|
// @desc Change password on first login (for imported users)
|
|
// @access Private
|
|
router.put('/first-password-change', [
|
|
auth,
|
|
body('newPassword').isLength({ min: 6 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { newPassword } = req.body;
|
|
|
|
// Check if user requires password change
|
|
if (!req.user.requiresPasswordChange) {
|
|
return res.status(400).json({ error: 'Password change not required.' });
|
|
}
|
|
|
|
await req.user.update({
|
|
password: newPassword,
|
|
requiresPasswordChange: false
|
|
});
|
|
|
|
res.json({
|
|
message: 'Password changed successfully.',
|
|
user: {
|
|
id: req.user.id,
|
|
firstName: req.user.firstName,
|
|
lastName: req.user.lastName,
|
|
email: req.user.email,
|
|
role: req.user.role,
|
|
avatar: req.user.avatar,
|
|
phone: req.user.phone,
|
|
requiresPasswordChange: false
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('First password change error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/auth/trainee-login
|
|
// @desc Login trainee with phone or email
|
|
// @access Public
|
|
router.post('/trainee-login', [
|
|
body('identifier').notEmpty().withMessage('Phone or email is required'),
|
|
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 { identifier, password } = req.body;
|
|
|
|
// Find user by email or phone
|
|
const user = await User.findOne({
|
|
where: {
|
|
[require('sequelize').Op.or]: [
|
|
{ email: identifier },
|
|
{ phone: identifier }
|
|
],
|
|
role: 'trainee',
|
|
isActive: true
|
|
}
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Invalid credentials or account inactive.' });
|
|
}
|
|
|
|
const isPasswordValid = await user.comparePassword(password);
|
|
|
|
if (!isPasswordValid) {
|
|
return res.status(401).json({ error: 'Invalid credentials.' });
|
|
}
|
|
|
|
// Update last login
|
|
await user.update({ lastLogin: new Date() });
|
|
|
|
const token = generateToken(user.id);
|
|
|
|
res.json({
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
email: user.email,
|
|
phone: user.phone,
|
|
role: user.role,
|
|
avatar: user.avatar,
|
|
requiresPasswordChange: user.requiresPasswordChange
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Trainee login error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/auth/check-enrollment
|
|
// @desc Check if trainee is enrolled in any courses
|
|
// @access Private (Trainee)
|
|
router.post('/check-enrollment', auth, async (req, res) => {
|
|
try {
|
|
if (req.user.role !== 'trainee') {
|
|
return res.status(403).json({ error: 'Only trainees can check enrollment.' });
|
|
}
|
|
|
|
const { Enrollment, Course } = require('../models');
|
|
|
|
const enrollments = await Enrollment.findAll({
|
|
where: {
|
|
userId: req.user.id,
|
|
status: ['active', 'pending']
|
|
},
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
attributes: ['id', 'title', 'thumbnail', 'description'],
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
}
|
|
],
|
|
order: [['enrolledAt', 'DESC']]
|
|
});
|
|
|
|
res.json({
|
|
hasEnrollments: enrollments.length > 0,
|
|
enrollments: enrollments.map(e => ({
|
|
id: e.id,
|
|
status: e.status,
|
|
progress: e.progress,
|
|
enrolledAt: e.enrolledAt,
|
|
course: e.course
|
|
}))
|
|
});
|
|
} catch (error) {
|
|
console.error('Check enrollment error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|