New Features: - Comprehensive course content management system - Multi-type content support (Documents, Images, Videos, Articles, Quizzes, Certificates) - File upload system with 100MB limit and type validation - Quiz system with multiple question types - Complete enrollment and subscriber management - Status tracking (pending, active, completed, cancelled) - Payment management (pending, paid, failed, refunded) - Progress tracking (0-100%) with automatic updates - Course capacity limits and validation - Certificate issuance tracking - Enrollment analytics and statistics Technical Improvements: - Enhanced file upload system with type validation - New database models: CourseContent, QuizQuestion - Comprehensive API endpoints for content and enrollment management - Role-based access control for all new features - Enhanced error handling and validation - File management with automatic cleanup This version provides a complete foundation for creating rich educational content and managing student enrollments.
360 lines
No EOL
11 KiB
JavaScript
360 lines
No EOL
11 KiB
JavaScript
const express = require('express');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { CourseContent, QuizQuestion, Course } = require('../models');
|
|
const { auth, requireTrainer } = require('../middleware/auth');
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const router = express.Router();
|
|
|
|
// Multer storage for course content files
|
|
const contentFileStorage = multer.diskStorage({
|
|
destination: function (req, file, cb) {
|
|
const courseId = req.params.courseId;
|
|
const contentType = req.params.contentType || 'documents';
|
|
const dir = path.join(__dirname, '../uploads/courses', courseId, contentType);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
cb(null, dir);
|
|
},
|
|
filename: function (req, file, cb) {
|
|
const timestamp = Date.now();
|
|
const originalName = file.originalname.replace(/\s+/g, '_');
|
|
cb(null, `${timestamp}_${originalName}`);
|
|
}
|
|
});
|
|
|
|
const uploadContentFile = multer({
|
|
storage: contentFileStorage,
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = {
|
|
document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'],
|
|
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
video: ['video/mp4', 'video/webm', 'video/ogg', 'video/avi']
|
|
};
|
|
|
|
const contentType = req.params.contentType;
|
|
if (allowedTypes[contentType] && allowedTypes[contentType].includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error(`Invalid file type for ${contentType}`), false);
|
|
}
|
|
},
|
|
limits: { fileSize: 100 * 1024 * 1024 } // 100MB limit
|
|
});
|
|
|
|
// @route GET /api/courses/:courseId/content
|
|
// @desc Get all content for a course
|
|
// @access Private (Course owner or enrolled students)
|
|
router.get('/:courseId/content', auth, async (req, res) => {
|
|
try {
|
|
const { courseId } = req.params;
|
|
const { type, isPublished } = req.query;
|
|
|
|
const whereClause = { courseId };
|
|
if (type) whereClause.type = type;
|
|
if (isPublished !== undefined) {
|
|
whereClause.isPublished = isPublished === 'true';
|
|
}
|
|
|
|
const contents = await CourseContent.findAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: QuizQuestion,
|
|
as: 'questions',
|
|
attributes: ['id', 'question', 'questionType', 'points', 'order']
|
|
}
|
|
],
|
|
order: [['order', 'ASC']]
|
|
});
|
|
|
|
res.json({ contents });
|
|
} catch (error) {
|
|
console.error('Get course content error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/courses/:courseId/content/:contentId
|
|
// @desc Get specific content by ID
|
|
// @access Private (Course owner or enrolled students)
|
|
router.get('/:courseId/content/:contentId', auth, async (req, res) => {
|
|
try {
|
|
const { contentId } = req.params;
|
|
|
|
const content = await CourseContent.findByPk(contentId, {
|
|
include: [
|
|
{
|
|
model: QuizQuestion,
|
|
as: 'questions',
|
|
order: [['order', 'ASC']]
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!content) {
|
|
return res.status(404).json({ error: 'Content not found.' });
|
|
}
|
|
|
|
res.json({ content });
|
|
} catch (error) {
|
|
console.error('Get content error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/courses/:courseId/content
|
|
// @desc Create new course content
|
|
// @access Private (Course owner only)
|
|
router.post('/:courseId/content', [
|
|
auth,
|
|
requireTrainer,
|
|
body('title').isLength({ min: 1, max: 200 }),
|
|
body('type').isIn(['document', 'image', 'video', 'article', 'quiz', 'certificate']),
|
|
body('description').optional().isLength({ max: 1000 }),
|
|
body('order').optional().isInt({ min: 0 }),
|
|
body('points').optional().isInt({ min: 0 }),
|
|
body('isRequired').optional().isBoolean()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId } = req.params;
|
|
const {
|
|
title,
|
|
description,
|
|
type,
|
|
content,
|
|
order,
|
|
points,
|
|
isRequired,
|
|
articleContent,
|
|
quizData,
|
|
certificateTemplate
|
|
} = req.body;
|
|
|
|
// Verify course ownership
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
if (course.trainerId !== req.user.id && req.user.role !== 'super_admin') {
|
|
return res.status(403).json({ error: 'Not authorized to add content to this course.' });
|
|
}
|
|
|
|
const courseContent = await CourseContent.create({
|
|
courseId,
|
|
title,
|
|
description,
|
|
type,
|
|
content: content || {},
|
|
order: order || 0,
|
|
points: points || 0,
|
|
isRequired: isRequired !== undefined ? isRequired : true,
|
|
articleContent,
|
|
quizData,
|
|
certificateTemplate
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'Content created successfully.',
|
|
content: courseContent
|
|
});
|
|
} catch (error) {
|
|
console.error('Create content error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/courses/:courseId/content/:contentId
|
|
// @desc Update course content
|
|
// @access Private (Course owner only)
|
|
router.put('/:courseId/content/:contentId', [
|
|
auth,
|
|
requireTrainer,
|
|
body('title').optional().isLength({ min: 1, max: 200 }),
|
|
body('description').optional().isLength({ max: 1000 }),
|
|
body('order').optional().isInt({ min: 0 }),
|
|
body('points').optional().isInt({ min: 0 }),
|
|
body('isRequired').optional().isBoolean(),
|
|
body('isPublished').optional().isBoolean()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { contentId } = req.params;
|
|
|
|
const content = await CourseContent.findByPk(contentId, {
|
|
include: [{ model: Course, as: 'course' }]
|
|
});
|
|
|
|
if (!content) {
|
|
return res.status(404).json({ error: 'Content not found.' });
|
|
}
|
|
|
|
// Verify course ownership
|
|
if (content.course.trainerId !== req.user.id && req.user.role !== 'super_admin') {
|
|
return res.status(403).json({ error: 'Not authorized to update this content.' });
|
|
}
|
|
|
|
const updateData = req.body;
|
|
await content.update(updateData);
|
|
|
|
res.json({
|
|
message: 'Content updated successfully.',
|
|
content
|
|
});
|
|
} catch (error) {
|
|
console.error('Update content error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route DELETE /api/courses/:courseId/content/:contentId
|
|
// @desc Delete course content
|
|
// @access Private (Course owner only)
|
|
router.delete('/:courseId/content/:contentId', auth, requireTrainer, async (req, res) => {
|
|
try {
|
|
const { contentId } = req.params;
|
|
|
|
const content = await CourseContent.findByPk(contentId, {
|
|
include: [{ model: Course, as: 'course' }]
|
|
});
|
|
|
|
if (!content) {
|
|
return res.status(404).json({ error: 'Content not found.' });
|
|
}
|
|
|
|
// Verify course ownership
|
|
if (content.course.trainerId !== req.user.id && req.user.role !== 'super_admin') {
|
|
return res.status(403).json({ error: 'Not authorized to delete this content.' });
|
|
}
|
|
|
|
// Delete associated file if exists
|
|
if (content.fileUrl) {
|
|
const filePath = path.join(__dirname, '..', content.fileUrl);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
|
|
await content.destroy();
|
|
|
|
res.json({ message: 'Content deleted successfully.' });
|
|
} catch (error) {
|
|
console.error('Delete content error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/courses/:courseId/content/:contentType/upload
|
|
// @desc Upload file for course content
|
|
// @access Private (Course owner only)
|
|
router.post('/:courseId/content/:contentType/upload', [
|
|
auth,
|
|
requireTrainer,
|
|
uploadContentFile.single('file')
|
|
], async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No file uploaded.' });
|
|
}
|
|
|
|
const { courseId, contentType } = req.params;
|
|
const { contentId } = req.body;
|
|
|
|
// Verify course ownership
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
if (course.trainerId !== req.user.id && req.user.role !== 'super_admin') {
|
|
return res.status(403).json({ error: 'Not authorized to upload content to this course.' });
|
|
}
|
|
|
|
const fileUrl = `/uploads/courses/${courseId}/${contentType}/${req.file.filename}`;
|
|
|
|
// Update content if contentId is provided
|
|
if (contentId) {
|
|
const content = await CourseContent.findByPk(contentId);
|
|
if (content) {
|
|
await content.update({
|
|
fileUrl,
|
|
fileSize: req.file.size,
|
|
fileType: req.file.mimetype
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
message: 'File uploaded successfully.',
|
|
fileUrl,
|
|
fileSize: req.file.size,
|
|
fileType: req.file.mimetype
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload content file error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/courses/:courseId/content/:contentId/questions
|
|
// @desc Add quiz questions to content
|
|
// @access Private (Course owner only)
|
|
router.post('/:courseId/content/:contentId/questions', [
|
|
auth,
|
|
requireTrainer,
|
|
body('questions').isArray({ min: 1 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { contentId } = req.params;
|
|
const { questions } = req.body;
|
|
|
|
const content = await CourseContent.findByPk(contentId, {
|
|
include: [{ model: Course, as: 'course' }]
|
|
});
|
|
|
|
if (!content) {
|
|
return res.status(404).json({ error: 'Content not found.' });
|
|
}
|
|
|
|
if (content.type !== 'quiz') {
|
|
return res.status(400).json({ error: 'Can only add questions to quiz content.' });
|
|
}
|
|
|
|
// Verify course ownership
|
|
if (content.course.trainerId !== req.user.id && req.user.role !== 'super_admin') {
|
|
return res.status(403).json({ error: 'Not authorized to add questions to this content.' });
|
|
}
|
|
|
|
const createdQuestions = await QuizQuestion.bulkCreate(
|
|
questions.map((q, index) => ({
|
|
contentId,
|
|
...q,
|
|
order: q.order || index
|
|
}))
|
|
);
|
|
|
|
res.status(201).json({
|
|
message: 'Questions added successfully.',
|
|
questions: createdQuestions
|
|
});
|
|
} catch (error) {
|
|
console.error('Add quiz questions error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|