Major Features Added: - Complete Plugin Architecture System with financial plugin - Multi-currency support with exchange rates - Course type system (online, classroom, hybrid) - Attendance tracking and QR code scanning - Classroom sessions management - Course sections and content management - Professional video player with authentication - Secure media serving system - Shopping cart and checkout system - Financial dashboard and earnings tracking - Trainee progress tracking - User notes and assignments system Backend Infrastructure: - Plugin loader and registry system - Multi-currency database models - Secure media middleware - Course access middleware - Financial plugin with payment processing - Database migrations for new features - API endpoints for all new functionality Frontend Components: - Course management interface - Content creation and editing - Section management with drag-and-drop - Professional video player - QR scanner for attendance - Shopping cart and checkout flow - Financial dashboard - Plugin management interface - Trainee details and progress views This represents a major evolution of CourseWorx from a basic LMS to a comprehensive educational platform with plugin architecture.
132 lines
4.1 KiB
JavaScript
132 lines
4.1 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const {
|
|
authenticateMediaAccess,
|
|
checkCourseAccess,
|
|
addMediaSecurityHeaders,
|
|
logMediaAccess
|
|
} = require('../middleware/mediaAuth');
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* Secure Media Serving Route
|
|
* Single endpoint for all media files with proper security
|
|
* Replaces the multiple fragmented routes created during CORS attempts
|
|
*/
|
|
|
|
/**
|
|
* GET /api/media/*
|
|
* Serves all media files (images, videos, documents) with security controls
|
|
*/
|
|
router.get('/*',
|
|
// Extract media path from request
|
|
(req, res, next) => {
|
|
req.mediaPath = req.path.substring(1); // Remove leading slash
|
|
next();
|
|
},
|
|
|
|
// Security middleware chain
|
|
authenticateMediaAccess,
|
|
checkCourseAccess,
|
|
addMediaSecurityHeaders,
|
|
logMediaAccess,
|
|
|
|
// Serve the actual file
|
|
async (req, res) => {
|
|
try {
|
|
const { mediaPath } = req;
|
|
|
|
// Decode URL-encoded characters (important for Arabic folder names)
|
|
const decodedPath = decodeURIComponent(mediaPath);
|
|
const fullPath = path.join(__dirname, '../uploads', decodedPath);
|
|
|
|
// Security check: Ensure path is within uploads directory
|
|
const uploadsDir = path.join(__dirname, '../uploads');
|
|
const resolvedPath = path.resolve(fullPath);
|
|
if (!resolvedPath.startsWith(path.resolve(uploadsDir))) {
|
|
return res.status(403).json({ error: 'Access denied: Invalid path' });
|
|
}
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(fullPath)) {
|
|
return res.status(404).json({
|
|
error: 'File not found',
|
|
path: decodedPath
|
|
});
|
|
}
|
|
|
|
// Get file stats for proper headers
|
|
const stats = fs.statSync(fullPath);
|
|
const fileExtension = path.extname(fullPath).toLowerCase();
|
|
|
|
// Set appropriate content type
|
|
let contentType = 'application/octet-stream'; // default
|
|
|
|
// Image types
|
|
if (fileExtension === '.jpg' || fileExtension === '.jpeg') {
|
|
contentType = 'image/jpeg';
|
|
} else if (fileExtension === '.png') {
|
|
contentType = 'image/png';
|
|
} else if (fileExtension === '.gif') {
|
|
contentType = 'image/gif';
|
|
} else if (fileExtension === '.webp') {
|
|
contentType = 'image/webp';
|
|
} else if (fileExtension === '.svg') {
|
|
contentType = 'image/svg+xml';
|
|
}
|
|
// Video types
|
|
else if (fileExtension === '.mp4') {
|
|
contentType = 'video/mp4';
|
|
} else if (fileExtension === '.webm') {
|
|
contentType = 'video/webm';
|
|
} else if (fileExtension === '.avi') {
|
|
contentType = 'video/avi';
|
|
}
|
|
// Document types
|
|
else if (fileExtension === '.pdf') {
|
|
contentType = 'application/pdf';
|
|
} else if (fileExtension === '.doc') {
|
|
contentType = 'application/msword';
|
|
} else if (fileExtension === '.docx') {
|
|
contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
}
|
|
|
|
// Set content headers
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Content-Length', stats.size);
|
|
|
|
// Handle range requests for video streaming
|
|
const range = req.headers.range;
|
|
if (range && contentType.startsWith('video/')) {
|
|
const parts = range.replace(/bytes=/, "").split("-");
|
|
const start = parseInt(parts[0], 10);
|
|
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
|
|
const chunksize = (end - start) + 1;
|
|
|
|
const fileStream = fs.createReadStream(fullPath, { start, end });
|
|
|
|
res.writeHead(206, {
|
|
'Content-Range': `bytes ${start}-${end}/${stats.size}`,
|
|
'Accept-Ranges': 'bytes',
|
|
'Content-Length': chunksize,
|
|
});
|
|
|
|
fileStream.pipe(res);
|
|
} else {
|
|
// Serve entire file
|
|
res.sendFile(fullPath);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Media serving error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: process.env.NODE_ENV === 'development' ? error.message : 'File serving failed'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
module.exports = router;
|