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.
334 lines
9.5 KiB
JavaScript
334 lines
9.5 KiB
JavaScript
/**
|
|
* CourseWorx Plugin Loader
|
|
*
|
|
* This module is responsible for discovering, validating, and loading
|
|
* plugins from the plugins directory. It handles plugin lifecycle
|
|
* management and integration with the Express application.
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const pluginRegistry = require('./plugin-registry');
|
|
const pluginValidator = require('./plugin-validator');
|
|
|
|
class PluginLoader {
|
|
constructor() {
|
|
this.pluginsDir = path.join(__dirname, '..', 'plugins');
|
|
this.loadedPlugins = new Map();
|
|
this.loadErrors = new Map();
|
|
}
|
|
|
|
/**
|
|
* Initialize the plugin system
|
|
* @param {Object} app - Express application instance
|
|
*/
|
|
async initialize(app) {
|
|
console.log('🔌 Initializing plugin system...');
|
|
|
|
try {
|
|
// Ensure plugins directory exists
|
|
await this.ensurePluginsDirectory();
|
|
|
|
// Load all plugins
|
|
await this.loadAllPlugins(app);
|
|
|
|
// Register API routes for enabled plugins
|
|
this.registerApiRoutes(app);
|
|
|
|
console.log(`✅ Plugin system initialized. Loaded ${this.loadedPlugins.size} plugins.`);
|
|
|
|
// Log any load errors
|
|
if (this.loadErrors.size > 0) {
|
|
console.warn('⚠️ Some plugins failed to load:');
|
|
for (const [pluginName, error] of this.loadErrors) {
|
|
console.warn(` - ${pluginName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize plugin system:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure the plugins directory exists
|
|
*/
|
|
async ensurePluginsDirectory() {
|
|
try {
|
|
await fs.access(this.pluginsDir);
|
|
} catch (error) {
|
|
// Directory doesn't exist, create it
|
|
await fs.mkdir(this.pluginsDir, { recursive: true });
|
|
console.log(`📁 Created plugins directory: ${this.pluginsDir}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load all plugins from the plugins directory
|
|
* @param {Object} app - Express application instance
|
|
*/
|
|
async loadAllPlugins(app) {
|
|
try {
|
|
const pluginFolders = await fs.readdir(this.pluginsDir);
|
|
|
|
for (const folder of pluginFolders) {
|
|
const pluginPath = path.join(this.pluginsDir, folder);
|
|
const pluginStat = await fs.stat(pluginPath);
|
|
|
|
if (pluginStat.isDirectory()) {
|
|
await this.loadPlugin(folder, pluginPath, app);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error reading plugins directory:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a single plugin
|
|
* @param {string} pluginName - Plugin folder name
|
|
* @param {string} pluginPath - Full path to plugin directory
|
|
* @param {Object} app - Express application instance
|
|
*/
|
|
async loadPlugin(pluginName, pluginPath, app) {
|
|
try {
|
|
console.log(`📦 Loading plugin: ${pluginName}`);
|
|
|
|
// Check if plugin has required files
|
|
const packageJsonPath = path.join(pluginPath, 'package.json');
|
|
const indexJsPath = path.join(pluginPath, 'index.js');
|
|
|
|
// Check if package.json exists
|
|
try {
|
|
await fs.access(packageJsonPath);
|
|
} catch (error) {
|
|
throw new Error('Missing package.json file');
|
|
}
|
|
|
|
// Check if index.js exists
|
|
try {
|
|
await fs.access(indexJsPath);
|
|
} catch (error) {
|
|
throw new Error('Missing index.js file');
|
|
}
|
|
|
|
// Load and validate package.json
|
|
const packageJson = require(packageJsonPath);
|
|
const validationResult = pluginValidator.validatePackageJson(packageJson);
|
|
|
|
if (!validationResult.isValid) {
|
|
throw new Error(`Package.json validation failed: ${validationResult.errors.join(', ')}`);
|
|
}
|
|
|
|
// Load the plugin module
|
|
const pluginModule = require(pluginPath);
|
|
|
|
if (typeof pluginModule.register !== 'function') {
|
|
throw new Error('Plugin must export a register function');
|
|
}
|
|
|
|
// Register the plugin in the registry
|
|
const pluginInfo = pluginRegistry.registerPlugin(pluginName, packageJson);
|
|
|
|
// Call the plugin's register function
|
|
const registerContext = {
|
|
app,
|
|
registry: pluginRegistry,
|
|
pluginName,
|
|
pluginPath,
|
|
packageJson
|
|
};
|
|
|
|
await pluginModule.register(registerContext);
|
|
|
|
// Enable the plugin by default (unless specified otherwise)
|
|
if (packageJson.courseworx?.autoEnable !== false) {
|
|
pluginRegistry.enablePlugin(pluginName);
|
|
}
|
|
|
|
// Store the loaded plugin info
|
|
this.loadedPlugins.set(pluginName, {
|
|
path: pluginPath,
|
|
module: pluginModule,
|
|
info: pluginInfo,
|
|
loadedAt: new Date()
|
|
});
|
|
|
|
console.log(`✅ Plugin loaded successfully: ${pluginName} v${packageJson.version}`);
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Failed to load plugin ${pluginName}:`, error.message);
|
|
this.loadErrors.set(pluginName, error);
|
|
|
|
// Don't throw here, continue loading other plugins
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register API routes for enabled plugins
|
|
* @param {Object} app - Express application instance
|
|
*/
|
|
registerApiRoutes(app) {
|
|
const apiRoutes = pluginRegistry.getApiRoutes();
|
|
console.log(`🔗 Found ${apiRoutes.length} API routes to register:`, apiRoutes.map(r => r.path));
|
|
|
|
for (const routeInfo of apiRoutes) {
|
|
try {
|
|
console.log(`🔗 Registering API routes: ${routeInfo.path} for plugin: ${routeInfo.plugin}`);
|
|
app.use(routeInfo.path, routeInfo.router);
|
|
console.log(`✅ Successfully registered API routes: ${routeInfo.path}`);
|
|
} catch (error) {
|
|
console.error(`❌ Failed to register API routes for ${routeInfo.plugin}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reload a specific plugin
|
|
* @param {string} pluginName - Plugin name
|
|
* @param {Object} app - Express application instance
|
|
*/
|
|
async reloadPlugin(pluginName, app) {
|
|
try {
|
|
console.log(`🔄 Reloading plugin: ${pluginName}`);
|
|
|
|
// Unregister the plugin from registry
|
|
if (pluginRegistry.getPlugin(pluginName)) {
|
|
pluginRegistry.unregisterPlugin(pluginName);
|
|
}
|
|
|
|
// Remove from loaded plugins
|
|
this.loadedPlugins.delete(pluginName);
|
|
this.loadErrors.delete(pluginName);
|
|
|
|
// Reload the plugin
|
|
const pluginPath = path.join(this.pluginsDir, pluginName);
|
|
await this.loadPlugin(pluginName, pluginPath, app);
|
|
|
|
console.log(`✅ Plugin reloaded: ${pluginName}`);
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Failed to reload plugin ${pluginName}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reload all plugins
|
|
* @param {Object} app - Express application instance
|
|
*/
|
|
async reloadAllPlugins(app) {
|
|
console.log('🔄 Reloading all plugins...');
|
|
|
|
// Clear the registry
|
|
pluginRegistry.clear();
|
|
|
|
// Clear loaded plugins
|
|
this.loadedPlugins.clear();
|
|
this.loadErrors.clear();
|
|
|
|
// Reload all plugins
|
|
await this.loadAllPlugins(app);
|
|
|
|
// Re-register API routes
|
|
this.registerApiRoutes(app);
|
|
|
|
console.log(`✅ All plugins reloaded. Loaded ${this.loadedPlugins.size} plugins.`);
|
|
}
|
|
|
|
/**
|
|
* Get information about loaded plugins
|
|
*/
|
|
getLoadedPlugins() {
|
|
return Array.from(this.loadedPlugins.values()).map(plugin => ({
|
|
name: plugin.info.name,
|
|
version: plugin.info.version,
|
|
description: plugin.info.description,
|
|
author: plugin.info.author,
|
|
enabled: plugin.info.enabled,
|
|
loadedAt: plugin.loadedAt,
|
|
path: plugin.path
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get plugin load errors
|
|
*/
|
|
getLoadErrors() {
|
|
return Array.from(this.loadErrors.entries()).map(([name, error]) => ({
|
|
name,
|
|
error: error.message,
|
|
stack: error.stack
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Check if a plugin is loaded
|
|
* @param {string} pluginName - Plugin name
|
|
*/
|
|
isPluginLoaded(pluginName) {
|
|
return this.loadedPlugins.has(pluginName);
|
|
}
|
|
|
|
/**
|
|
* Get plugin statistics
|
|
*/
|
|
getStats() {
|
|
const registryStats = pluginRegistry.getStats();
|
|
|
|
return {
|
|
...registryStats,
|
|
loadedPlugins: this.loadedPlugins.size,
|
|
loadErrors: this.loadErrors.size,
|
|
pluginsDirectory: this.pluginsDir
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate a plugin without loading it
|
|
* @param {string} pluginPath - Path to plugin directory
|
|
*/
|
|
async validatePlugin(pluginPath) {
|
|
try {
|
|
const packageJsonPath = path.join(pluginPath, 'package.json');
|
|
const indexJsPath = path.join(pluginPath, 'index.js');
|
|
|
|
// Check required files
|
|
await fs.access(packageJsonPath);
|
|
await fs.access(indexJsPath);
|
|
|
|
// Load and validate package.json
|
|
const packageJson = require(packageJsonPath);
|
|
const validationResult = pluginValidator.validatePackageJson(packageJson);
|
|
|
|
// Check if register function exists
|
|
const pluginModule = require(pluginPath);
|
|
const hasRegisterFunction = typeof pluginModule.register === 'function';
|
|
|
|
return {
|
|
isValid: validationResult.isValid && hasRegisterFunction,
|
|
errors: [
|
|
...validationResult.errors,
|
|
...(hasRegisterFunction ? [] : ['Missing register function'])
|
|
],
|
|
warnings: validationResult.warnings,
|
|
metadata: packageJson
|
|
};
|
|
|
|
} catch (error) {
|
|
return {
|
|
isValid: false,
|
|
errors: [error.message],
|
|
warnings: [],
|
|
metadata: null
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create and export a singleton instance
|
|
const pluginLoader = new PluginLoader();
|
|
|
|
module.exports = pluginLoader;
|