Skip to content

Commit

Permalink
Merge branch 'main' into locked-template-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hardillb committed Jul 30, 2024
2 parents 2b2cdc8 + 628c562 commit 9fa2f5a
Show file tree
Hide file tree
Showing 29 changed files with 800 additions and 210 deletions.
58 changes: 1 addition & 57 deletions docs/install/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,62 +14,6 @@ We also provide one-click installs of the Docker version:
- [Digital Ocean Docker Install Guide](/docs/install/docker/digital-ocean.md)
- [AWS Docker Install Guide](/docs/install/docker/aws-marketplace.md)

## Request a Trial Enterprise License

Experience the full capabilities of FlowFuse by obtaining a complimentary 30-day Enterprise license. This trial offers you an opportunity to thoroughly evaluate the features and functionalities of FlowFuse in your environment. To begin your trial, simply complete the form below.

<div id="license-message"></div>

<script charset="utf-8" type="text/javascript" src="//js-eu1.hsforms.net/forms/embed/v2.js"></script>
<script>
function GenerateLicense(formData) {
if (formData) {
const jsonData = typeof formData === 'object' ? JSON.stringify(formData) : formData;

fetch('https://energetic-sanderling-4472.flowfuse.cloud/license/trial', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const messageElement = document.getElementById('license-message');
if (messageElement) {
messageElement.innerHTML = `<p><strong>Thank you for requesting a trial license. Below is your license key. Please copy it and save it securely, as it will not be available again if you leave, come back, or refresh the screen:</strong></p><code style="display:block;overflow-wrap: anywhere;padding: 10px;border: 1px solid lightgray;margin-top: 10px;"">${data[0].license}</code>`;
} else {
console.error('Message element not found');
}
})
.catch(error => {
const messageElement = document.getElementById('license-message');
if (messageElement) {
messageElement.textContent = 'Error generating license. Please try again later.';
} else {
console.error('Message element not found');
}
});
}
}
hbspt.forms.create({
region: "eu1",
portalId: "26586079",
formId: "41e858e1-6756-45be-9082-3980237fa229",
onFormSubmitted: function ($form, data) {
document.querySelector('.hbspt-form').style.display = 'none';
GenerateLicense(data.submissionValues);
}
});
</script>
## Upgrading FlowFuse

If you are upgrading FlowFuse, please refer to the [Upgrade Guide](/docs/upgrade/README.md)
Expand All @@ -86,4 +30,4 @@ If you need assistance, request our complimentary Installation Service, and we w
portalId: "26586079",
formId: "22edc659-d098-4767-aeb1-6480daae41ad"
});
</script>
</script>
38 changes: 36 additions & 2 deletions forge/db/controllers/Invitation.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,30 @@ module.exports = {
}

for (let i = 0; i < pendingInvites.length; i++) {
const invite = await app.db.models.Invitation.create(pendingInvites[i].opts)
let invite = await app.db.models.Invitation.create(pendingInvites[i].opts)
// Re-get the new invite so the User/Team properties are pre-fetched
results[pendingInvites[i].userDetail] = await app.db.models.Invitation.byId(invite.hashid)
invite = await app.db.models.Invitation.byId(invite.hashid)
results[pendingInvites[i].userDetail] = invite
if (!invite.external) {
await app.notifications.send(
invite.invitee,
'team-invite',
{
invite: {
id: invite.hashid
},
team: {
id: invite.team.hashid,
name: invite.team.name
},
invitor: {
username: invitor.username
},
role
},
`team-invite:${invite.hashid}`
)
}
}
return results
},
Expand All @@ -80,13 +101,26 @@ module.exports = {
throw new Error('Cannot identify user for this invitation')
}
await app.db.controllers.Team.addUser(invitation.team, invitedUser, role)
const notificationReference = `team-invite:${invitation.hashid}`
await invitation.destroy()
await app.notifications.remove(invitedUser, notificationReference)

app.auditLog.Team.team.user.invite.accepted(user, null, invitation.team, invitedUser, role)
},

rejectInvitation: async (app, invitation, user) => {
const role = invitation.role || Roles.Member
let invitedUser = invitation.invitee
if (!invitedUser && invitation.external) {
// This won't have a full user object attached as they had not registered
// when the invitation was created.
if (user.email === invitation.email) {
invitedUser = user
}
}
const notificationReference = `team-invite:${invitation.hashid}`
await invitation.destroy()
await app.notifications.remove(invitedUser, notificationReference)
app.auditLog.Team.team.user.invite.rejected(user, null, invitation.team, invitation.invitee, role)
}
}
34 changes: 34 additions & 0 deletions forge/db/migrations/20240711-01-add-Notifications-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Add Notifications table
*/
const { DataTypes } = require('sequelize')

module.exports = {
/**
* upgrade database
* @param {QueryInterface} context Sequelize.QueryInterface
*/
up: async (context) => {
await context.createTable('Notifications', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
type: { type: DataTypes.STRING, allowNull: false },
reference: { type: DataTypes.STRING, allowNull: true },
read: { type: DataTypes.BOOLEAN, defaultValue: false },
data: { type: DataTypes.TEXT },
createdAt: { type: DataTypes.DATE, allowNull: false },
UserId: {
type: DataTypes.INTEGER,
references: { model: 'Users', key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
}
})
},
down: async (context) => {
}
}
99 changes: 99 additions & 0 deletions forge/db/models/Notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* An user notification
*/

const { DataTypes, Op } = require('sequelize')

const { buildPaginationSearchClause } = require('../utils')

module.exports = {
name: 'Notification',
schema: {
type: { type: DataTypes.STRING, allowNull: false },
reference: { type: DataTypes.STRING, allowNull: true },
read: { type: DataTypes.BOOLEAN, defaultValue: false },
data: {
type: DataTypes.TEXT,
get () {
const rawValue = this.getDataValue('data')
if (rawValue === undefined || rawValue === null) {
return {}
}
return JSON.parse(rawValue)
},
set (value) {
this.setDataValue('data', JSON.stringify(value))
}
}
},
options: {
updatedAt: false
},
associations: function (M) {
this.belongsTo(M.User, {
onDelete: 'CASCADE'
})
},
finders: function (M) {
return {
static: {
byId: async (id, user) => {
if (typeof id === 'string') {
id = M.Notification.decodeHashid(id)
}
return this.findOne({
where: {
id,
UserId: user.id
}
})
},
byReference: async (reference, user) => {
return this.findOne({
where: {
reference,
UserId: user.id
}
})
},
forUser: async (user, pagination = {}) => {
const limit = parseInt(pagination.limit) || 100
const where = {
UserId: user.id
}
if (pagination.cursor) {
// As we aren't using the default cursor behaviour (Op.gt)
// set the appropriate clause and delete cursor so that
// buildPaginationSearchClause doesn't do it for us
where.id = { [Op.lt]: M.Notification.decodeHashid(pagination.cursor) }
delete pagination.cursor
}
const { count, rows } = await this.findAndCountAll({
where: buildPaginationSearchClause(
pagination,
where
),
order: [['id', 'DESC']],
include: {
model: M.User,
attributes: ['id', 'hashid', 'username']
},
limit
})

return {
meta: {
next_cursor: rows.length === limit ? rows[rows.length - 1].hashid : undefined
},
count,
notifications: rows
}
}
}
}
},
meta: {
slug: false,
links: false
}
}
3 changes: 2 additions & 1 deletion forge/db/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ const modelTypes = [
'StorageLibrary',
'AuditLog',
'BrokerClient',
'OAuthSession'
'OAuthSession',
'Notification'
]

// A local map of the known models.
Expand Down
36 changes: 36 additions & 0 deletions forge/db/views/Notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module.exports = function (app) {
app.addSchema({
$id: 'Notification',
type: 'object',
properties: {
id: { type: 'string' },
type: { type: 'string' },
createdAt: { type: 'string' },
read: { type: 'boolean' },
data: { type: 'object', additionalProperties: true }
}
})
app.addSchema({
$id: 'NotificationList',
type: 'array',
items: {
$ref: 'Notification'
}
})

function notificationList (notifications) {
return notifications.map(notification => {
return {
id: notification.hashid,
type: notification.type,
createdAt: notification.createdAt,
read: notification.read,
data: notification.data
}
})
}

return {
notificationList
}
}
1 change: 1 addition & 0 deletions forge/db/views/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const modelTypes = [
'Device',
'DeviceGroup',
'Invitation',
'Notification',
'Project',
'ProjectSnapshot',
'ProjectStack',
Expand Down
2 changes: 2 additions & 0 deletions forge/forge.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const db = require('./db')
const ee = require('./ee')
const housekeeper = require('./housekeeper')
const license = require('./licensing')
const notifications = require('./notifications')
const postoffice = require('./postoffice')
const routes = require('./routes')
const settings = require('./settings')
Expand Down Expand Up @@ -460,6 +461,7 @@ module.exports = async (options = {}) => {
await server.register(routes, { logLevel: server.config.logging.http })
// Post Office : handles email
await server.register(postoffice)
await server.register(notifications)
// Comms : real-time communication broker
await server.register(comms)
// Containers:
Expand Down
38 changes: 38 additions & 0 deletions forge/notifications/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const fp = require('fastify-plugin')

module.exports = fp(async function (app, _opts) {
/**
* Send a user a notification
* @param {User} user who the notification is for
* @param {string} type the type of the notification
* @param {Object} data meta data for the notification - specific to the type
* @param {string} reference a key that can be used to lookup this notification, for example: `invite:HASHID`
*
*/
async function send (user, type, data, reference = null) {
return app.db.models.Notification.create({
UserId: user.id,
type,
reference,
data
})
}

/**
* Remove a notification for a user with the given reference.
* For example, when an invite is accepted/rejected, we can clear the associated notification
* @param {User} user
* @param {string} reference
*/
async function remove (user, reference) {
const notification = await app.db.models.Notification.byReference(reference, user)
if (notification) {
await notification.destroy()
}
}

app.decorate('notifications', {
send,
remove
})
}, { name: 'app.notifications' })
4 changes: 4 additions & 0 deletions forge/routes/api/teamInvitations.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ module.exports = async function (app) {
if (invitation && invitation.teamId === request.team.id) {
const role = invitation.role || Roles.Member
const invitedUser = app.auditLog.formatters.userObject(invitation.external ? invitation : invitation.invitee)
if (!invitation.external) {
const notificationReference = `team-invite:${invitation.hashid}`
await app.notifications.remove(invitation.invitee, notificationReference)
}
await invitation.destroy()
await app.auditLog.Team.team.user.uninvited(request.session.User, null, request.team, invitedUser, role)
reply.send({ status: 'okay' })
Expand Down
Loading

0 comments on commit 9fa2f5a

Please sign in to comment.