const { getAllPluginDomains, getPlugin, getAllPluginRegistrations, reloadPlugin, stopPlugin, startPlugin } = require('../../../plugins/plugin-handler'); const { logError, logInfo, logDebug } = require('../../../infrastructure/logger'); const { broadcast } = require('../websocket'); const fs = require('fs').promises; const path = require('path'); async function handlePluginsRoutes(req, res) { const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname; const method = req.method; /** * Get plugin settings file path * @param {string} domain - Plugin domain * @returns {string} Settings file path */ function getPluginSettingsPath(domain) { const settingsDir = path.join(process.cwd(), 'cache', 'plugin-settings'); return path.join(settingsDir, `${domain}.json`); } /** * Load plugin settings from file * @param {string} domain - Plugin domain * @returns {Promise} Plugin settings */ async function loadPluginSettings(domain) { try { const settingsPath = getPluginSettingsPath(domain); const data = await fs.readFile(settingsPath, 'utf8'); return JSON.parse(data); } catch (err) { if (err.code === 'ENOENT') { // Settings file doesn't exist yet, return empty object return {}; } logError('PluginsRoute', `Error loading settings for ${domain}: ${err.message}`); return {}; } } /** * Save plugin settings to file * @param {string} domain - Plugin domain * @param {Object} settings - Settings to save * @returns {Promise} */ async function savePluginSettings(domain, settings) { try { const settingsPath = getPluginSettingsPath(domain); const settingsDir = path.dirname(settingsPath); // Ensure directory exists await fs.mkdir(settingsDir, { recursive: true }); // Save settings to file await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); logDebug('PluginsRoute', `Saved settings for plugin ${domain}`); } catch (err) { logError('PluginsRoute', `Error saving settings for ${domain}: ${err.message}`); throw err; } } // GET /api/plugins - List all plugins with their info if (method === 'GET' && urlPath === '/api/plugins') { try { // Get all plugin domains from disk (including stopped ones) const pluginHandler = require('../../../plugins/plugin-handler'); // We need to get all domains from disk, not just loaded ones const pluginSitesDir = path.join(process.cwd(), 'plugin-sites'); const allDomains = new Set(['p2ns.admin']); try { const entries = await fs.readdir(pluginSitesDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const domain = entry.name; const pluginDir = path.join(pluginSitesDir, domain); const configPath = path.join(pluginDir, 'config.json'); try { await fs.access(configPath); const configContent = await fs.readFile(configPath, 'utf8'); const config = JSON.parse(configContent); if (config && config.name && config.version) { allDomains.add(domain); } } catch (err) { // No valid config } } } } catch (err) { // Directory might not exist } const pluginDomains = Array.from(allDomains); const loadedDomains = getAllPluginDomains(); const registrations = getAllPluginRegistrations(); const plugins = await Promise.all(pluginDomains.map(async (domain) => { const plugin = getPlugin(domain); // Plugin might be stopped, check if it exists in plugin-sites const isLoaded = loadedDomains.includes(domain); if (!plugin) { // Check if plugin directory exists to show stopped plugin const pluginSitesDir = path.join(process.cwd(), 'plugin-sites'); const pluginDir = path.join(pluginSitesDir, domain); try { await fs.access(pluginDir); // Plugin exists but is stopped - load config to show basic info const configPath = path.join(pluginDir, 'config.json'); let config = {}; try { const configData = await fs.readFile(configPath, 'utf8'); config = JSON.parse(configData); } catch (err) { // Config might not exist } return { domain, name: config?.name || domain, version: config?.version || '1.0.0', description: config?.description || '', author: config?.author || '', homepage: config?.homepage || '', license: config?.license || '', enabled: config?.enabled !== false, // Default to true if not specified status: 'stopped', hasHandler: false, hasWww: false, hasDatabase: false, actions: [], settings: {} }; } catch (err) { return null; // Plugin directory doesn't exist } } // Load saved settings const savedSettings = await loadPluginSettings(domain); // Merge saved settings with registered settings (saved values override defaults) const registeredSettings = registrations[domain]?.settings || {}; const mergedSettings = {}; for (const [key, config] of Object.entries(registeredSettings)) { mergedSettings[key] = { ...config, value: savedSettings[key] !== undefined ? savedSettings[key] : config.default }; } return { domain, name: plugin.config?.name || domain, version: plugin.config?.version || '1.0.0', description: plugin.config?.description || '', author: plugin.config?.author || '', homepage: plugin.config?.homepage || '', license: plugin.config?.license || '', icon: plugin.config?.icon || null, enabled: plugin.config?.enabled !== false, // Default to true if not specified status: plugin.handler ? 'loaded' : 'static', hasHandler: !!plugin.handler, hasWww: !!plugin.wwwDir, hasDatabase: !!plugin.db, actions: registrations[domain]?.actions || [], settings: mergedSettings }; })); const filteredPlugins = plugins.filter(Boolean); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ plugins: filteredPlugins })); } catch (err) { logError('PluginsRoute', `Error listing plugins: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to list plugins' })); } return true; } // GET /api/plugins/:domain - Get specific plugin info if (method === 'GET' && urlPath.startsWith('/api/plugins/') && !urlPath.includes('/actions/') && !urlPath.includes('/settings') && !urlPath.includes('/reload') && !urlPath.includes('/stop') && !urlPath.includes('/start')) { try { const domain = urlPath.split('/api/plugins/')[1]; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Domain parameter required' })); return true; } const plugin = getPlugin(domain); if (!plugin) { // Check if plugin exists but is stopped const pluginSitesDir = path.join(process.cwd(), 'plugin-sites'); const pluginDir = path.join(pluginSitesDir, domain); try { await fs.access(pluginDir); const configPath = path.join(pluginDir, 'config.json'); let config = {}; try { const configData = await fs.readFile(configPath, 'utf8'); config = JSON.parse(configData); } catch (err) { // Config might not exist } const pluginInfo = { domain, name: config?.name || domain, version: config?.version || '1.0.0', description: config?.description || '', author: config?.author || '', homepage: config?.homepage || '', license: config?.license || '', status: 'stopped', hasHandler: false, hasWww: false, hasDatabase: false, pluginDir, actions: [], settings: {} }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(pluginInfo)); return true; } catch (err) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Plugin not found' })); return true; } } const registrations = getAllPluginRegistrations(); // Load saved settings const savedSettings = await loadPluginSettings(domain); // Merge saved settings with registered settings const registeredSettings = registrations[domain]?.settings || {}; const mergedSettings = {}; for (const [key, config] of Object.entries(registeredSettings)) { mergedSettings[key] = { ...config, value: savedSettings[key] !== undefined ? savedSettings[key] : config.default }; } const pluginInfo = { domain, name: plugin.config?.name || domain, version: plugin.config?.version || '1.0.0', description: plugin.config?.description || '', author: plugin.config?.author || '', homepage: plugin.config?.homepage || '', license: plugin.config?.license || '', status: plugin.handler ? 'loaded' : 'static', hasHandler: !!plugin.handler, hasWww: !!plugin.wwwDir, hasDatabase: !!plugin.db, pluginDir: plugin.pluginDir, actions: registrations[domain]?.actions || [], settings: mergedSettings }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(pluginInfo)); } catch (err) { logError('PluginsRoute', `Error getting plugin info: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to get plugin info' })); } return true; } // POST /api/plugins/:domain/reload - Reload a plugin if (method === 'POST' && urlPath.includes('/reload') && !urlPath.includes('/stop') && !urlPath.includes('/start')) { try { const domain = urlPath.split('/api/plugins/')[1]?.split('/reload')[0]; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Domain parameter required' })); return true; } logInfo('PluginsRoute', `Reloading plugin ${domain} via API`); const reloadedPlugin = await reloadPlugin(domain); if (!reloadedPlugin) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to reload plugin' })); return true; } // Broadcast update to connected admin clients broadcast({ type: 'update-plugins', domain }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Plugin ${domain} reloaded successfully`, domain })); } catch (err) { logError('PluginsRoute', `Error reloading plugin: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Failed to reload plugin: ${err.message}` })); } return true; } // POST /api/plugins/:domain/stop - Stop a plugin if (method === 'POST' && urlPath.includes('/stop')) { try { const domain = urlPath.split('/api/plugins/')[1]?.split('/stop')[0]; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Domain parameter required' })); return true; } // Check if this is a system plugin that cannot be stopped const SYSTEM_PLUGINS = ['global.profile']; if (SYSTEM_PLUGINS.includes(domain)) { logWarn('PluginsRoute', `Cannot stop system plugin ${domain}`); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Cannot stop system plugin: ${domain}. This plugin is required by the system.`, domain, isSystemPlugin: true })); return true; } logInfo('PluginsRoute', `Stopping plugin ${domain} via API`); try { const success = await stopPlugin(domain); if (!success) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to stop plugin' })); return true; } } catch (err) { logError('PluginsRoute', `Error stopping plugin: ${err.message}`); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); return true; } // Broadcast update to connected admin clients broadcast({ type: 'update-plugins', domain }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Plugin ${domain} stopped successfully`, domain })); } catch (err) { logError('PluginsRoute', `Error stopping plugin: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Failed to stop plugin: ${err.message}` })); } return true; } // POST /api/plugins/:domain/toggle - Enable/disable a plugin if (method === 'POST' && urlPath.includes('/toggle')) { try { const domain = urlPath.split('/api/plugins/')[1]?.split('/toggle')[0]; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Domain parameter required' })); return true; } // Read request body to get enabled state let body = ''; req.on('data', chunk => { body += chunk.toString(); }); await new Promise(resolve => req.on('end', resolve)); const data = body ? JSON.parse(body) : {}; const enabled = data.enabled !== undefined ? data.enabled : true; // Check if this is a system plugin that cannot be disabled const SYSTEM_PLUGINS = ['global.profile']; if (!enabled && SYSTEM_PLUGINS.includes(domain)) { logWarn('PluginsRoute', `Cannot disable system plugin ${domain}`); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Cannot disable system plugin: ${domain}. This plugin is required by the system.`, domain, isSystemPlugin: true })); return true; } logInfo('PluginsRoute', `${enabled ? 'Enabling' : 'Disabling'} plugin ${domain} via API`); // Get plugin directory const pluginSitesDir = path.join(process.cwd(), 'plugin-sites'); const pluginDir = path.join(pluginSitesDir, domain); const configPath = path.join(pluginDir, 'config.json'); // Read current config let config = {}; try { const configData = await fs.readFile(configPath, 'utf8'); config = JSON.parse(configData); } catch (err) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Plugin config.json not found' })); return true; } // Update enabled flag (system plugins are always enabled) if (SYSTEM_PLUGINS.includes(domain)) { config.enabled = true; } else { config.enabled = enabled; } // Write updated config await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); logInfo('PluginsRoute', `Updated config.json for ${domain}: enabled=${enabled}`); // Apply the enabled/disabled state by reloading, starting, or stopping the plugin const pluginHandler = require('../../../plugins/plugin-handler'); const plugin = pluginHandler.getPlugin(domain); if (enabled) { // Enabling the plugin if (plugin) { // Plugin is currently loaded - reload it to ensure it's properly enabled // This will read the fresh config.json with enabled=true const reloaded = await pluginHandler.reloadPlugin(domain); if (!reloaded) { // Reload failed (might be disabled in config still) - try starting fresh logInfo('PluginsRoute', `Reload failed for ${domain}, attempting fresh start`); await pluginHandler.startPlugin(domain); } } else { // Plugin is not loaded - start it (this will read config.json with enabled=true) await pluginHandler.startPlugin(domain); } } else { // Disabling the plugin if (plugin) { // Plugin is loaded - stop it (this will clear caches and remove from internal domains) await pluginHandler.stopPlugin(domain); } else { // Plugin is not loaded - just clear caches to remove from internal domains pluginHandler.clearInternalDomainsCache(); try { const { invalidateEntriesCache } = require('../../../core/core'); invalidateEntriesCache(); logDebug('PluginsRoute', 'DNS cache invalidated after disabling plugin'); } catch (err) { logDebug('PluginsRoute', `Could not invalidate DNS cache: ${err.message}`); } // Update proxy server certificates to remove internal domain try { const { updateProxyServerCertificates } = require('../../../networking/internal_domains_proxy'); await updateProxyServerCertificates(); logDebug('PluginsRoute', 'Proxy server certificates updated after disabling plugin'); } catch (err) { logWarn('PluginsRoute', `Could not update proxy server certificates: ${err.message}`); } } } // Broadcast update to connected admin clients broadcast({ type: 'update-plugins', domain }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Plugin ${domain} ${enabled ? 'enabled' : 'disabled'} successfully`, domain, enabled })); } catch (err) { logError('PluginsRoute', `Error toggling plugin: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Failed to toggle plugin: ${err.message}` })); } return true; } // POST /api/plugins/:domain/start - Start a plugin if (method === 'POST' && urlPath.includes('/start')) { try { const domain = urlPath.split('/api/plugins/')[1]?.split('/start')[0]; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Domain parameter required' })); return true; } logInfo('PluginsRoute', `Starting plugin ${domain} via API`); const startedPlugin = await startPlugin(domain); if (!startedPlugin) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to start plugin' })); return true; } // Broadcast update to connected admin clients broadcast({ type: 'update-plugins', domain }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Plugin ${domain} started successfully`, domain })); } catch (err) { logError('PluginsRoute', `Error starting plugin: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Failed to start plugin: ${err.message}` })); } return true; } // POST /api/plugins/:domain/actions/:actionName - Execute a plugin action if (method === 'POST' && urlPath.includes('/actions/')) { try { const match = urlPath.match(/\/api\/plugins\/([^\/]+)\/actions\/(.+)$/); if (!match) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid action path' })); return true; } const domain = match[1]; const actionName = match[2]; const plugin = getPlugin(domain); if (!plugin) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Plugin not found' })); return true; } const registrations = getAllPluginRegistrations(); const action = registrations[domain]?.actions?.find(a => a.name === actionName); if (!action || !action.handler) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Action not found' })); return true; } // Parse request body for parameters let body = ''; req.on('data', chunk => { body += chunk.toString(); }); await new Promise(resolve => req.on('end', resolve)); const params = body ? JSON.parse(body) : {}; const { validatePluginActionParams } = require('../../../plugins/plugin-handler'); const validation = validatePluginActionParams(domain, actionName, params); if (!validation.valid) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid action parameters', details: validation.errors })); return true; } // Execute the action (handler may be a proxy function for child process) try { const result = await action.handler(validation.params); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, result })); } catch (err) { logError('PluginsRoute', `Error executing action ${actionName} for plugin ${domain}: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Action execution failed: ${err.message}` })); } } catch (err) { logError('PluginsRoute', `Error handling action request: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to execute action' })); } return true; } // POST /api/plugins/:domain/settings - Update plugin settings if (method === 'POST' && urlPath.includes('/settings') && !urlPath.includes('/actions/')) { try { const domain = urlPath.split('/api/plugins/')[1]?.split('/settings')[0]; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Domain parameter required' })); return true; } // Parse request body let body = ''; req.on('data', chunk => { body += chunk.toString(); }); await new Promise(resolve => req.on('end', resolve)); const settings = body ? JSON.parse(body) : {}; // Save settings to file await savePluginSettings(domain, settings); // Broadcast update broadcast({ type: 'update-plugin-settings', domain }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Settings updated for plugin ${domain}` })); } catch (err) { logError('PluginsRoute', `Error updating plugin settings: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to update settings' })); } return true; } return false; } module.exports = { handlePluginsRoutes };