Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions forge/ee/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
63 changes: 36 additions & 27 deletions forge/routes/api/deviceLive.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,36 +351,45 @@ 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 token = app.settings.get('platform:ff-npm-registry:token')
const npmRegURL = new URL(app.config['ff-npm-registry']?.url || 'https://registry.flowfuse.com/')
const certNodesCatalogue = app.config['ff-npm-registry']?.catalogue?.certifiedNodes || 'https://ff-certified-nodes.flowfuse.cloud/catalogue.json'
const ffNodesCatalogue = app.config['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.catalogues = response.palette.catalogues || []
}
function updateSettingsForCatalogue (scope, catalogueString) {
const catalogue = new URL(catalogueString)
response.palette.catalogues.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)
}
}

Expand Down
65 changes: 37 additions & 28 deletions forge/routes/api/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,36 +910,45 @@ 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 token = app.settings.get('platform:ff-npm-registry:token')
const npmRegURL = new URL(app.config['ff-npm-registry']?.url || 'https://registry.flowfuse.com/')
const certNodesCatalogue = app.config['ff-npm-registry']?.catalogue?.certifiedNodes || 'https://ff-certified-nodes.flowfuse.cloud/catalogue.json'
const ffNodesCatalogue = app.config['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)
}
}

Expand Down
6 changes: 2 additions & 4 deletions forge/routes/api/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
11 changes: 6 additions & 5 deletions forge/settings/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ 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:token': null
// The following properties can be overriden in the config yml file to point to a local registry for testing
// 'ff-npm-registry.url'
// 'ff-npm-registry.catalogue.certifiedNodes'
// 'ff-npm-registry.catalogue.ffNodes'
}
117 changes: 24 additions & 93 deletions frontend/src/pages/admin/CertifiedNodes/index.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
<template>
<ff-page>
<template #header>
<ff-page-header title="Certified Nodes" :tabs="sideNavigation" />
<ff-page-header title="FlowFuse Nodes" :tabs="sideNavigation" />
</template>
<div class="flex-grow">
<ff-loading v-if="loading" message="Saving Settings..." />
<FormRow v-model="input['platform:certifiedNodes:npmRegistryURL']" :label="'NPM Registry URL'" :error="errors['platform:certifiedNodes:npmRegistryURL']">
NPM Registry URL for Certified Nodes
<FormRow v-model="input.registryToken" type="password" :label="'FlowFuse Registry Token'">
Access token for the FlowFuse NPM registry
<template #description>
The URL of the NPM registry to use for certified nodes.
</template>
</FormRow>
<FormRow v-model="input['platform:certifiedNodes:token']" :label="'NPM Registry Token'" :error="errors['platform:certifiedNodes:token']">
NPM Registry Authentication Token
<template #description>
The authentication token for the NPM registry to use for certified nodes.
</template>
</FormRow>
<FormRow v-model="input['platform:certifiedNodes:catalogueURL']" :label="'Node-RED Catalogue URL'" :error="errors['platform:certifiedNodes:catalogueURL']">
Node-RED Catalogue URL
<template #description>
The URL of the Node-RED catalogue to use for certified nodes.
Access to Certified Nodes, or FlowFuse Exclusive nodes requires an access token for the FlowFuse NPM registry.
To obtain an access token, please contact <a target="_blank" class="underline" href="https://flowfuse.com/support">FlowFuse Support</a>.
</template>
</FormRow>
<div class="pt-8">
Expand All @@ -37,25 +26,20 @@ import settingsApi from '../../../api/settings.js'
import FormRow from '../../../components/FormRow.vue'
import Alerts from '../../../services/alerts.js'

const validSettings = [
'platform:certifiedNodes:npmRegistryURL',
'platform:certifiedNodes:token',
'platform:certifiedNodes:catalogueURL'
]

export default {
name: 'AdminCertifiedNodes',
components: {
FormRow
},
data () {
return {
placeholderToken: '********',
loading: false,
input: {},
input: {
registryToken: ''
},
errors: {
'platform:certifiedNodes:npmRegistryURL': '',
'platform:certifiedNodes:token': '',
'platform:certifiedNodes:catalogueURL': ''
'platform:ff-npm-registry:token': ''
}
}
},
Expand All @@ -64,86 +48,33 @@ export default {
sideNavigation () {
return []
},
isLicensed () {
return !!this.settings['platform:licensed']
},
saveEnabled () {
let result = false
if (this.validate()) {
validSettings.forEach((s) => {
result = result || (this.input[s] !== this.settings[s])
})
if (this.settings['platform:ff-npm-registry:enabled']) {
return this.input.registryToken !== this.placeholderToken
}
return result
return this.input.registryToken !== ''
}
},
created () {
validSettings.forEach(s => {
this.input[s] = this.settings[s]
})
if (this.settings['platform:ff-npm-registry:enabled']) {
this.input.registryToken = this.placeholderToken
}
},
methods: {
...mapActions('account', ['refreshSettings']),
validate () {
if (this.input['platform:certifiedNodes:npmRegistryURL']) {
try {
const url = new URL(this.input['platform:certifiedNodes:npmRegistryURL'])
// const url = URL.parse(this.input['platform:certifiedNodes:npmRegistryURL'])
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
this.errors['platform:certifiedNodes:npmRegistryURL'] = 'Invalid URL'
return false
} else {
this.errors['platform:certifiedNodes:npmRegistryURL'] = ''
}
} catch (e) {
this.errors['platform:certifiedNodes:npmRegistryURL'] = 'Invalid URL'
return false
}
} else {
this.errors['platform:certifiedNodes:npmRegistryURL'] = ''
}

if (this.input['platform:certifiedNodes:catalogueURL']) {
try {
const url = new URL(this.input['platform:certifiedNodes:catalogueURL'])
// const url = URL.parse(this.input['platform:certifiedNodes:catalogueURL'])
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
this.errors['platform:certifiedNodes:catalogueURL'] = 'Invalid URL'
return false
} else {
this.errors['platform:certifiedNodes:catalogueURL'] = ''
}
} catch (e) {
this.errors['platform:certifiedNodes:catalogueURL'] = 'Invalid URL'
return false
}
} else {
this.errors['platform:certifiedNodes:catalogueURL'] = ''
}

if (this.input['platform:certifiedNodes:npmRegistryURL'] &&
this.input['platform:certifiedNodes:catalogueURL'] &&
!this.input['platform:certifiedNodes:token']) {
this.errors['platform:certifiedNodes:token'] = 'Token is required when NPM Registry URL is set'
return false
} else {
this.errors['platform:certifiedNodes:token'] = ''
}

return true
},
async saveChanges () {
this.loading = true
const options = {}
validSettings.forEach((s) => {
if (this.input[s] !== this.settings[s]) {
options[s] = this.input[s]
}
})

const options = {
'platform:ff-npm-registry:token': this.input.registryToken
}
settingsApi.updateSettings(options)
.then(async () => {
await this.refreshSettings()
if (this.settings['platform:ff-npm-registry:enabled']) {
// Set the placeholder to the same length as the just-saved token, so
// it doesn't change
this.placeholderToken = this.input.registryToken
}
Alerts.emit('Settings changed successfully.', 'confirmation')
})
.catch(async (err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,12 @@
<FormRow v-model="input.properties.features.instanceResources" type="checkbox">Instance Resources</FormRow>
<FormRow v-model="input.properties.features.tables" type="checkbox">Tables</FormRow>
<FormRow v-model="input.properties.features.certifiedNodes" type="checkbox">Certified Nodes</FormRow>
<FormRow v-model="input.properties.features.ffNodes" type="checkbox">FlowFuse Exclusive Nodes</FormRow>
<FormRow v-model="input.properties.features.generatedSnapshotDescription" type="checkbox">Generated Snapshot Descriptions</FormRow>
<FormRow v-model="input.properties.features.assistantInlineCompletions" type="checkbox">Assistant Inline Code Completions</FormRow>
<FormRow v-model="input.properties.features.rbacApplication" type="checkbox">Application-level RBAC</FormRow>
<!-- to make the grid work nicely, only needed if there is an odd number of checkbox features above-->
<!-- <span /> -->
<span />
<FormRow v-model="input.properties.features.fileStorageLimit">Persistent File storage limit (Mb)</FormRow>
<FormRow v-model="input.properties.features.contextLimit">Persistent Context storage limit (Mb)</FormRow>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/admin/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default [
path: 'certified-nodes',
component: AdminCertifiedNodes,
meta: {
title: 'Admin - Certified Nodes'
title: 'Admin - FlowFuse Nodes'
}
}
]
Expand Down
Loading
Loading