BREAKING: All nodes must upgrade together. Legacy p2ns.core-request string messages are rejected; invite/consensus control plane uses protomux-rpc (invite.request, invite.ack, invite.relay*, consensus.*). Shared Corestore namespaces are the default for plugin DBs and drives; USE_SHARED_CORESTORE_NAMESPACES=false is debug-only. Admin plugin actions with params are validated before run. EXPERIMENTAL: End-to-end RPC invite path reuses existing handlers via adapters; invite wire still ships on the invite channel. Multi-peer invite/relay integration tests are not in CI yet (core-rpc-smoke only). SDK & channels: - channel-rpc.js, sdk.channels.rpc (register/request/event) - core-rpc.js for p2ns.core; action-params + plugin route validation - sdk.db.getCore/reopen, sdk.state.getPeerChannelSnapshot, sdk.metrics.getHolepunchStats (schema v1) Runtime: - p2ns.js: RPC-first core invite/consensus; Hyperswarm firewall/reconnect - channel-manager: required protomux-rpc per peer - db-shared-namespace-migration; drive-manager shared namespaces - proxy-server: invite.request RPC for joiners Plugins: file.drop, global.profile, peer.directory, domain.consensus, peer.visualize, example.plugin (RPC demo); peer.directory UI polish Also: CI/smoke scripts, diagnostics hardening, plugin config schema validation, admin action param modal, docs/RFCS, package-lock + engines.node >= 18
659 lines
24 KiB
JavaScript
659 lines
24 KiB
JavaScript
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<Object>} 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<void>}
|
|
*/
|
|
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 };
|
|
|