courseworx/backend/routes/courseContent.js
Mahmoud M. Abdalla b4d90c650f Release v1.1.0 - Course Content & Enrollment Management
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.
2025-07-27 23:59:01 +03:00

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;