@flowfuse/flowfuse
Version:
An open source low-code development platform
812 lines (748 loc) • 35.1 kB
JavaScript
const crypto = require('crypto')
const { ControllerError } = require('../../lib/errors')
const { KEY_CUSTOM_HOSTNAME, KEY_SETTINGS, KEY_STACK_UPGRADE_HOUR } = require('../models/ProjectSettings')
/**
* inflightProjectState - when projects are transitioning between states, there
* is no need to store that in the database. But we do need to know it so the
* information can be returned on the API.
*/
const inflightProjectState = 'project-inflightProjectState'
const latestProjectState = 'project-latestProjectState'
const inflightDeploys = 'project-inflightDeploys'
module.exports = {
init (app) {
app.caches.createCache(inflightProjectState)
app.caches.createCache(latestProjectState)
app.caches.createCache(inflightDeploys)
},
/**
* Get the in-flight state of a project
* @param {*} app
* @param {*} project
* @returns the in-flight state
*/
getInflightState: async function (app, project) {
return await app.caches.getCache(inflightProjectState).get(project.id)
},
/**
* Set the in-flight state of a project
* @param {*} app
* @param {*} project
* @param {*} state
*/
setInflightState: async function (app, project, state) {
await app.caches.getCache(inflightProjectState).set(project.id, state)
},
/**
* Check whether an instance is currently flagged as deploying
* @param {*} app
* @param {*} instance
*/
isDeploying: async function (app, instance) {
const has = await app.caches.getCache(inflightDeploys).get(instance.id)
return has === true
},
/**
* Mark an instance as currently being deployed
* @param {*} app
* @param {*} instance
*/
setInDeploy: async function (app, instance) {
await app.caches.getCache(inflightDeploys).set(instance.id, true)
},
/**
* Set the in-flight state of a project
* @param {*} app
* @param {*} project
*/
clearInflightState: async function (app, project) {
await app.caches.getCache(inflightProjectState).del(project.id)
await app.caches.getCache(inflightDeploys).del(project.id)
},
/**
* Get the settings object that should be passed to nr-launcher so it can
* start Node-RED with the proper project configuration.
*
* This merges the Project Template settings with the Project's own settings.
*
* @param {*} app the forge app
* @param {*} project the project to get the settings for
* @returns the runtime settings object
*/
getRuntimeSettings: async function (app, project) {
// This assumes the project has been loaded via `byId` so that
// it has the template and ProjectSettings attached
let result = {}
const env = {}
if (project.ProjectTemplate) {
result = project.ProjectTemplate.settings
if (result.env) {
result.env.forEach(envVar => {
env[envVar.name] = envVar.value
})
}
// special case for palette.modules - we don't want to inherit
// the template's list of modules. We want to use the list that
// is stored in the project's own settings.
if (result.palette?.modules) {
delete result.palette.modules
}
}
const projectSettingsRow = project.ProjectSettings?.find((projectSetting) => projectSetting.key === KEY_SETTINGS)
if (projectSettingsRow) {
const projectSettings = projectSettingsRow.value
result = app.db.controllers.ProjectTemplate.mergeSettings(result, projectSettings)
const envVars = app.db.controllers.Project.insertPlatformSpecificEnvVars(project, result.env)
// envVars is an array of env var objects. Need to convert to an env var object and merge into
// the existing env object
// convert [{name: 'a', value: '1'}, {name: 'b', value: '2'}] >> to >> { a: 1, b: 2 }
envVars.forEach(envVar => {
if (envVar.hidden) {
// Maintain the hidden flag
// TODO: handle encrypted values
env[envVar.name] = { value: envVar.value, hidden: true }
} else {
env[envVar.name] = envVar.value
}
})
}
// If we don't have any modules listed in project settings. We should
// look them up from StorageSettings in case this is a pre-existing project
// that has nodes installed from before we started tracking them ourselves
const moduleList = result.palette?.modules || await app.db.controllers.StorageSettings.getProjectModules(project) || []
const modules = {}
moduleList.forEach(module => {
modules[module.name] = module.version
})
// ensure email alert settings for resources have a value that matches
// what we show in the UI when no value is set in the DB
// (see forge/db/views/Project.js for the matching UI default values)
if (typeof result.emailAlerts !== 'object') {
result.emailAlerts = {}
}
if (typeof result.emailAlerts.resource !== 'object') {
result.emailAlerts.resource = {}
}
if (typeof result.emailAlerts.resource.cpu !== 'boolean') {
const templateSetting = project.ProjectTemplate?.settings?.emailAlerts?.resource?.cpu
result.emailAlerts.resource.cpu = templateSetting ?? true
}
if (typeof result.emailAlerts.resource.memory !== 'boolean') {
const templateSetting = project.ProjectTemplate?.settings?.emailAlerts?.resource?.memory
result.emailAlerts.resource.memory = templateSetting ?? true
}
result.palette = result.palette || {}
result.palette.modules = modules
result.env = env
return result
},
/**
* Exports the instance as a JSON object. Used as part of generating snapshots.
*/
exportProject: async function (app, project, components = {
flows: true,
credentials: true,
settings: true,
envVars: true
}) {
const projectExport = {}
if (components.flows) {
const flows = await app.db.models.StorageFlow.byProject(project.id)
projectExport.flows = !flows ? [] : JSON.parse(flows.flow)
if (components.credentials) {
const origCredentials = await app.db.models.StorageCredentials.byProject(project.id)
if (origCredentials) {
const encryptedCreds = JSON.parse(origCredentials.credentials)
if (components.credentialSecret) {
// This code path is currently unused. It is here
// for a future item where a user wants to export a project
// out of the platform. They will provide their own
// credentialSecret value - which we will used to re-encrypt
// the project credentials
const projectSecret = await project.getCredentialSecret()
const exportSecret = components.credentialSecret
projectExport.credentials = app.db.controllers.Project.exportCredentials(encryptedCreds, projectSecret, exportSecret)
} else {
projectExport.credentials = encryptedCreds
}
}
}
}
const settings = await app.db.controllers.Project.getRuntimeSettings(project)
if (components.settings || components.envVars) {
const envVars = settings.env
delete settings.env
if (components.settings) {
projectExport.settings = settings
}
if (components.envVars) {
projectExport.env = envVars
}
}
const NRSettings = await app.db.models.StorageSettings.byProject(project.id)
if (NRSettings) {
projectExport.modules = {}
try {
const nodeList = JSON.parse(NRSettings.settings).nodes || {}
Object.entries(nodeList).forEach(([key, value]) => {
projectExport.modules[key] = value.version
})
// The list from StorageSettings will only include modules that
// include nodes. The instance settings may specify other modules
// to be installed. We need to include them in the list.
if (settings.palette?.modules) {
Object.entries(settings.palette?.modules).forEach(([key, value]) => {
if (!Object.hasOwn(projectExport.modules, key)) {
projectExport.modules[key] = value
}
})
}
} catch (err) {}
}
return projectExport
},
importProjectSnapshot: async function restoreSnapshot (app, project, snapshot, { mergeEnvVars, mergeEditorSettings } = { mergeEnvVars: false, mergeEditorSettings: false }) {
const t = await app.db.sequelize.transaction() // start a transaction
try {
if (snapshot?.flows?.flows) {
const flows = JSON.stringify(!snapshot.flows.flows ? [] : snapshot.flows.flows)
await app.db.controllers.StorageFlows.updateOrCreateForProject(project, flows, { transaction: t })
if (snapshot.flows.credentials) {
await app.db.controllers.StorageCredentials.updateOrCreateForProject(project, snapshot.flows.credentials, { transaction: t })
}
}
if (snapshot?.settings?.settings || snapshot?.settings?.env) {
const snapshotSettings = JSON.parse(JSON.stringify(snapshot.settings.settings || {}))
snapshotSettings.env = []
const envVarKeys = Object.keys(snapshot.settings.env || {})
if (envVarKeys?.length) {
envVarKeys.forEach(key => {
const value = snapshot.settings.env[key]
if (typeof value === 'object' && value !== null && Object.hasOwn(value, 'hidden')) {
snapshotSettings.env.push({
name: key,
value: snapshot.settings.env[key].value,
hidden: snapshot.settings.env[key].hidden
})
} else {
snapshotSettings.env.push({
name: key,
value
})
}
})
}
if (snapshotSettings.palette?.modules) {
const moduleList = []
for (const [name, version] of Object.entries(snapshotSettings.palette.modules)) {
moduleList.push({ name, version, local: true })
}
snapshotSettings.palette.modules = moduleList
} else {
snapshotSettings.palette = snapshotSettings.palette || {}
snapshotSettings.palette.modules = []
}
if (!project.ProjectTemplate) {
project = await app.db.models.Project.byId(project.id)
}
const newSettings = app.db.controllers.ProjectTemplate.validateSettings(snapshotSettings, project.ProjectTemplate, true)
const currentProjectSettings = await project.getSetting('settings') || {} // necessary?
const updatedSettings = app.db.controllers.ProjectTemplate.mergeSettings(currentProjectSettings, newSettings, { mergeEnvVars, mergeEditorSettings, targetTemplate: project.ProjectTemplate })
await project.updateSetting('settings', updatedSettings, { transaction: t }) // necessary?
}
await t.commit() // all good, commit the transaction
} catch (error) {
await t.rollback() // rollback the transaction.
throw error
}
},
/**
* Takes a credentials object and re-encrypts it with a new key.
* If oldKey is blank or undefined, it assumes the credentials object is
* unencrypted at this point and only needs to be re-encrypted
*/
exportCredentials: function (app, original, oldKey, newKey) {
if (oldKey && original.$) {
const oldHash = crypto.createHash('sha256').update(oldKey).digest()
original = decryptCreds(oldHash, original)
}
const newHash = crypto.createHash('sha256').update(newKey).digest()
return encryptCreds(newHash, original)
},
/**
* Helper for the above exportCredentials method
* @param {*} existingCredentials
* @param {*} oldCredentialSecret
* @param {*} newCredentialSecret
* @returns
*/
reEncryptCredentials (app, existingCredentials, oldCredentialSecret, newCredentialSecret) {
const newCredentials = app.db.controllers.Project.exportCredentials(existingCredentials, oldCredentialSecret, newCredentialSecret)
return newCredentials
},
/**
* Remove platform specific environment variables
* @param {[{name:string, value:string}]} envVars Environment variables array
*/
removePlatformSpecificEnvVars: function (app, envVars) {
if (!envVars || !Array.isArray(envVars)) {
return []
}
return [...envVars.filter(e => e.name.startsWith('FF_') === false)]
},
/**
* Insert platform specific environment variables
* @param {Project} project The device
* @param {[{name:string, value:string}]} envVars Environment variables array
*/
insertPlatformSpecificEnvVars: function (app, project, envVars) {
if (!envVars || !Array.isArray(envVars)) {
envVars = []
}
const makeVar = (name, value, deprecated) => {
return { name, value: value || '', platform: true, deprecated } // add `platform` and `deprecated` flags for UI
}
let customHostname
if (app.config.features.enabled('customHostnames')) {
const projectURL = URL.parse(project.url)
const hostnameSetting = project.ProjectSettings?.find(setting => setting.key === KEY_CUSTOM_HOSTNAME)
customHostname = hostnameSetting?.value ? `${projectURL.protocol}//${hostnameSetting.value}` : undefined
}
const result = []
result.push(makeVar('FF_INSTANCE_ID', project.id || ''))
result.push(makeVar('FF_INSTANCE_NAME', project.name || ''))
result.push(makeVar('FF_PROJECT_ID', project.id || '', true)) // deprecated as of V1.6.0
result.push(makeVar('FF_PROJECT_NAME', project.name || '', true)) // deprecated as of V1.6.0
result.push(makeVar('FF_INSTANCE_URL', customHostname || project.url || ''))
result.push(...app.db.controllers.Project.removePlatformSpecificEnvVars(envVars))
return result
},
/**
*
* @param {*} app
* @param {Team} team
* @param {Application} application
* @param {User} user
* @param {ProjectType} type
* @param {ProjectStack} stack
* @param {ProjectTemplate} template
* @param {{name: string, ha: {}, sourceProject: Project, sourceProjectOptions: {}, flowTemplate: FlowTemplate}} properties Props of the project to create
* @returns
*/
create: async function (
app,
team,
application,
user,
type,
stack,
template,
{
name = '',
ha = null,
sourceProject = null,
sourceProjectOptions = {},
flowBlueprint = null,
flows = null
} = {}
) {
if (!user) {
throw new ControllerError('invalid_user', 'Invalid user')
}
if (!team) {
throw new ControllerError('invalid_team', 'Invalid team')
}
if (!application) {
throw new ControllerError('invalid_application', 'Invalid application')
}
if (!type) {
throw new ControllerError('invalid_project_type', 'Invalid project type')
}
if (sourceProject && flowBlueprint) {
throw new ControllerError('invalid_request', 'Source Project and Flow Blueprint cannot both be used')
}
if (flowBlueprint && flows) {
throw new ControllerError('invalid_request', 'Custom flows and Flow Blueprint cannot be used at the same time')
}
if (sourceProject) {
if (sourceProject.Team.id !== team.id) {
throw new ControllerError('invalid_source_project', 'Source Project Not in Same Team', 403)
}
}
if (!stack || stack.ProjectTypeId !== type.id) {
throw new ControllerError('invalid_stack', 'Invalid stack')
}
if (!template) {
throw new ControllerError('invalid_template', 'Invalid template')
}
name = name.trim()
const safeName = name?.toLowerCase()
if (app.db.models.Project.BANNED_NAME_LIST.includes(safeName)) {
throw new ControllerError('invalid_project_name', 'name not allowed', 409)
}
if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(safeName) === false) {
throw new ControllerError('invalid_project_name', 'name not allowed', 409)
}
if (await app.db.models.Project.isNameUsed(safeName)) {
throw new ControllerError('invalid_project_name', 'name in use', 409)
}
if (app.license.active() && app.ha) {
if (ha && !await app.ha.isHAAllowed(team, type, ha)) {
throw new ControllerError('invalid_ha', 'Invalid HA configuration')
}
}
const transaction = await app.db.sequelize.transaction()
// This will perform all checks needed to ensure this instance type can be created for this team.
// Throws an exception if not allowed
try {
await team.checkInstanceTypeCreateAllowed(type, transaction)
} catch (err) {
await transaction.rollback()
throw err
}
let instance
try {
instance = await app.db.models.Project.create({
name,
ApplicationId: application.id,
type: '',
url: ''
}, { transaction })
await transaction.commit()
} catch (err) {
await transaction.rollback()
throw new ControllerError('unexpected_error', err.message, null, { cause: err })
}
await team.addProject(instance)
await instance.setProjectStack(stack)
await instance.setProjectTemplate(template)
await instance.setProjectType(type)
if (app.license.active() && app.ha && ha) {
await instance.updateHASettings(ha)
}
if (flows && Array.isArray(flows) && flows.length > 0) {
try {
await app.db.controllers.StorageFlows.updateOrCreateForProject(instance, JSON.stringify(flows))
} catch (e) {
app.log.error(`Error creating flows for project ${instance.id} - ${e.stack}`)
}
}
await instance.reload({
include: [
{ model: app.db.models.Team },
{ model: app.db.models.ProjectType },
{ model: app.db.models.ProjectStack },
{ model: app.db.models.ProjectTemplate },
{ model: app.db.models.ProjectSettings }
]
})
if (sourceProject) {
await app.db.controllers.Project.importFromInstance(instance, sourceProject, sourceProjectOptions)
} else {
const newProjectSettings = { header: { title: instance.name } }
// Copy the palette modules from the template (if any)
// This is an instance creation time only operation to avoid the complexities of
// merging the palette modules from the template with the instance palette modules.
if (template.settings.palette?.modules?.length > 0) {
newProjectSettings.palette = { modules: [...template.settings.palette.modules] }
}
await instance.updateSetting(KEY_SETTINGS, newProjectSettings)
await instance.updateSetting('credentialSecret', app.db.models.Project.generateCredentialSecret())
if (flowBlueprint) {
await app.db.controllers.Project.applyFlowBlueprint(instance, flowBlueprint)
}
}
if (app.config.features.enabled('autoStackUpdate')) {
// need to check TeamType flag, commented out code ready for Team Overrides
await team.ensureTeamTypeExists()
if (team.TeamType.getProperty('autoStackUpdate')) {
const autoStackUpdate = team.TeamType.getProperty('autoStackUpdate')
if (autoStackUpdate.enabled && !autoStackUpdate.allowDisable) {
const days = autoStackUpdate.days
const hours = autoStackUpdate.hours
// generate random day and hour in ranges
const day = days[Math.floor(days.length * Math.random())]
const hour = hours[Math.floor(hours.length * Math.random())]
await instance.updateSetting(`${KEY_STACK_UPGRADE_HOUR}_${day}`, { hour })
}
}
}
await app.containers.start(instance)
await app.auditLog.Project.project.created(user, null, team, instance)
if (sourceProject) {
await app.auditLog.Team.project.duplicated(user, null, team, sourceProject, instance)
} else {
await app.auditLog.Team.project.created(user, null, team, instance)
}
return instance
},
/**
* This method imports from an existing instance, whereas importProject imports from a representation of an instance
* Long term, these two method should be combined.
*
* @param {*} app
* @param {Project} targetInstance
* @param {Project} sourceInstance
* @param {{flows: boolean, credentials: boolean, envVars: boolean}} options
*/
importFromInstance: async function (app, targetInstance, sourceInstance, options = {}) {
// need to copy values over
const settingsString = (await app.db.models.StorageSettings.byProject(sourceInstance.id))?.settings ?? '{}'
const newSettings = {
users: {}
}
const sourceSettings = JSON.parse(settingsString)
if (settingsString) {
newSettings.nodes = sourceSettings.nodes
}
const newCredentialSecret = app.db.models.Project.generateCredentialSecret()
if (options.flows) {
const sourceFlows = await app.db.models.StorageFlow.byProject(sourceInstance.id)
if (sourceFlows) {
const newFlow = await app.db.models.StorageFlow.create({
flow: sourceFlows.flow,
ProjectId: targetInstance.id
})
await newFlow.save()
}
if (options.credentials) {
// To copy over the credentials, we have to:
// - get the existing credentials + credentialSecret
// - generate a new credentialSecret for the new project
// (this is normally left to NR to do itself)
// - re-encrypt the credentials using the new key
const origCredentialsModel = await app.db.models.StorageCredentials.byProject(sourceInstance.id)
if (origCredentialsModel) {
const origCredentials = JSON.parse(origCredentialsModel.credentials) // .credentials is stored as text in the DB
const origCredentialSecret = await sourceInstance.getSetting('credentialSecret') || sourceSettings._credentialSecret // Legacy
const newCredentials = await app.db.controllers.Project.reEncryptCredentials(origCredentials, origCredentialSecret, newCredentialSecret)
await app.db.models.StorageCredentials.create({
credentials: JSON.stringify(newCredentials),
ProjectId: targetInstance.id
})
}
}
}
await targetInstance.updateSetting('credentialSecret', newCredentialSecret)
const settings = await app.db.models.StorageSettings.create({
settings: JSON.stringify(newSettings),
ProjectId: targetInstance.id
})
await settings.save()
const sourceProjectSettings = await sourceInstance.getSetting(KEY_SETTINGS) || { env: [] }
const sourceProjectEnvVars = sourceProjectSettings.env || []
const newProjectSettings = { ...sourceProjectSettings }
newProjectSettings.env = []
if (options.envVars) {
sourceProjectEnvVars.forEach(envVar => {
newProjectSettings.env.push({
name: envVar.name,
value: options.envVars === 'keys' ? '' : envVar.value,
hidden: envVar.hidden ?? false
})
})
}
newProjectSettings.header = { title: targetInstance.name }
await targetInstance.updateSetting(KEY_SETTINGS, newProjectSettings)
return targetInstance
},
/**
* Imports settings, flows, and credentials from a project export object
*
* @param {*} app
* @param {*} project
* @param {*} components
*/
importProject: async function (app, project, components) {
const transaction = await app.db.sequelize.transaction()
try {
if (components.flows) {
await app.db.controllers.StorageFlows.updateOrCreateForProject(project, components.flows, { transaction })
}
if (components.credentials) {
const projectSecret = await project.getCredentialSecret()
const encryptedCreds = app.db.controllers.Project.exportCredentials(JSON.parse(components.credentials), components.credsSecret, projectSecret)
await app.db.controllers.StorageCredentials.updateOrCreateForProject(project, encryptedCreds, { transaction })
}
await transaction.commit()
} catch (error) {
transaction.rollback()
throw error
}
if (project.state === 'running') {
await app.db.controllers.Project.setInflightState(project, 'restarting')
project.state = 'running'
await project.save()
const result = await app.containers.restartFlows(project)
app.db.controllers.Project.clearInflightState(project)
return result
}
},
/**
* Add a module to the list of project modules.
*/
addProjectModule: async function (app, project, module, version) {
return app.db.controllers.Project.mergeProjectModules(project, [{
name: module,
version,
local: true
}], true)
},
/**
* Remove a module from the list of project modules.
*/
removeProjectModule: async function (app, project, module) {
let changed = false
let newProjectModuleList
const currentProjectSettings = await project.getSetting('settings') || {}
if (currentProjectSettings?.palette?.modules) {
newProjectModuleList = currentProjectSettings?.palette?.modules.filter(m => {
if (m.name === module) {
changed = true
return false
}
return true
})
}
if (changed) {
currentProjectSettings.palette = currentProjectSettings.palette || {}
currentProjectSettings.palette.modules = newProjectModuleList
await project.updateSetting('settings', currentProjectSettings)
}
},
/**
* Updates the project settings.palette.modules value based on the
* module list Node-RED has provided to the StorageSettings api.
*/
mergeProjectModules: async function (app, project, moduleList, updateExisting = false) {
let changed = false
let newProjectModuleList
const currentProjectSettings = await project.getSetting('settings') || {}
if (currentProjectSettings?.palette?.modules) {
// The project has an existing list of modules - need to resolve any
// changes between the two lists
// As the moduleList comes from Node-RED's runtimeSettings object,
// it will only contain the modules that include Node-RED nodes.
// Regular npm modules that do not include nodes (nrlint for eg)
// will not be included.
// So we don't want to remove anything from the current list just
// because it isn't listed in what moduleList provides.
// We use the nodes.remove audit event to remove entries relating
// to node modules.
// For modules we already know about, we only update the recorded
// version if explicitly asked to. That only happens when triggered
// by the 'nodes.install' audit event.
// This is the list of modules from ProjectSettings
const existingModulesList = currentProjectSettings?.palette?.modules
const existingModules = {}
existingModulesList.forEach(m => { existingModules[m.name] = m })
const newModules = {}
moduleList.forEach(m => {
newModules[m.name] = m
if (!existingModules[m.name] || updateExisting) {
changed = true
existingModules[m.name] = m
if (/^\d/.test(m.version)) {
m.version = `~${m.version}`
}
}
})
newProjectModuleList = Object.values(existingModules)
} else {
// No existing modules - so updated with the list as provided
changed = true
newProjectModuleList = moduleList.map(m => {
if (/^\d/.test(m.version)) {
m.version = `~${m.version}`
}
return m
})
}
if (changed) {
currentProjectSettings.palette = currentProjectSettings.palette || {}
currentProjectSettings.palette.modules = newProjectModuleList
await project.updateSetting('settings', currentProjectSettings)
}
},
/**
* Applies the contents of a FlowTemplate to an Instance. We assume this
* is being invoked when the Instance is being created - so it will overwrite
* any existing flows and will merge any modules with those provided by the ProjectTemplate.
* @param {*} app
* @param {*} instance
* @param {*} flowBlueprint
*/
applyFlowBlueprint: async function (app, instance, flowBlueprint) {
const flows = flowBlueprint.flows || { flows: [], credentials: {} }
if (flows.flows) {
await app.db.controllers.StorageFlows.updateOrCreateForProject(instance, JSON.stringify(flows.flows))
}
if (flows.credentials) {
const projectSecret = await instance.getCredentialSecret()
const encryptedCreds = app.db.controllers.Project.exportCredentials(flows.credentials, null, projectSecret)
await app.db.controllers.StorageCredentials.updateOrCreateForProject(instance, encryptedCreds)
}
const modules = flowBlueprint.modules || {}
for (const [moduleName, version] of Object.entries(modules)) {
await app.db.controllers.Project.addProjectModule(instance, moduleName, version)
}
},
/**
* Retrieves the latest state of a specified project.
*
* @param {string|Number} projectId - The unique identifier of the project whose latest state is to be retrieved.
* @returns {string} The latest state of the specified project.
*/
getLatestProjectState: async function (app, projectId) {
return await app.caches.getCache(latestProjectState).get(projectId)
// return latestProjectState[projectId]
},
/**
* Updates the state of the latest project for a given project ID.
*
* @function
* @param {string|number} projectId - The unique identifier of the project whose state is being updated.
* @param {any} state - The new state to be assigned to the project.
*/
setLatestProjectState: async function (app, projectId, state) {
app.caches.getCache(latestProjectState).set(projectId, state)
},
/**
* Clears the latest project state for a specific project.
*
* This function deletes the entry in the `latestProjectState` object corresponding
* to the provided project's ID. The state deletion is context-specific to the project
* and associated application.
*
* @param {String|Number} projectId - The project id whose latest state should be cleared.
*/
clearLatestProjectState: async function (app, projectId) {
return await app.caches.getCache(latestProjectState).del(projectId)
},
/**
* Updates the latest project state for the specified app and project based on the given state.
*
* @param {String|Number} projectId - The project id related to the driver state update.
* @param {string} state - The new state to update, can be 'running', 'stopped', or other valid states.
*/
updateLatestProjectState: async function (app, projectId, state) {
if (['running'].includes(state)) {
await this.clearLatestProjectState(app, projectId)
} else {
await this.setLatestProjectState(app, projectId, state)
}
}
}
function decryptCreds (key, cipher) {
let flows = cipher.$
const initVector = Buffer.from(flows.substring(0, 32), 'hex')
flows = flows.substring(32)
const decipher = crypto.createDecipheriv('aes-256-ctr', key, initVector)
const decrypted = decipher.update(flows, 'base64', 'utf8') + decipher.final('utf8')
return JSON.parse(decrypted)
}
function encryptCreds (key, plain) {
const initVector = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-ctr', key, initVector)
return { $: initVector.toString('hex') + cipher.update(JSON.stringify(plain), 'utf8', 'base64') + cipher.final('base64') }
}