Skip to content

Commit

Permalink
Merge branch 'main' into 3789-snapshot-view-flows
Browse files Browse the repository at this point in the history
  • Loading branch information
Steve-Mcl authored May 8, 2024
2 parents 3ea42b3 + 9395ad5 commit 3fb8254
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 7 deletions.
33 changes: 33 additions & 0 deletions forge/db/controllers/StorageSession.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,38 @@ module.exports = {
sessions.session = '{}'
await sessions.save()
}
},
async removeUserFromTeamSessions (app, user, team) {
const instances = await app.db.models.Project.byTeam(team.hashid)
for (let i = 0; i < instances.length; i++) {
const instance = instances[i]
const sessions = await app.db.models.StorageSession.byProject(instance.id)
if (sessions) {
const sessionInfo = JSON.parse(sessions.sessions)
let modified = false
const userSessions = Object.values(sessionInfo).filter(session => {
if (session.user === user.username) {
delete sessionInfo[session.accessToken]
modified = true
return true
}
return false
})
if (modified) {
sessions.sessions = JSON.stringify(sessionInfo)
await sessions.save()
}
if (userSessions.length > 0) {
for (let i = 0; i < userSessions.length; i++) {
const token = userSessions[i].accessToken
try {
await app.containers.revokeUserToken(instance, token) // logout:nodered(step-2)
} catch (error) {
app.log.warn(`Failed to revoke token for Instance ${instance.id}: ${error.toString()}`) // log error but continue to delete session
}
}
}
}
}
}
}
3 changes: 2 additions & 1 deletion forge/db/controllers/Team.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ module.exports = {
}
await userRole.destroy()

await app.db.controllers.StorageSession.removeUserFromTeamSessions(user, team)

return true
// console.warn('TODO: forge.db.controllers.Team.removeUser - expire oauth sessions')
}

return false
Expand Down
5 changes: 3 additions & 2 deletions forge/db/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { DataTypes, Op } = require('sequelize')

const Controllers = require('../controllers')

const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED } = require('./ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL } = require('./ProjectSettings')

const BANNED_NAME_LIST = [
'www',
Expand Down Expand Up @@ -358,7 +358,8 @@ module.exports = {
{ key: KEY_SETTINGS },
{ key: KEY_HOSTNAME },
{ key: KEY_HA },
{ key: KEY_PROTECTED }
{ key: KEY_PROTECTED },
{ key: KEY_HEALTH_CHECK_INTERVAL }
]
},
required: false
Expand Down
2 changes: 2 additions & 0 deletions forge/db/models/ProjectSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const KEY_SETTINGS = 'settings'
const KEY_HOSTNAME = 'hostname'
const KEY_HA = 'ha'
const KEY_PROTECTED = 'protected'
const KEY_HEALTH_CHECK_INTERVAL = 'healthCheckInterval'

module.exports = {
KEY_SETTINGS,
KEY_HOSTNAME,
KEY_HA,
KEY_PROTECTED,
KEY_HEALTH_CHECK_INTERVAL,
name: 'ProjectSettings',
schema: {
ProjectId: { type: DataTypes.UUID, unique: 'pk_settings' },
Expand Down
16 changes: 15 additions & 1 deletion forge/db/views/Project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED } = require('../models/ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL } = require('../models/ProjectSettings')

module.exports = function (app) {
app.addSchema({
Expand Down Expand Up @@ -32,6 +32,13 @@ module.exports = function (app) {
protected: {
type: 'object',
additionalProperties: true
},
launcherSettings: {
type: 'object',
properties: {
healthCheckInterval: { type: 'number' }
},
additionalProperties: false
}
}
})
Expand Down Expand Up @@ -61,6 +68,13 @@ module.exports = function (app) {
} else {
result.settings = {}
}
// Launcher Settings
const heathCheckIntervalRow = proj.ProjectSettings?.find((projectSettingsRow) => projectSettingsRow.key === KEY_HEALTH_CHECK_INTERVAL)
if (heathCheckIntervalRow) {
result.launcherSettings = {}
result.launcherSettings.healthCheckInterval = heathCheckIntervalRow?.value
}
// Environment
result.settings.env = app.db.controllers.Project.insertPlatformSpecificEnvVars(proj, result.settings.env)
if (!result.settings.palette?.modules) {
// If there are no modules listed in settings, check the StorageSettings
Expand Down
24 changes: 23 additions & 1 deletion forge/routes/api/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KEY_HOSTNAME, KEY_SETTINGS } = require('../../db/models/ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HEALTH_CHECK_INTERVAL } = require('../../db/models/ProjectSettings')
const { Roles } = require('../../lib/roles')

const { isFQDN } = require('../../lib/validate')
Expand Down Expand Up @@ -312,6 +312,7 @@ module.exports = async function (app) {
name: { type: 'string' },
hostname: { type: 'string' },
settings: { type: 'object' },
launcherSettings: { type: 'object' },
projectType: { type: 'string' },
stack: { type: 'string' },
sourceProject: {
Expand Down Expand Up @@ -478,6 +479,19 @@ module.exports = async function (app) {
changesToPersist.stack = { from: request.project.stack, to: stack }
}

// Launcher settings
if (request.body?.launcherSettings?.healthCheckInterval) {
const oldInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL)
const newInterval = parseInt(request.body.launcherSettings.healthCheckInterval, 10)
if (isNaN(newInterval) || newInterval < 5000) {
reply.code(400).send({ code: 'invalid_heathCheckInterval', error: 'Invalid heath check interval' })
return
}
if (oldInterval !== newInterval) {
changesToPersist.healthCheckInterval = { from: oldInterval, to: newInterval }
}
}

/// Persist the changes
const updates = new app.auditLog.formatters.UpdatesCollection()
const transaction = await app.db.sequelize.transaction() // start a transaction
Expand Down Expand Up @@ -541,6 +555,11 @@ module.exports = async function (app) {
}
}

if (changesToPersist.healthCheckInterval) {
await request.project.updateSetting(KEY_HEALTH_CHECK_INTERVAL, changesToPersist.healthCheckInterval.to, { transaction })
updates.pushDifferences({ healthCheckInterval: changesToPersist.healthCheckInterval.from }, { healthCheckInterval: changesToPersist.healthCheckInterval.to })
}

await transaction.commit() // all good, commit the transaction

// Log the updates
Expand Down Expand Up @@ -795,7 +814,9 @@ module.exports = async function (app) {
reply.code(400).send({ code: 'project_suspended', error: 'Project suspended' })
return
}
// get settings from the driver
const settings = await app.containers.settings(request.project)
// add instance settings
settings.env = settings.env || {}
settings.baseURL = request.project.url
settings.forgeURL = app.config.base_url
Expand All @@ -805,6 +826,7 @@ module.exports = async function (app) {
settings.auditURL = request.project.auditURL
settings.state = request.project.state
settings.stack = request.project.ProjectStack?.properties || {}
settings.healthCheckInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL)
settings.settings = await app.db.controllers.Project.getRuntimeSettings(request.project)
if (settings.settings.env) {
settings.env = Object.assign({}, settings.settings.env, settings.env)
Expand Down
5 changes: 5 additions & 0 deletions forge/routes/api/teamMembers.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ module.exports = async function (app) {
const role = app.auditLog.formatters.roleObject(result.role)
updates.push('role', oldRole?.role || result.oldRole, role?.role || result.role)
await app.auditLog.Team.team.user.roleChanged(request.session.User, null, request.team, result.user, updates)
if (result.role < result.oldRole) {
// We should invalidate session for this user for the teams NR instances if lower
// might want to make this only if it drop under Member
await app.db.controllers.StorageSession.removeUserFromTeamSessions(request.user, request.team)
}
}
reply.send({ status: 'okay' })
} catch (err) {
Expand Down
116 changes: 116 additions & 0 deletions frontend/src/pages/instance/Settings/LauncherSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<FormHeading class="mb-6">Launcher Settings</FormHeading>
<form class="space-y-6" data-el="launcher-settings-form">
<FormRow v-model="input.healthCheckInterval" type="number" :error="errors.healthCheckInterval">
Health check interval (ms)
<template #description>
The interval at which the launcher will check the health of Node-RED.
Flows that perform CPU intensive work may need to increase this from the default of 7500ms.
</template>
</FormRow>

<div class="space-x-4 whitespace-nowrap">
<ff-button size="small" :disabled="!unsavedChanges || !validateFormInputs()" data-action="save-settings" @click="saveSettings()">Save settings</ff-button>
</div>
</form>
</template>

<script>
import { useRouter } from 'vue-router'
import { mapState } from 'vuex'
import InstanceApi from '../../../api/instances.js'
import FormHeading from '../../../components/FormHeading.vue'
import FormRow from '../../../components/FormRow.vue'
import permissionsMixin from '../../../mixins/Permissions.js'
import alerts from '../../../services/alerts.js'
export default {
name: 'LauncherSettings',
components: {
FormRow,
FormHeading
},
mixins: [permissionsMixin],
inheritAttrs: false,
props: {
project: {
type: Object,
required: true
}
},
emits: ['instance-updated'],
data () {
return {
mounted: false,
original: {
healthCheckInterval: null
},
input: {
healthCheckInterval: null
},
errors: {
healthCheckInterval: ''
}
}
},
computed: {
...mapState('account', ['team', 'teamMembership']),
unsavedChanges: function () {
return this.original.healthCheckInterval !== this.input.healthCheckInterval
}
},
watch: {
project: 'getSettings',
'input.healthCheckInterval': function (value) {
if (this.mounted) {
this.validateFormInputs()
}
}
},
mounted () {
this.checkAccess()
this.getSettings()
this.mounted = true
},
methods: {
checkAccess: function () {
if (!this.hasPermission('project:edit')) {
useRouter().push({ replace: true, path: 'general' })
}
},
validateFormInputs () {
if (!this.unsavedChanges) {
this.errors.healthCheckInterval = ''
} else {
const hci = parseInt(this.input.healthCheckInterval)
if (isNaN(hci) || hci < 5000) {
this.errors.healthCheckInterval = 'Health check interval must be 5000 or greater'
} else {
this.errors.healthCheckInterval = ''
}
}
return !this.errors.healthCheckInterval
},
getSettings: function () {
this.original.healthCheckInterval = this.project?.launcherSettings?.healthCheckInterval
this.input.healthCheckInterval = this.project?.launcherSettings.healthCheckInterval
},
async saveSettings () {
const launcherSettings = {
healthCheckInterval: this.input.healthCheckInterval
}
if (!this.validateFormInputs()) {
alerts.emit('Please correct the errors before saving.', 'error')
return
}
await InstanceApi.updateInstance(this.project.id, { launcherSettings })
this.$emit('instance-updated')
alerts.emit('Instance successfully updated. Restart the instance to apply the changes.', 'confirmation')
}
}
}
</script>
1 change: 1 addition & 0 deletions frontend/src/pages/instance/Settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default {
this.sideNavigation.push({ name: 'Editor', path: './editor' })
this.sideNavigation.push({ name: 'Security', path: './security' })
this.sideNavigation.push({ name: 'Palette', path: './palette' })
this.sideNavigation.push({ name: 'Launcher', path: './launcher' })
if (this.features.emailAlerts && this.team.type.properties.features?.emailAlerts) {
this.sideNavigation.push({ name: 'Alerts', path: './alerts' })
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/instance/Settings/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import InstanceSettingsEditor from './Editor.vue'
import InstanceSettingsEnvVar from './Environment.vue'
import InstanceSettingsGeneral from './General.vue'
import InstanceSettingsHA from './HighAvailability.vue'
import InstanceSettingsLauncher from './LauncherSettings.vue'
import InstanceSettingsPalette from './Palette.vue'
import InstanceSettingsProtect from './ProtectInstance.vue'
import InstanceSettingsSecurity from './Security.vue'
Expand All @@ -26,5 +27,6 @@ export default [
title: 'Instance - Change Type'
}
},
{ path: 'launcher', name: 'instance-settings-launcher', component: InstanceSettingsLauncher },
{ path: 'alerts', name: 'instance-settings-alerts', component: InstanceSettingsAlerts }
]
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ describe('FlowFuse EE - Instance - Alerts', () => {
navigateToInstanceSettings('BTeam', 'instance-2-1')

// check Alerts in list and click
cy.get('[data-el="section-side-menu"] li').should('have.length', 8)
cy.get('[data-el="section-side-menu"] li:last a').contains('Alerts')
cy.get('[data-el="section-side-menu"] li:last').click()

Expand Down
Loading

0 comments on commit 3fb8254

Please sign in to comment.