From 11f47c057939cd0ebf4db2049171476d426f6dcb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 7 Oct 2025 14:23:17 +0100 Subject: [PATCH 1/4] Add FF exclusive nodes feature flag --- forge/ee/lib/index.js | 1 + forge/routes/api/deviceLive.js | 64 ++++++---- forge/routes/api/project.js | 66 +++++----- forge/routes/api/settings.js | 6 +- forge/settings/defaults.js | 10 +- .../src/pages/admin/CertifiedNodes/index.vue | 117 ++++-------------- .../TeamTypes/dialogs/TeamTypeEditDialog.vue | 1 + frontend/src/pages/admin/routes.js | 2 +- frontend/src/store/modules/account/index.js | 2 + frontend/src/store/modules/ux/index.js | 2 +- 10 files changed, 112 insertions(+), 159 deletions(-) diff --git a/forge/ee/lib/index.js b/forge/ee/lib/index.js index 4f13e2ad6f..4b7fc5ba40 100644 --- a/forge/ee/lib/index.js +++ b/forge/ee/lib/index.js @@ -31,6 +31,7 @@ module.exports = fp(async function (app, opts) { app.decorate('tables', await require('./tables').init(app)) } app.config.features.register('certifiedNodes', true, true) + app.config.features.register('ffNodes', true, true) app.config.features.register('rbacApplication', true, true) } diff --git a/forge/routes/api/deviceLive.js b/forge/routes/api/deviceLive.js index a6ef9290d4..bf2cd4417e 100644 --- a/forge/routes/api/deviceLive.js +++ b/forge/routes/api/deviceLive.js @@ -351,36 +351,46 @@ module.exports = async function (app) { } // Platform wide catalogue and npm registry - const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes') && !!teamType.getFeatureProperty('certifiedNodes', false) - if (platformNPMEnabled) { - const npmRegURLString = app.settings.get('platform:certifiedNodes:npmRegistryURL') - const token = app.settings.get('platform:certifiedNodes:token') - const catalogueString = app.settings.get('platform:certifiedNodes:catalogueURL') - if (npmRegURLString && token && catalogueString) { - const npmRegURL = new URL(npmRegURLString) - const catalogue = new URL(catalogueString) - if (!response.palette) { - response.palette = {} + const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes', false) && + !!app.config.features.enabled('ffNodes', false) && + !!app.settings.get('platform:ff-npm-registry:token') + const certifiedNodesEnabledForTeam = teamType.getFeatureProperty('certifiedNodes', false) + const ffNodesEnabledForTeam = teamType.getFeatureProperty('ffNodes', false) + + if (platformNPMEnabled && (certifiedNodesEnabledForTeam || ffNodesEnabledForTeam)) { + try { + const npmRegURL = new URL(app.settings.get('platform:ff-npm-registry:url') || 'https://registry.flowfuse.com/') + const token = app.settings.get('platform:ff-npm-registry:token') + + const certNodesCatalogue = app.settings.get('platform:ff-npm-registry:catalogue:certifiedNodes') || 'https://ff-certified-nodes.flowfuse.cloud/catalogue.json' + const ffNodesCatalogue = app.settings.get('platform:ff-npm-registry:catalogue:ffNodes') || 'https://ff-certified-nodes.flowfuse.cloud/ff-catalogue.json' + + // Handle FF Exclusive Nodes + + if (certNodesCatalogue || ffNodesCatalogue) { + // At least one is configured - so initialise the settings + response.palette = response.palette || {} + response.palette.catalogue = response.palette.catalogue || [] + } + function updateSettingsForCatalogue (scope, catalogueString) { + const catalogue = new URL(catalogueString) + response.palette.catalogue.push(catalogue.toString()) + const npmrcEntry = `${scope}:registry=${npmRegURL.toString()}\n` + + `//${npmRegURL.host}:_auth="${token}"\n` + if (response.palette.npmrc) { + response.palette.npmrc += '\n' + npmrcEntry + } else { + response.palette.npmrc = npmrcEntry + } } - if (response.palette?.catalogues) { - response.palette.catalogues - .push(catalogue.toString()) - } else { - response.palette.catalogues = [ - catalogue.toString() - ] + if (certifiedNodesEnabledForTeam && certNodesCatalogue) { + updateSettingsForCatalogue('@flowfuse-certified-nodes', certNodesCatalogue) } - if (response.palette?.npmrc) { - response.palette.npmrc = `${response.palette.npmrc}\n` + - `@flowfuse-certified-nodes:registry=${npmRegURL.toString()}\n` + - `@flowfuse-nodes:registry=${npmRegURL.toString()}\n` + - `//${npmRegURL.host}:_auth="${token}"\n` - } else { - response.palette.npmrc = - `@flowfuse-certified-nodes:registry=${npmRegURL.toString()}\n` + - `@flowfuse-nodes:registry=${npmRegURL.toString()}\n` + - `//${npmRegURL.host}:_auth="${token}"\n` + if (ffNodesEnabledForTeam && ffNodesCatalogue) { + updateSettingsForCatalogue('@flowfuse-nodes', ffNodesCatalogue) } + } catch (err) { + app.log.error('Failed to configure platform npm registry for device', err) } } diff --git a/forge/routes/api/project.js b/forge/routes/api/project.js index 4ed5ccba9c..896d971eb6 100644 --- a/forge/routes/api/project.js +++ b/forge/routes/api/project.js @@ -910,36 +910,46 @@ module.exports = async function (app) { } // Platform wide catalogue and npm registry - const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes') && !!teamType.getFeatureProperty('certifiedNodes', false) - if (platformNPMEnabled) { - const npmRegURLString = app.settings.get('platform:certifiedNodes:npmRegistryURL') - const token = app.settings.get('platform:certifiedNodes:token') - const catalogueString = app.settings.get('platform:certifiedNodes:catalogueURL') - if (npmRegURLString && token && catalogueString) { - const npmRegURL = new URL(npmRegURLString) - const catalogue = new URL(catalogueString) - if (!settings.settings?.palette) { - settings.settings.palette = {} - } - if (settings.settings?.palette?.catalogue) { - settings.settings.palette.catalogue - .push(catalogue.toString()) - } else { - settings.settings.palette.catalogue = [ - catalogue.toString() - ] + const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes', false) && + !!app.config.features.enabled('ffNodes', false) && + !!app.settings.get('platform:ff-npm-registry:token') + + const certifiedNodesEnabledForTeam = teamType.getFeatureProperty('certifiedNodes', false) + const ffNodesEnabledForTeam = teamType.getFeatureProperty('ffNodes', false) + if (platformNPMEnabled && (certifiedNodesEnabledForTeam || ffNodesEnabledForTeam)) { + try { + const npmRegURL = new URL(app.settings.get('platform:ff-npm-registry:url') || 'https://registry.flowfuse.com/') + const token = app.settings.get('platform:ff-npm-registry:token') + + const certNodesCatalogue = app.settings.get('platform:ff-npm-registry:catalogue:certifiedNodes') || 'https://ff-certified-nodes.flowfuse.cloud/catalogue.json' + const ffNodesCatalogue = app.settings.get('platform:ff-npm-registry:catalogue:ffNodes') || 'https://ff-certified-nodes.flowfuse.cloud/ff-catalogue.json' + + // Handle FF Exclusive Nodes + + if (certNodesCatalogue || ffNodesCatalogue) { + // At least one is configured - so initialise the settings + settings.settings.palette = settings.settings.palette || {} + settings.settings.palette.catalogue = settings.settings.palette.catalogue || [] + } + function updateSettingsForCatalogue (scope, catalogueString) { + const catalogue = new URL(catalogueString) + settings.settings.palette.catalogue.push(catalogue.toString()) + const npmrcEntry = `${scope}:registry=${npmRegURL.toString()}\n` + + `//${npmRegURL.host}:_auth="${token}"\n` + if (settings.settings.palette.npmrc) { + settings.settings.palette.npmrc += '\n' + npmrcEntry + } else { + settings.settings.palette.npmrc = npmrcEntry + } } - if (settings.settings?.palette?.npmrc) { - settings.settings.palette.npmrc = `${settings.settings.palette.npmrc}\n` + - `@flowfuse-certified-nodes:registry=${npmRegURL.toString()}\n` + - `@flowfuse-nodes:registry=${npmRegURL.toString()}\n` + - `//${npmRegURL.host}:_auth="${token}"\n` - } else { - settings.settings.palette.npmrc = - `@flowfuse-certified-nodes:registry=${npmRegURL.toString()}\n` + - `@flowfuse-nodes:registry=${npmRegURL.toString()}\n` + - `//${npmRegURL.host}:_auth="${token}"\n` + if (certifiedNodesEnabledForTeam && certNodesCatalogue) { + updateSettingsForCatalogue('@flowfuse-certified-nodes', certNodesCatalogue) } + if (ffNodesEnabledForTeam && ffNodesCatalogue) { + updateSettingsForCatalogue('@flowfuse-nodes', ffNodesCatalogue) + } + } catch (err) { + app.log.error('Failed to configure platform npm registry for device', err) } } diff --git a/forge/routes/api/settings.js b/forge/routes/api/settings.js index 8e43f5beaf..c47ea7eb6f 100644 --- a/forge/routes/api/settings.js +++ b/forge/routes/api/settings.js @@ -89,10 +89,8 @@ module.exports = async function (app) { } }) response['platform:stats:token'] = app.settings.get('platform:stats:token') - if (app.config.features.enabled('certifiedNodes')) { - response['platform:certifiedNodes:npmRegistryURL'] = app.settings.get('platform:certifiedNodes:npmRegistryURL') - response['platform:certifiedNodes:token'] = app.settings.get('platform:certifiedNodes:token') - response['platform:certifiedNodes:catalogueURL'] = app.settings.get('platform:certifiedNodes:catalogueURL') + if (app.config.features.enabled('certifiedNodes') || app.config.features.enabled('ffNodes')) { + response['platform:ff-npm-registry:enabled'] = !!app.settings.get('platform:ff-npm-registry:token') } } if (app.config.features.enabled('sso') && app.settings.get('platform:sso:google') && app.settings.get('platform:sso:google:clientId')) { diff --git a/forge/settings/defaults.js b/forge/settings/defaults.js index 76d6e680f1..79a9e29a8c 100644 --- a/forge/settings/defaults.js +++ b/forge/settings/defaults.js @@ -71,9 +71,9 @@ module.exports = { 'platform:sso:google:clientId': null, // Client ID for Google SSO 'platform:sso:direct': false, // Direct SSO Login - // Certified Nodes - 'platform:certifiedNodes:npmRegistryURL': null, // NPM registry URL for certified nodes - 'platform:certifiedNodes:token': null, // Token for certified nodes - 'platform:certifiedNodes:catalogueURL': null // Catalogue URL for certified nodes - + // FlowFuse npm registry + 'platform:ff-npm-registry:url': null, + 'platform:ff-npm-registry:token': null, + 'platform:ff-npm-registry:catalogue:certifiedNodes': null, + 'platform:ff-npm-registry:catalogue:ffNodes': null } diff --git a/frontend/src/pages/admin/CertifiedNodes/index.vue b/frontend/src/pages/admin/CertifiedNodes/index.vue index 28f61dc488..4121fdc157 100644 --- a/frontend/src/pages/admin/CertifiedNodes/index.vue +++ b/frontend/src/pages/admin/CertifiedNodes/index.vue @@ -1,26 +1,15 @@