@flowfuse/flowfuse
Advanced tools
Comparing version 1.15.1-cc8404f-202401101717.0 to 1.15.1-cea84ad-202401161048.0
@@ -11,3 +11,3 @@ const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
const loggers = { | ||
@@ -23,6 +23,4 @@ User: user.getLoggers(app), | ||
app.decorate('auditLog', loggers) | ||
next() | ||
}, { | ||
name: 'app.auditLog' | ||
}) |
@@ -107,11 +107,11 @@ /** | ||
// Receive status events from project launchers | ||
// - ff/v1/+/l/+/status | ||
{ topic: /^ff\/v1\/[^/]+\/l\/[^/]+\/status$/ }, | ||
// - ff/v1/<team>/l/<instance>/status | ||
{ topic: /^ff\/v1\/[^/]+\/l\/[^/]+\/status$/, shared: true }, | ||
// Receive status events, logs and command responses from devices | ||
// - ff/v1/+/d/+/status | ||
{ topic: /^ff\/v1\/[^/]+\/d\/[^/]+\/status$/ }, | ||
// - ff/v1/+/d/+/logs | ||
// - ff/v1/<team>/d/<device>/status | ||
{ topic: /^ff\/v1\/[^/]+\/d\/[^/]+\/status$/, shared: true }, | ||
// - ff/v1/<team>/d/<device>/logs | ||
{ topic: /^ff\/v1\/[^/]+\/d\/[^/]+\/logs$/ }, | ||
// - ff/v1/+/d/+/logs | ||
{ topic: /^ff\/v1\/[^/]+\/d\/[^/]+\/response$/ } | ||
// Receive broadcast response notification | ||
{ topic: /^ff\/v1\/[^/]+\/d\/[^/]+\/response(\/[^/]+)?$/ } | ||
], | ||
@@ -163,4 +163,4 @@ pub: [ | ||
{ topic: /^ff\/v1\/([^/]+)\/d\/([^/]+)\/logs$/, verify: 'checkTeamAndObjectIds' }, | ||
// - ff/v1/<team>/d/<device>/response | ||
{ topic: /^ff\/v1\/([^/]+)\/d\/([^/]+)\/response$/, verify: 'checkTeamAndObjectIds' } | ||
// - ff/v1/<team>/d/<device>/response[/<instance>] | ||
{ topic: /^ff\/v1\/([^/]+)\/d\/([^/]+)\/response(\/[^/]+)?$/, verify: 'checkTeamAndObjectIds' } | ||
] | ||
@@ -203,3 +203,3 @@ } | ||
const shareGroup = sharedSubParts[1] | ||
if (shareGroup !== usernameParts[2]) { | ||
if (shareGroup !== 'platform' && shareGroup !== usernameParts[2]) { | ||
return false | ||
@@ -206,0 +206,0 @@ } |
const EventEmitter = require('events') | ||
const mqtt = require('mqtt') | ||
const { v4: uuidv4 } = require('uuid') | ||
@@ -13,2 +14,3 @@ /** | ||
this.app = app | ||
this.platformId = uuidv4() | ||
} | ||
@@ -73,10 +75,10 @@ | ||
this.client.subscribe([ | ||
// Launcher status | ||
'ff/v1/+/l/+/status', | ||
// Device status | ||
'ff/v1/+/d/+/status', | ||
// Device logs | ||
// Launcher status - shared subscription | ||
'$share/platform/ff/v1/+/l/+/status', | ||
// Device status - shared subscription | ||
'$share/platform/ff/v1/+/d/+/status', | ||
// Device logs - not shared subscription | ||
'ff/v1/+/d/+/logs', | ||
// Device response | ||
'ff/v1/+/d/+/response' | ||
// Device response - not shared subscription | ||
'ff/v1/+/d/+/response/' + this.platformId | ||
]) | ||
@@ -83,0 +85,0 @@ } |
@@ -6,2 +6,3 @@ /** | ||
const SemVer = require('semver') | ||
const { v4: uuidv4 } = require('uuid') | ||
@@ -83,2 +84,9 @@ | ||
// If the device is owned by an application (in the DB) and the agent is reporting version < 1.11.0 | ||
// then we need to send an update command to the device | ||
if (Object.hasOwn(payload, 'snapshot') && device.isApplicationOwned) { | ||
if (!device.agentVersion || SemVer.lt(device.agentVersion, '1.11.0')) { | ||
sendUpdateCommand = true | ||
} | ||
} | ||
if (Object.hasOwn(payload, 'project') && payload.project !== (device.Project?.id || null)) { | ||
@@ -153,3 +161,4 @@ // The Project is incorrect | ||
const inFlightCommand = this.inFlightCommands[message.correlationData] | ||
if (inFlightCommand && inFlightCommand.command === message.command) { | ||
if (inFlightCommand) { | ||
// This command is known to the local instance - process it | ||
inFlightCommand.resolve(message.payload) | ||
@@ -246,3 +255,3 @@ delete this.inFlightCommands[response.correlationData] | ||
const inFlightCommand = DeviceCommsHandler.newResponseMonitor(command, deviceId, teamId, options) | ||
const inFlightCommand = DeviceCommsHandler.newResponseMonitor(command, deviceId, teamId, this.client.platformId, options) | ||
@@ -300,3 +309,3 @@ const promise = new Promise((resolve, reject) => { | ||
commandMessage.correlationData = cmr.correlationData | ||
commandMessage.responseTopic = `ff/v1/${cmr.teamId}/d/${cmr.deviceId}/response` | ||
commandMessage.responseTopic = `ff/v1/${cmr.teamId}/d/${cmr.deviceId}/response/${cmr.platformId}` | ||
commandMessage.payload = payload | ||
@@ -314,3 +323,3 @@ return commandMessage | ||
*/ | ||
static newResponseMonitor (command, deviceId, teamId, options = { timeout: DEFAULT_TIMEOUT }) { | ||
static newResponseMonitor (command, deviceId, teamId, platformId, options = { timeout: DEFAULT_TIMEOUT }) { | ||
const now = Date.now() | ||
@@ -329,2 +338,3 @@ const correlationData = uuidv4() // generate a random correlation data (uuid) | ||
responseMonitor.teamId = teamId | ||
responseMonitor.platformId = platformId | ||
responseMonitor.correlationData = correlationData | ||
@@ -331,0 +341,0 @@ return responseMonitor |
@@ -17,3 +17,3 @@ const fp = require('fastify-plugin') | ||
*/ | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
// Check the runtime configuration includes the minimum required configuration | ||
@@ -60,5 +60,4 @@ // to use the MQTT broker service | ||
} | ||
next() | ||
}, { | ||
name: 'app.comms' | ||
}) |
@@ -123,3 +123,3 @@ /** | ||
attach: fp(async function (app, opts, next) { | ||
attach: fp(async function (app, opts) { | ||
config.features = features(app, config) | ||
@@ -139,4 +139,3 @@ config.rate_limits = rateLimits.getLimits(app, config.rate_limits) | ||
} | ||
next() | ||
}, { name: 'app.config' }) | ||
} |
@@ -45,3 +45,3 @@ /** | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
const containerDialect = app.config.driver.type | ||
@@ -69,4 +69,2 @@ const containerModule = DRIVER_MODULES[containerDialect] | ||
} | ||
next() | ||
}, { name: 'app.containers' }) |
@@ -54,3 +54,3 @@ const { Op } = require('sequelize') | ||
ownerType: 'user', | ||
scope: 'password:Reset', | ||
scope: 'password:reset', | ||
ownerId: user.hashid | ||
@@ -57,0 +57,0 @@ } |
@@ -0,1 +1,2 @@ | ||
const SemVer = require('semver') | ||
const { literal } = require('sequelize') | ||
@@ -71,3 +72,8 @@ | ||
delete payload.project // exclude project property to avoid triggering the wrong kind of update on the device | ||
if (payload.snapshot === null) { | ||
if (!device.agentVersion || SemVer.lt(device.agentVersion, '1.11.0')) { | ||
// device is running an agent version < 1.11.0 we need to clear it | ||
payload.snapshot = null | ||
payload.project = null | ||
payload.settings = null | ||
} else if (payload.snapshot === null) { | ||
payload.snapshot = '0' // '0' indicates that the application owned device should start with starter flows | ||
@@ -74,0 +80,0 @@ } |
@@ -33,2 +33,7 @@ const jwt = require('jsonwebtoken') | ||
} | ||
if (newPassword === user.username) { | ||
throw new Error('Password must not match username') | ||
} else if (newPassword === user.email) { | ||
throw new Error('Password must not match email') | ||
} | ||
user.password = newPassword | ||
@@ -43,5 +48,10 @@ user.password_expired = false | ||
resetPassword: async function (app, user, newPassword) { | ||
// if (zxcvbn(newPassword.score < 3)) { | ||
// throw new Error('Password Too Weak') | ||
// } | ||
if (zxcvbn(newPassword).score < 2) { | ||
throw new Error('Password Too Weak') | ||
} | ||
if (newPassword === user.username) { | ||
throw new Error('Password must not match username') | ||
} else if (newPassword === user.email) { | ||
throw new Error('Password must not match email') | ||
} | ||
user.password = newPassword | ||
@@ -173,2 +183,5 @@ user.password_expired = false | ||
await requestingUser.save() | ||
await app.db.controllers.AccessToken.deleteAllUserPasswordResetTokens(requestingUser) | ||
return requestingUser | ||
@@ -175,0 +188,0 @@ } catch (err) { |
@@ -26,3 +26,3 @@ /** | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
utils.init(app) | ||
@@ -92,4 +92,2 @@ const dbOptions = { | ||
await controllers.init(app) | ||
next() | ||
}, { name: 'app.db' }) |
@@ -51,2 +51,18 @@ /** | ||
}, | ||
getTeamsForUser: async (userId, includeTeam = false) => { | ||
if (typeof userId === 'string') { | ||
userId = M.User.decodeHashid(userId) | ||
} | ||
const opts = { | ||
where: { | ||
UserId: userId | ||
} | ||
} | ||
if (includeTeam) { | ||
opts.include = { | ||
model: M.Team | ||
} | ||
} | ||
return this.findAll(opts) | ||
}, | ||
getTeamMembership: async (userId, teamId, includeTeam) => { | ||
@@ -53,0 +69,0 @@ if (typeof teamId === 'string') { |
@@ -259,2 +259,5 @@ /** | ||
}, | ||
getTeamMemberships: async function (includeTeam = false) { | ||
return M.TeamMember.getTeamsForUser(this.id, includeTeam) | ||
}, | ||
teamCount: async function () { | ||
@@ -261,0 +264,0 @@ return M.TeamMember.count({ |
@@ -7,2 +7,3 @@ module.exports = function (app) { | ||
properties: { | ||
email: { type: 'string' }, | ||
email_verified: { type: 'boolean' }, | ||
@@ -20,2 +21,5 @@ defaultTeam: { type: 'string' }, | ||
const result = userSummary(user) | ||
if (user.email) { | ||
result.email = user.email | ||
} | ||
if (user.password_expired) { | ||
@@ -48,3 +52,2 @@ result.password_expired = true | ||
name: { type: 'string' }, | ||
email: { type: 'string' }, | ||
avatar: { type: 'string' }, | ||
@@ -63,3 +66,2 @@ admin: { type: 'boolean' }, | ||
'name', | ||
'email', | ||
'avatar', | ||
@@ -66,0 +68,0 @@ 'admin', |
@@ -6,3 +6,3 @@ const fp = require('fastify-plugin') | ||
*/ | ||
module.exports = fp(async function (app, opts, next) { | ||
module.exports = fp(async function (app, opts) { | ||
// Load ee only if enabled in the license | ||
@@ -9,0 +9,0 @@ if (app.license.active()) { |
@@ -107,3 +107,3 @@ /** | ||
const connected = this.isConnected(deviceId) | ||
return { url, enabled, connected } | ||
return { url, enabled, connected, affinity: this.#getTunnel(deviceId)?.affinity } | ||
} | ||
@@ -134,2 +134,9 @@ | ||
setTunnelAffinity (deviceId, affinity) { | ||
const tunnel = this.#getTunnel(deviceId) | ||
if (tunnel) { | ||
tunnel.affinity = affinity | ||
} | ||
} | ||
isEnabled (deviceId) { | ||
@@ -369,3 +376,4 @@ const tunnel = this.#getTunnel(deviceId) | ||
_handleHTTP: null, | ||
_handleWS: null | ||
_handleWS: null, | ||
affinity: null | ||
} | ||
@@ -372,0 +380,0 @@ return tunnel |
const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, opts, done) { | ||
module.exports = fp(async function (app, opts) { | ||
if (app.config.billing) { | ||
@@ -28,4 +28,2 @@ app.decorate('billing', await require('./billing').init(app)) | ||
app.config.features.register('customCatalogs', true, true) | ||
done() | ||
}, { name: 'app.ee.lib' }) |
@@ -0,1 +1,3 @@ | ||
const { Roles, TeamRoles } = require('../../../lib/roles') | ||
module.exports.init = async function (app) { | ||
@@ -63,2 +65,123 @@ // Set the SSO feature flag | ||
/** | ||
* Update a user's team memberships according to the SAML Assertions | ||
* received when they logged in. | ||
* | ||
* @param {*} samlUser The user profile object provided by the authentication provider | ||
* @param {*} user The FF User object who is logging in | ||
* @param {*} providerOpts The SAML Provider configuration object | ||
*/ | ||
async function updateTeamMembership (samlUser, user, providerOpts) { | ||
// Look for the expected assertion in the SAML profile we have received | ||
// This is an array of groups the user belongs to. We expect them to be | ||
// of the form 'ff-SLUG-ROLE' - anything else is ignored | ||
let groupAssertions = samlUser[providerOpts.groupAssertionName] | ||
if (groupAssertions) { | ||
const promises = [] | ||
if (!Array.isArray(groupAssertions)) { | ||
groupAssertions = [groupAssertions] | ||
} | ||
const desiredTeamMemberships = {} | ||
groupAssertions.forEach(ga => { | ||
// Parse the group name - format: 'ff-SLUG-ROLE' | ||
// Generate a slug->role object (desiredTeamMemberships) | ||
const match = /^ff-(.+)-([^-]+)$/.exec(ga) | ||
if (match) { | ||
const teamSlug = match[1] | ||
const teamRoleName = match[2] | ||
const teamRole = Roles[teamRoleName] | ||
// Check this role is a valid team role | ||
if (TeamRoles.includes(teamRole)) { | ||
// Check if this team is allowed to be managed for this SSO provider | ||
// - either `groupAllTeams` is true (allowing all teams to be managed this way) | ||
// - or `groupTeams` (array) contains the teamSlug | ||
if (providerOpts.groupAllTeams || (providerOpts.groupTeams || []).includes(teamSlug)) { | ||
// In case we have multiple assertions for a single team, | ||
// ensure we keep the highest level of access | ||
desiredTeamMemberships[teamSlug] = Math.max(desiredTeamMemberships[teamSlug] || 0, teamRole) | ||
} | ||
} | ||
} | ||
}) | ||
// Get the existing memberships and generate a slug->membership object (existingMemberships) | ||
const existingMemberships = {} | ||
;((await user.getTeamMemberships(true)) || []).forEach(membership => { | ||
// Filter out any teams that are not to be managed by this configuration. | ||
// A team is managed by this configuration if any of the follow is true: | ||
// - groupAllTeams is true (all teams to be managed) | ||
// - groupTeams includes this team (this is explicitly a team to be managed) | ||
// - groupOtherTeams is false (not allowed to be a member of other teams - so need to remove them) | ||
if ( | ||
providerOpts.groupAllTeams || | ||
(providerOpts.groupTeams || []).includes(membership.Team.slug) || | ||
!providerOpts.groupOtherTeams | ||
) { | ||
existingMemberships[membership.Team.slug] = membership | ||
} | ||
}) | ||
// We now have the list of desiredTeamMemberships and existingMemberships | ||
// that are in scope of being modified | ||
// - Check each existing membership | ||
// - if in desired list, update role to match and delete from desired list | ||
// - if not in desired list, | ||
// - if groupOtherTeams is false or, delete membership | ||
// - else leave alone | ||
for (const [teamSlug, membership] of Object.entries(existingMemberships)) { | ||
if (Object.hasOwn(desiredTeamMemberships, teamSlug)) { | ||
// This team is in the desired list | ||
if (desiredTeamMemberships[teamSlug] !== membership.role) { | ||
// Role has changed - update membership | ||
// console.log(`changing role in team ${teamSlug} from ${membership.role} to ${desiredTeamMemberships[teamSlug]}`) | ||
const updates = new app.auditLog.formatters.UpdatesCollection() | ||
const oldRole = app.auditLog.formatters.roleObject(membership.role) | ||
const role = app.auditLog.formatters.roleObject(desiredTeamMemberships[teamSlug]) | ||
updates.push('role', oldRole.role, role.role) | ||
membership.role = desiredTeamMemberships[teamSlug] | ||
promises.push(membership.save().then(() => { | ||
return app.auditLog.Team.team.user.roleChanged(user, null, membership.Team, user, updates) | ||
})) | ||
} else { | ||
// Role has not changed - no update needed | ||
// console.log(`no change needed for team ${teamSlug} role ${membership.role}`) | ||
} | ||
// Remove from the desired list as it has been dealt with | ||
delete desiredTeamMemberships[teamSlug] | ||
} else { | ||
// console.log(`removing from team ${teamSlug}`) | ||
// This team is not in the desired list - delete the membership | ||
promises.push(membership.destroy().then(() => { | ||
return app.auditLog.Team.team.user.removed(user, null, membership.Team, user) | ||
})) | ||
} | ||
} | ||
// - Check remaining desired memberships | ||
// - create membership | ||
for (const [teamSlug, teamRole] of Object.entries(desiredTeamMemberships)) { | ||
// This is a new team membership | ||
promises.push(app.db.models.Team.bySlug(teamSlug).then(team => { | ||
if (team) { | ||
// console.log(`adding to team ${teamSlug} role ${teamRole}`) | ||
return app.db.controllers.Team.addUser(team, user, teamRole).then(() => { | ||
return app.auditLog.Team.team.user.added(user, null, team, user) | ||
}) | ||
} else { | ||
// console.log(`team not found ${teamSlug}`) | ||
// Unrecognised team - ignore | ||
return null | ||
} | ||
})) | ||
} | ||
await Promise.all(promises) | ||
} else { | ||
const missingGroupAssertions = new Error(`SAML response missing ${providerOpts.groupAssertionName} assertion`) | ||
missingGroupAssertions.code = 'unknown_sso_user' | ||
throw missingGroupAssertions | ||
} | ||
} | ||
return { | ||
@@ -68,4 +191,5 @@ handleLoginRequest, | ||
getProviderOptions, | ||
getProviderForEmail | ||
getProviderForEmail, | ||
updateTeamMembership | ||
} | ||
} |
@@ -115,2 +115,5 @@ const { generateToken } = require('../../../db/utils') | ||
} | ||
if (cmdResponse.affinity) { | ||
tunnelManager.setTunnelAffinity(deviceId, cmdResponse.affinity) | ||
} | ||
} catch (error) { | ||
@@ -117,0 +120,0 @@ // ensure any attempt to enable the editor is cleaned up if an error occurs |
@@ -29,3 +29,3 @@ module.exports = async function (app) { | ||
} else if (request.session.ownerId !== request.params.projectId) { | ||
// AccesToken being used - but not owned by this project | ||
// AccessToken being used - but not owned by this project | ||
reply.code(404).send({ code: 'not_found', error: 'Not Found' }) | ||
@@ -32,0 +32,0 @@ return // eslint-disable-line no-useless-return |
@@ -1,2 +0,2 @@ | ||
module.exports = async function (app, opts, done) { | ||
module.exports = async function (app, opts) { | ||
/** | ||
@@ -63,3 +63,2 @@ * Begin MFA setup | ||
}) | ||
done() | ||
} |
@@ -5,3 +5,3 @@ const { Authenticator } = require('@fastify/passport') | ||
module.exports = fp(async function (app, opts, done) { | ||
module.exports = fp(async function (app, opts) { | ||
app.addHook('onRequest', async (request, reply) => { | ||
@@ -72,11 +72,23 @@ if (!request.session) { | ||
} | ||
}, async (req, profile, done) => { | ||
if (profile.nameID) { | ||
const user = await app.db.models.User.byUsernameOrEmail(profile.nameID) | ||
}, async (request, samlUser, done) => { | ||
if (samlUser.nameID) { | ||
// console.log(profile) | ||
const user = await app.db.models.User.byUsernameOrEmail(samlUser.nameID) | ||
if (user) { | ||
const state = JSON.parse(request.body.RelayState) | ||
const providerOpts = await app.sso.getProviderOptions(state.provider) | ||
if (providerOpts.groupMapping) { | ||
// This SSO provider is configured to manage team membership. | ||
try { | ||
await app.sso.updateTeamMembership(samlUser, user, providerOpts) | ||
} catch (err) { | ||
done(err) | ||
return | ||
} | ||
} | ||
done(null, user) | ||
} else { | ||
const unknownError = new Error(`Unknown user: ${profile.nameID}`) | ||
const unknownError = new Error(`Unknown user: ${samlUser.nameID}`) | ||
unknownError.code = 'unknown_sso_user' | ||
const userInfo = app.auditLog.formatters.userObject({ email: profile.nameID }) | ||
const userInfo = app.auditLog.formatters.userObject({ email: samlUser.nameID }) | ||
const resp = { code: 'unknown_sso_user', error: 'unauthorized' } | ||
@@ -140,3 +152,2 @@ await app.auditLog.User.account.login(userInfo, resp, userInfo) | ||
}) | ||
done() | ||
}, { name: 'app.ee.routes.sso.auth' }) |
@@ -6,3 +6,3 @@ const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, opts, done) { | ||
module.exports = fp(async function (app, opts) { | ||
registerPermissions({ | ||
@@ -94,4 +94,2 @@ 'saml-provider:create': { description: 'Create a SAML Provider', role: Roles.Admin }, | ||
await app.register(require('./auth')) | ||
done() | ||
}, { name: 'app.ee.routes.sso' }) |
@@ -20,3 +20,3 @@ const { captureCheckIn, captureException } = require('@sentry/node') | ||
*/ | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
const tasks = {} | ||
@@ -26,3 +26,3 @@ const delayedStartupTasks = [] | ||
// Ensure we stop any scheduled tasks when the app is shutting down | ||
app.addHook('onClose', async (_) => { | ||
app.addHook('onClose', async () => { | ||
Object.values(tasks).forEach(task => { | ||
@@ -153,4 +153,2 @@ if (task.job) { | ||
}) | ||
next() | ||
}, { name: 'app.housekeeper' }) |
const { Op } = require('sequelize') | ||
const { randomInt } = require('../utils') | ||
module.exports = { | ||
name: 'expireTokens', | ||
startup: true, | ||
schedule: '@daily', | ||
// Pick a random hour/minute for this task to run at. If the application is | ||
// horizontal scaled, this will avoid two instances running at the same time | ||
schedule: `${randomInt(0, 59)} ${randomInt(0, 23)} * * *`, | ||
run: async function (app) { | ||
@@ -8,0 +12,0 @@ await app.db.models.Session.destroy({ where: { expiresAt: { [Op.lt]: Date.now() } } }) |
@@ -6,7 +6,8 @@ const fs = require('fs/promises') | ||
const { randomInt } = require('../utils') | ||
const METRICS_DIR = path.join(__dirname, 'telemetryMetrics') | ||
// Pick a random time to run the ping on | ||
const randomInt = (min, max) => { return min + Math.floor(Math.random() * (max - min)) } | ||
const PING_TIME = `${randomInt(0, 60)} ${randomInt(0, 24)} * * *` | ||
const PING_TIME = `${randomInt(0, 59)} ${randomInt(0, 23)} * * *` | ||
@@ -13,0 +14,0 @@ /** |
@@ -18,2 +18,7 @@ const Roles = { | ||
// For convenience, we want to be able to look up role values with both 'Role' and 'role' | ||
Object.keys(RoleNames).forEach(role => { | ||
Roles[RoleNames[role]] = parseInt(role) | ||
}) | ||
const TeamRoles = [ | ||
@@ -20,0 +25,0 @@ Roles.Dashboard, |
@@ -5,3 +5,3 @@ const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, opts, next) { | ||
module.exports = fp(async function (app, opts) { | ||
// Dev License: | ||
@@ -87,4 +87,2 @@ /* | ||
next() | ||
function status () { | ||
@@ -91,0 +89,0 @@ const PRE_EXPIRE_WARNING_DAYS = 30 // hard coded |
@@ -7,3 +7,3 @@ const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
let mailTransport | ||
@@ -179,4 +179,2 @@ let exportableSettings = {} | ||
}) | ||
next() | ||
}, { name: 'app.postoffice' }) |
@@ -5,3 +5,3 @@ const { readFileSync, existsSync } = require('fs') | ||
const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, opts, done) { | ||
module.exports = fp(async function (app, opts) { | ||
await app.register(require('@fastify/swagger'), { | ||
@@ -70,2 +70,9 @@ openapi: { | ||
hideUntagged: true, | ||
staticCSP: true, | ||
transformStaticCSP: header => { | ||
header.replace( | ||
/script-src 'self'/, | ||
"script-src 'self' 'unsafe-inline'" | ||
) | ||
}, | ||
uiConfig: { | ||
@@ -98,2 +105,3 @@ defaultModelsExpandDepth: -1, | ||
} | ||
// Fully built path | ||
@@ -162,4 +170,2 @@ let faviconPath = path.join(__dirname, '../../frontend/dist/favicon-32x32.png') | ||
}) | ||
done() | ||
}, { name: 'app.routes.api-docs' }) |
@@ -129,2 +129,5 @@ const { Op } = require('sequelize') | ||
preHandler: app.needsPermission('platform:stats'), | ||
config: { | ||
rateLimit: app.config.rate_limits | ||
}, | ||
schema: { | ||
@@ -265,30 +268,2 @@ summary: 'Get a platform stats - admin-only', | ||
// Undocumented | ||
app.get('/debug/db-migrations', { preHandler: app.needsPermission('platform:debug') }, async (request, reply) => { | ||
reply.send((await app.db.sequelize.query('select * from "MetaVersions"'))[0]) | ||
}) | ||
// Undocumented | ||
app.get('/debug/db-schema', { preHandler: app.needsPermission('platform:debug') }, async (request, reply) => { | ||
const result = {} | ||
let tables | ||
if (app.config.db.type === 'postgres') { | ||
const response = await app.db.sequelize.query('select * from information_schema.tables') | ||
const tt = response[0] | ||
tables = [] | ||
for (let i = 0; i < tt.length; i++) { | ||
const table = tt[i] | ||
if (table.table_schema === 'public') { | ||
tables.push(table.table_name) | ||
} | ||
} | ||
} else { | ||
const response = await app.db.sequelize.showAllSchemas() | ||
tables = response.map(t => t.name) | ||
} | ||
for (let i = 0; i < tables.length; i++) { | ||
result[tables[i]] = await app.db.sequelize.getQueryInterface().describeTable(tables[i]) | ||
} | ||
reply.send(result) | ||
}) | ||
/** | ||
@@ -295,0 +270,0 @@ * Get platform audit logs |
@@ -1,3 +0,1 @@ | ||
const semver = require('semver') | ||
// eslint-disable-next-line no-unused-vars | ||
@@ -409,8 +407,2 @@ const { DeviceTunnelManager } = require('../../ee/lib/deviceEditor/DeviceTunnelManager') | ||
} else { | ||
// check the agent version is compatible with the application | ||
// the agent semver must be greater than or equal to first version that supports applications | ||
if (!device.agentVersion || semver.lt(device.agentVersion, '1.11.0')) { | ||
reply.code(400).send({ code: 'invalid_agent_version', error: 'invalid agent version' }) | ||
return | ||
} | ||
// Check if the specified application is in the same team | ||
@@ -417,0 +409,0 @@ assignToApplication = await app.db.models.Application.byId(request.body.application) |
@@ -39,2 +39,14 @@ const SemVer = require('semver') | ||
await app.db.controllers.Device.updateState(request.device, request.body) | ||
if (request.device.isApplicationOwned) { | ||
if (!request.device.agentVersion || SemVer.lt(request.device.agentVersion, '1.11.0')) { | ||
reply.code(409).send({ | ||
error: 'incorrect-agent-version', | ||
mode: request.device.mode || null, | ||
project: null, | ||
settings: null, | ||
snapshot: null | ||
}) | ||
return | ||
} | ||
} | ||
if (Object.hasOwn(request.body, 'project') && request.body.project !== (request.device.Project?.id || null)) { | ||
@@ -107,2 +119,10 @@ reply.code(409).send({ | ||
} | ||
if (SemVer.satisfies(SemVer.coerce(device.agentVersion), '>=1.11.2')) { | ||
// 1.11.2 includes fix for ESM loading of GOT, so lets use 'latest' as before | ||
nodeRedVersion = 'latest' | ||
} | ||
if (!device.agentVersion || SemVer.lt(device.agentVersion, '1.11.0')) { | ||
reply.code(400).send({ code: 'invalid_agent_version', error: 'invalid agent version' }) | ||
return | ||
} | ||
// determine is device is in application mode? if so, return a default snapshot to permit the user to generate flows | ||
@@ -109,0 +129,0 @@ const DEFAULT_APP_SNAPSHOT = { |
@@ -59,2 +59,5 @@ /** | ||
app.post('/', { | ||
config: { | ||
rateLimit: app.config.rate_limits ? { max: 5, timeWindow: 30000 } : false | ||
}, | ||
schema: { | ||
@@ -61,0 +64,0 @@ summary: 'Create an invitation', |
@@ -52,3 +52,6 @@ const sharedUser = require('./shared/users') | ||
preHandler: app.needsPermission('user:edit'), | ||
config: { allowExpiredPassword: true }, | ||
config: { | ||
allowExpiredPassword: true, | ||
rateLimit: app.config.rate_limits ? { max: 5, timeWindow: 30000 } : false | ||
}, | ||
schema: { | ||
@@ -141,2 +144,5 @@ summary: 'Change the current users password', | ||
preHandler: app.needsPermission('user:edit'), | ||
config: { | ||
rateLimit: app.config.rate_limits ? { max: 5, timeWindow: 30000 } : false | ||
}, | ||
schema: { | ||
@@ -240,2 +246,5 @@ summary: 'Update the current users settings', | ||
app.post('/tokens', { | ||
config: { | ||
rateLimit: app.config.rate_limits ? { max: 5, timeWindow: 30000 } : false | ||
}, | ||
schema: { | ||
@@ -242,0 +251,0 @@ summary: 'Create user Personal Access Token', |
@@ -48,3 +48,3 @@ /** | ||
*/ | ||
async function init (app, opts, done) { | ||
async function init (app, opts) { | ||
await app.register(require('./oauth'), { logLevel: app.config.logging.http }) | ||
@@ -177,6 +177,2 @@ await app.register(require('./permissions')) | ||
// app.post('/account/register', (request, reply) => { | ||
// | ||
// }) | ||
/** | ||
@@ -199,3 +195,10 @@ * Login a user. | ||
config: { | ||
rateLimit: app.config.rate_limits // rate limit this route regardless of global/per-route mode (if enabled) | ||
rateLimit: app.config.rate_limits | ||
? { | ||
max: 5, | ||
timeWindow: 30000, | ||
keyGenerator: app.config.rate_limits.keyGenerator, | ||
hard: true | ||
} | ||
: false | ||
}, | ||
@@ -332,3 +335,10 @@ schema: { | ||
config: { | ||
rateLimit: app.config.rate_limits // rate limit this route regardless of global/per-route mode (if enabled) | ||
rateLimit: app.config.rate_limits | ||
? { | ||
max: 5, | ||
timeWindow: 30000, | ||
keyGenerator: app.config.rate_limits.keyGenerator, | ||
hard: true | ||
} | ||
: false | ||
}, | ||
@@ -453,7 +463,7 @@ schema: { | ||
if (/user_username_lower_unique|Users_username_key/.test(err.parent?.toString())) { | ||
responseMessage = 'username not available' | ||
responseCode = 'invalid_username' | ||
responseMessage = 'Username or email not available' | ||
responseCode = 'invalid_request' | ||
} else if (/user_email_lower_unique|Users_email_key/.test(err.parent?.toString())) { | ||
responseMessage = 'email not available' | ||
responseCode = 'invalid_email' | ||
responseMessage = 'Username or email not available' | ||
responseCode = 'invalid_request' | ||
} else if (err.errors) { | ||
@@ -616,3 +626,10 @@ responseMessage = err.errors.map(err => err.message).join(',') | ||
config: { | ||
rateLimit: false, // never rate limit this route | ||
rateLimit: app.config.rate_limits | ||
? { | ||
max: 5, | ||
timeWindow: 30000, | ||
keyGenerator: app.config.rate_limits.keyGenerator, | ||
hard: true | ||
} | ||
: false, | ||
allowUnverifiedEmail: true | ||
@@ -699,3 +716,10 @@ }, | ||
config: { | ||
rateLimit: app.config.rate_limits // rate limit this route regardless of global/per-route mode (if enabled) | ||
rateLimit: app.config.rate_limits | ||
? { | ||
max: 5, | ||
timeWindow: 30000, | ||
keyGenerator: app.config.rate_limits.keyGenerator, | ||
hard: true | ||
} | ||
: false | ||
}, | ||
@@ -801,4 +825,2 @@ schema: { | ||
}) | ||
done() | ||
} |
@@ -24,3 +24,3 @@ const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, opts, done) { | ||
module.exports = fp(async function (app, opts) { | ||
function hasPermission (teamMembership, scope) { | ||
@@ -104,3 +104,2 @@ if (!teamMembership) { | ||
app.decorate('needsPermission', needsPermission) | ||
done() | ||
}, { name: 'app.routes.auth.permissions' }) |
@@ -14,3 +14,3 @@ /** | ||
const fp = require('fastify-plugin') | ||
module.exports = fp(async function (app, opts, done) { | ||
module.exports = fp(async function (app, opts) { | ||
app.decorate('getPaginationOptions', (request, defaults) => { | ||
@@ -32,3 +32,2 @@ const result = { ...defaults, ...request.query } | ||
await app.register(require('./logging'), { prefix: '/logging', logLevel: app.config.logging.http }) | ||
done() | ||
}, { name: 'app.routes' }) |
@@ -9,3 +9,3 @@ const fs = require('fs') | ||
module.exports = fp(async function (app, _opts, next) { | ||
module.exports = fp(async function (app, _opts) { | ||
const settings = { ...defaultSettings } | ||
@@ -59,3 +59,2 @@ | ||
app.decorate('settings', settingsApi) | ||
next() | ||
}, { name: 'app.settings' }) |
@@ -1,1 +0,1 @@ | ||
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},t=(new Error).stack;t&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[t]="bcaf1993-55f0-41e5-981d-a47e57d80c87",e._sentryDebugIdIdentifier="sentry-dbid-bcaf1993-55f0-41e5-981d-a47e57d80c87")}catch(e){}}();var _global="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};_global.SENTRY_RELEASE={id:"cc8404f0f69946a44010db493510d3e02abcb85e"},function(){"use strict";var e,t,n,r,o,u={},i={};function f(e){var t=i[e];if(void 0!==t)return t.exports;var n=i[e]={id:e,loaded:!1,exports:{}};return u[e](n,n.exports,f),n.loaded=!0,n.exports}f.m=u,e=[],f.O=function(t,n,r,o){if(!n){var u=1/0;for(d=0;d<e.length;d++){n=e[d][0],r=e[d][1],o=e[d][2];for(var i=!0,a=0;a<n.length;a++)(!1&o||u>=o)&&Object.keys(f.O).every((function(e){return f.O[e](n[a])}))?n.splice(a--,1):(i=!1,o<u&&(u=o));if(i){e.splice(d--,1);var c=r();void 0!==c&&(t=c)}}return t}o=o||0;for(var d=e.length;d>0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[n,r,o]},f.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},f.t=function(e,r){if(1&r&&(e=this(e)),8&r)return e;if("object"==typeof e&&e){if(4&r&&e.__esModule)return e;if(16&r&&"function"==typeof e.then)return e}var o=Object.create(null);f.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach((function(t){u[t]=function(){return e[t]}}));return u.default=function(){return e},f.d(o,u),o},f.d=function(e,t){for(var n in t)f.o(t,n)&&!f.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},f.f={},f.e=function(e){return Promise.all(Object.keys(f.f).reduce((function(t,n){return f.f[n](e,t),t}),[]))},f.u=function(e){return"async-vendors.js"},f.miniCssF=function(e){},f.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),f.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},f.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="@flowfuse/flowfuse:",f.l=function(e,t,n,u){if(r[e])r[e].push(t);else{var i,a;if(void 0!==n)for(var c=document.getElementsByTagName("script"),d=0;d<c.length;d++){var l=c[d];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(a=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,f.nc&&i.setAttribute("nonce",f.nc),i.setAttribute("data-webpack",o+n),i.src=e),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(b);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach((function(e){return e(n)})),t)return t(n)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),a&&document.head.appendChild(i)}},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.p="/app/",function(){f.b=document.baseURI||self.location.href;var e={666:0};f.f.j=function(t,n){var r=f.o(e,t)?e[t]:void 0;if(0!==r)if(r)n.push(r[2]);else if(666!=t){var o=new Promise((function(n,o){r=e[t]=[n,o]}));n.push(r[2]=o);var u=f.p+f.u(t),i=new Error;f.l(u,(function(n){if(f.o(e,t)&&(0!==(r=e[t])&&(e[t]=void 0),r)){var o=n&&("load"===n.type?"missing":n.type),u=n&&n.target&&n.target.src;i.message="Loading chunk "+t+" failed.\n("+o+": "+u+")",i.name="ChunkLoadError",i.type=o,i.request=u,r[1](i)}}),"chunk-"+t,t)}else e[t]=0},f.O.j=function(t){return 0===e[t]};var t=function(t,n){var r,o,u=n[0],i=n[1],a=n[2],c=0;if(u.some((function(t){return 0!==e[t]}))){for(r in i)f.o(i,r)&&(f.m[r]=i[r]);if(a)var d=a(f)}for(t&&t(n);c<u.length;c++)o=u[c],f.o(e,o)&&e[o]&&e[o][0](),e[o]=0;return f.O(d)},n=self.webpackChunk_flowfuse_flowfuse=self.webpackChunk_flowfuse_flowfuse||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))}(),f.nc=void 0}(); | ||
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},t=(new Error).stack;t&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[t]="bcaf1993-55f0-41e5-981d-a47e57d80c87",e._sentryDebugIdIdentifier="sentry-dbid-bcaf1993-55f0-41e5-981d-a47e57d80c87")}catch(e){}}();var _global="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};_global.SENTRY_RELEASE={id:"cea84ad20d245166791498f3641423986d69875f"},function(){"use strict";var e,t,n,r,o,u={},i={};function f(e){var t=i[e];if(void 0!==t)return t.exports;var n=i[e]={id:e,loaded:!1,exports:{}};return u[e](n,n.exports,f),n.loaded=!0,n.exports}f.m=u,e=[],f.O=function(t,n,r,o){if(!n){var u=1/0;for(d=0;d<e.length;d++){n=e[d][0],r=e[d][1],o=e[d][2];for(var i=!0,a=0;a<n.length;a++)(!1&o||u>=o)&&Object.keys(f.O).every((function(e){return f.O[e](n[a])}))?n.splice(a--,1):(i=!1,o<u&&(u=o));if(i){e.splice(d--,1);var c=r();void 0!==c&&(t=c)}}return t}o=o||0;for(var d=e.length;d>0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[n,r,o]},f.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},f.t=function(e,r){if(1&r&&(e=this(e)),8&r)return e;if("object"==typeof e&&e){if(4&r&&e.__esModule)return e;if(16&r&&"function"==typeof e.then)return e}var o=Object.create(null);f.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach((function(t){u[t]=function(){return e[t]}}));return u.default=function(){return e},f.d(o,u),o},f.d=function(e,t){for(var n in t)f.o(t,n)&&!f.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},f.f={},f.e=function(e){return Promise.all(Object.keys(f.f).reduce((function(t,n){return f.f[n](e,t),t}),[]))},f.u=function(e){return"async-vendors.js"},f.miniCssF=function(e){},f.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),f.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},f.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="@flowfuse/flowfuse:",f.l=function(e,t,n,u){if(r[e])r[e].push(t);else{var i,a;if(void 0!==n)for(var c=document.getElementsByTagName("script"),d=0;d<c.length;d++){var l=c[d];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(a=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,f.nc&&i.setAttribute("nonce",f.nc),i.setAttribute("data-webpack",o+n),i.src=e),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach((function(e){return e(n)})),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),a&&document.head.appendChild(i)}},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.p="/app/",function(){f.b=document.baseURI||self.location.href;var e={666:0};f.f.j=function(t,n){var r=f.o(e,t)?e[t]:void 0;if(0!==r)if(r)n.push(r[2]);else if(666!=t){var o=new Promise((function(n,o){r=e[t]=[n,o]}));n.push(r[2]=o);var u=f.p+f.u(t),i=new Error;f.l(u,(function(n){if(f.o(e,t)&&(0!==(r=e[t])&&(e[t]=void 0),r)){var o=n&&("load"===n.type?"missing":n.type),u=n&&n.target&&n.target.src;i.message="Loading chunk "+t+" failed.\n("+o+": "+u+")",i.name="ChunkLoadError",i.type=o,i.request=u,r[1](i)}}),"chunk-"+t,t)}else e[t]=0},f.O.j=function(t){return 0===e[t]};var t=function(t,n){var r,o,u=n[0],i=n[1],a=n[2],c=0;if(u.some((function(t){return 0!==e[t]}))){for(r in i)f.o(i,r)&&(f.m[r]=i[r]);if(a)var d=a(f)}for(t&&t(n);c<u.length;c++)o=u[c],f.o(e,o)&&e[o]&&e[o][0](),e[o]=0;return f.O(d)},n=self.webpackChunk_flowfuse_flowfuse=self.webpackChunk_flowfuse_flowfuse||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))}(),f.nc=void 0}(); |
{ | ||
"name": "@flowfuse/flowfuse", | ||
"version": "1.15.1-cc8404f-202401101717.0", | ||
"version": "1.15.1-cea84ad-202401161048.0", | ||
"description": "An open source low-code development platform", | ||
@@ -67,3 +67,3 @@ "homepage": "https://flowfuse.com", | ||
"@fastify/swagger": "^8.10.1", | ||
"@fastify/swagger-ui": "^1.9.0", | ||
"@fastify/swagger-ui": "^2.1.0", | ||
"@fastify/websocket": "^8.1.0", | ||
@@ -84,5 +84,5 @@ "@flowfuse/driver-localfs": "nightly", | ||
"dotenv": "^16.3.1", | ||
"fastify": "^4.24.0", | ||
"fastify": "^4.25.2", | ||
"fastify-metrics": "^10.3.2", | ||
"fastify-plugin": "^4.5.0", | ||
"fastify-plugin": "^4.5.1", | ||
"handlebars": "^4.7.7", | ||
@@ -89,0 +89,0 @@ "hashids": "^2.3.0", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
6361342
311
34819
+ Added@fastify/swagger-ui@2.1.0(transitive)
- Removed@fastify/swagger-ui@1.10.2(transitive)
Updated@fastify/swagger-ui@^2.1.0
Updatedfastify@^4.25.2
Updatedfastify-plugin@^4.5.1