mirror of
https://github.com/DumbWareio/DumbAssets.git
synced 2026-02-20 00:24:29 +08:00
- Add complete Home Assistant integration with schema-driven configuration - Support for importing HA entities (devices/sensors) as assets - Intelligent categorization based on entity domains (light, sensor, switch, etc.) - Device filtering and bulk import functionality - Custom SVG logo and branded CSS styles - API endpoints for connection testing, device listing, and import - Automatic conversion of HA entities to DumbAssets format with custom fields - Registered in integration manager following established patterns
304 lines
11 KiB
JavaScript
304 lines
11 KiB
JavaScript
/**
|
|
* Integration Manager - Server-side integration registry and manager
|
|
* Handles registration, configuration, and endpoint management for all integrations
|
|
*/
|
|
|
|
const { TOKENMASK } = require('../src/constants');
|
|
const PaperlessIntegration = require('./paperless'); // Import Paperless schema
|
|
const PapraIntegration = require('./papra'); // Import Papra schema
|
|
const HomeAssistantIntegration = require('./homeassistant'); // Import Home Assistant schema
|
|
|
|
class IntegrationManager {
|
|
constructor() {
|
|
this.integrations = new Map();
|
|
this.registerBuiltInIntegrations();
|
|
}
|
|
|
|
/**
|
|
* Register built-in integrations
|
|
*/
|
|
registerBuiltInIntegrations() {
|
|
// Register Paperless NGX integration
|
|
this.registerIntegration('paperless', PaperlessIntegration.SCHEMA);
|
|
|
|
// Register Papra integration
|
|
this.registerIntegration('papra', PapraIntegration.SCHEMA);
|
|
|
|
// Register Home Assistant integration
|
|
this.registerIntegration('homeassistant', HomeAssistantIntegration.SCHEMA);
|
|
|
|
// Future integrations can be added here
|
|
// this.registerIntegration('nextcloud', { ... });
|
|
// this.registerIntegration('sharepoint', { ... });
|
|
}
|
|
|
|
/**
|
|
* Register routes for all integrations
|
|
* @param {Object} app - Express application instance
|
|
* @param {Function} getSettings - Function to retrieve application settings
|
|
* This method registers all integration-specific routes
|
|
* by calling their static `registerRoutes` methods. which is ultimately called from server.js.
|
|
* It allows each integration to define its own API endpoints
|
|
*/
|
|
registerRoutes(app, getSettings) {
|
|
PaperlessIntegration.registerRoutes(app, getSettings);
|
|
PapraIntegration.registerRoutes(app, getSettings);
|
|
HomeAssistantIntegration.registerRoutes(app, getSettings);
|
|
// Future integrations can register their routes here
|
|
}
|
|
|
|
/**
|
|
* Register a new integration
|
|
* @param {string} id - Unique integration identifier
|
|
* @param {Object} config - Integration configuration
|
|
*/
|
|
registerIntegration(id, config) {
|
|
const integration = {
|
|
id,
|
|
name: config.name,
|
|
description: config.description || '',
|
|
version: config.version || '1.0.0',
|
|
enabled: config.enabled || false,
|
|
icon: config.icon || 'gear',
|
|
logoHref: config.logoHref || null, // Optional logo URL for frontend display
|
|
colorScheme: config.colorScheme || 'default', // Default color scheme for UI
|
|
category: config.category || 'general',
|
|
|
|
// Configuration schema for settings UI
|
|
configSchema: config.configSchema || {},
|
|
|
|
// Default configuration values
|
|
defaultConfig: config.defaultConfig || {},
|
|
|
|
// API endpoints this integration provides
|
|
endpoints: config.endpoints || [],
|
|
|
|
// Middleware functions
|
|
middleware: config.middleware || {},
|
|
|
|
// Validation functions
|
|
validators: config.validators || {},
|
|
|
|
// Status check function
|
|
statusCheck: config.statusCheck || null,
|
|
|
|
// Integration-specific metadata
|
|
metadata: config.metadata || {}
|
|
};
|
|
|
|
this.integrations.set(id, integration);
|
|
console.log(`✅ Registered integration: ${integration.name} (${id})`);
|
|
return integration;
|
|
}
|
|
|
|
/**
|
|
* Get all registered integrations
|
|
*/
|
|
getAllIntegrations() {
|
|
return Array.from(this.integrations.values());
|
|
}
|
|
|
|
/**
|
|
* Get a specific integration by ID
|
|
*/
|
|
getIntegration(id) {
|
|
return this.integrations.get(id);
|
|
}
|
|
|
|
/**
|
|
* Get enabled integrations only
|
|
*/
|
|
getEnabledIntegrations() {
|
|
return Array.from(this.integrations.values()).filter(integration => integration.enabled);
|
|
}
|
|
|
|
/**
|
|
* Get integrations by category
|
|
*/
|
|
getIntegrationsByCategory(category) {
|
|
return Array.from(this.integrations.values()).filter(integration => integration.category === category);
|
|
}
|
|
|
|
/**
|
|
* Update integration configuration
|
|
*/
|
|
updateIntegrationConfig(id, config) {
|
|
const integration = this.integrations.get(id);
|
|
if (!integration) {
|
|
throw new Error(`Integration not found: ${id}`);
|
|
}
|
|
|
|
// Merge with existing config
|
|
integration.config = { ...integration.defaultConfig, ...config };
|
|
integration.enabled = config.enabled || false;
|
|
|
|
console.log(`🔄 Updated integration config: ${integration.name}`);
|
|
return integration;
|
|
}
|
|
|
|
/**
|
|
* Validate integration configuration
|
|
*/
|
|
validateConfig(id, config) {
|
|
const integration = this.integrations.get(id);
|
|
if (!integration) {
|
|
throw new Error(`Integration not found: ${id}`);
|
|
}
|
|
|
|
const validator = integration.validators.configValidator;
|
|
if (validator) {
|
|
return validator(config);
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Check integration status/health
|
|
*/
|
|
async checkIntegrationStatus(id, config) {
|
|
const integration = this.integrations.get(id);
|
|
if (!integration || !integration.statusCheck) {
|
|
return { status: 'unknown', message: 'No status check available' };
|
|
}
|
|
|
|
try {
|
|
return await integration.statusCheck(config);
|
|
} catch (error) {
|
|
return { status: 'error', message: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize configuration for frontend (mask sensitive fields)
|
|
*/
|
|
sanitizeConfigForFrontend(id, config) {
|
|
const integration = this.integrations.get(id);
|
|
if (!integration) return config;
|
|
|
|
const sanitized = { ...config };
|
|
const schema = integration.configSchema;
|
|
|
|
// Mask sensitive fields
|
|
for (const [fieldName, fieldConfig] of Object.entries(schema)) {
|
|
if (fieldConfig.sensitive && sanitized[fieldName]) {
|
|
sanitized[fieldName] = TOKENMASK;
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Get integration configuration for frontend settings
|
|
*/
|
|
getIntegrationsForSettings() {
|
|
return Array.from(this.integrations.values()).map(integration => ({
|
|
id: integration.id,
|
|
name: integration.name,
|
|
description: integration.description,
|
|
icon: integration.icon,
|
|
logoHref: integration.logoHref,
|
|
colorScheme: integration.colorScheme || 'default',
|
|
category: integration.category,
|
|
configSchema: integration.configSchema,
|
|
defaultConfig: integration.defaultConfig,
|
|
endpoints: integration.endpoints,
|
|
metadata: integration.metadata
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Apply integration settings updates, handling sensitive data preservation and validation
|
|
*/
|
|
applyIntegrationSettings(serverConfig, updatedConfig) {
|
|
if (!updatedConfig.integrationSettings) {
|
|
return updatedConfig;
|
|
}
|
|
|
|
for (const [integrationId, newConfig] of Object.entries(updatedConfig.integrationSettings)) {
|
|
const integration = this.getIntegration(integrationId);
|
|
if (!integration) {
|
|
console.warn(`Unknown integration in settings: ${integrationId}`);
|
|
continue;
|
|
}
|
|
|
|
const serverIntegrationConfig = serverConfig.integrationSettings?.[integrationId] || {};
|
|
const schema = integration.configSchema;
|
|
|
|
// Handle sensitive field preservation and validation
|
|
for (const [fieldName, fieldConfig] of Object.entries(schema)) {
|
|
const newValue = newConfig[fieldName];
|
|
|
|
if (fieldConfig.sensitive && newValue === TOKENMASK) {
|
|
// Preserve existing sensitive value if token mask is present
|
|
if (serverIntegrationConfig[fieldName]) {
|
|
newConfig[fieldName] = serverIntegrationConfig[fieldName];
|
|
} else {
|
|
newConfig[fieldName] = '';
|
|
}
|
|
}
|
|
|
|
// Validate and sanitize URL fields
|
|
if (fieldConfig.type === 'url' && newValue && newValue.trim()) {
|
|
const trimmedUrl = newValue.trim();
|
|
if (!/^https?:\/\//i.test(trimmedUrl)) {
|
|
throw new Error(`Invalid ${integration.name} ${fieldConfig.label}: URL must start with http:// or https://`);
|
|
}
|
|
// Remove trailing slash for consistency
|
|
newConfig[fieldName] = trimmedUrl.endsWith('/')
|
|
? trimmedUrl.slice(0, -1)
|
|
: trimmedUrl;
|
|
}
|
|
}
|
|
|
|
// Run integration-specific validation
|
|
const validation = this.validateConfig(integrationId, newConfig);
|
|
if (!validation.valid) {
|
|
throw new Error(`${integration.name} configuration error: ${validation.errors.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
return updatedConfig;
|
|
}
|
|
|
|
/**
|
|
* Prepare configuration for testing, handling masked sensitive fields
|
|
*/
|
|
async prepareConfigForTesting(integrationId, testConfig, getSettings) {
|
|
const integration = this.getIntegration(integrationId);
|
|
if (!integration) {
|
|
throw new Error(`Integration not found: ${integrationId}`);
|
|
}
|
|
|
|
const preparedConfig = { ...testConfig };
|
|
const schema = integration.configSchema;
|
|
|
|
// Handle masked sensitive fields
|
|
for (const [fieldName, fieldConfig] of Object.entries(schema)) {
|
|
if (fieldConfig.sensitive && preparedConfig[fieldName] === TOKENMASK) {
|
|
try {
|
|
// Get the actual stored value
|
|
const settings = await getSettings();
|
|
const storedConfig = settings.integrationSettings?.[integrationId];
|
|
|
|
if (storedConfig?.[fieldName] && storedConfig[fieldName] !== TOKENMASK) {
|
|
preparedConfig[fieldName] = storedConfig[fieldName];
|
|
} else {
|
|
throw new Error(`No stored ${fieldConfig.label || fieldName} found. Please enter a new value.`);
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Failed to retrieve stored ${fieldConfig.label || fieldName}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return preparedConfig;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
const integrationManager = new IntegrationManager();
|
|
|
|
module.exports = { IntegrationManager, integrationManager };
|