@flowfuse/flowfuse
Version:
An open source low-code development platform
219 lines (205 loc) • 8.32 kB
JavaScript
const fp = require('fastify-plugin')
const loader = require('./loader')
module.exports = fp(async function (app, opts) {
/*
Dev License:
License Details:
{
"id": "a428c0da-51a4-4e75-bb90-e8932b498dda",
"ver": "2024-03-04",
"iss": "FlowForge Inc.",
"sub": "FlowForge Inc. Development",
"nbf": 1709510400,
"exp": 7986816000,
"note": "Development-mode Only. Not for production",
"users": 150,
"teams": 50,
"instances": 100,
"tier": "enterprise",
"dev": true
}
License:
---
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0MjhjMGRhLTUxYTQtNGU3NS1iYjkwLWU4OTMyYjQ5OGRkYSIsInZlciI6IjIwMjQtMDMtMDQiLCJpc3MiOiJGbG93Rm9yZ2UgSW5jLiIsInN1YiI6IkZsb3dGb3JnZSBJbmMuIERldmVsb3BtZW50IiwibmJmIjoxNzA5NTEwNDAwLCJleHAiOjc5ODY4MTYwMDAsIm5vdGUiOiJEZXZlbG9wbWVudC1tb2RlIE9ubHkuIE5vdCBmb3IgcHJvZHVjdGlvbiIsInVzZXJzIjoxNTAsInRlYW1zIjo1MCwiaW5zdGFuY2VzIjoxMDAsInRpZXIiOiJlbnRlcnByaXNlIiwiZGV2Ijp0cnVlLCJpYXQiOjE3MDk1NjI5NTZ9.f9ZDE-IelVeM53JsHWyHd9FggZ30CRFCJ_jhxebALwt--TFnmL5d7f9CBd9g6fmGjro_y0ZINBJKkzYPSeXKrw
---
*/
// TODO: load license from local file or app.config.XYZ
// Default to separate licensing for devices/projects.
// This will change to combined licensing when we update the defaults
let licenseModeCombinedInstances = true
const defaultLimits = {
users: 5,
teams: 5,
instances: 5,
mqttClients: 0
}
let userLicense = await app.settings.get('license')
if (!userLicense) {
userLicense = app.config.license
}
// if (!userLicense) {
// console.warn("No user-provided license found - using development license")
// userLicense = devLicense;
// }
let activeLicense = null
const licenseApi = {
apply: async (license) => {
await applyLicense(license)
await app.settings.set('license', license)
},
inspect: async (license) => {
return await loader.verifyLicense(license)
},
active: () => activeLicense !== null,
usage: async (resource) => {
return await usage(resource)
},
get: (key) => {
if (!key) {
return activeLicense
}
if (activeLicense) {
if (Object.hasOwn(activeLicense, key)) {
if (key === 'tier') {
return activeLicense[key]?.toLowerCase()
}
return activeLicense[key]
}
return undefined
}
return defaultLimits[key]
},
status: () => {
return status()
},
defaults: defaultLimits
}
if (userLicense) {
try {
await applyLicense(userLicense)
} catch (err) {
throw new Error('Failed to apply license: ' + err.toString())
}
} else {
app.log.info('No license applied')
await reportUsage()
}
app.decorate('license', licenseApi)
function status () {
const PRE_EXPIRE_WARNING_DAYS = 30 // hard coded
const oneDay = 24 * 60 * 60 * 1000 // 24 hours
const now = Date.now()
const isLicensed = licenseApi.active()
const licenseType = isLicensed ? (licenseApi.get('dev') ? 'DEV' : 'EE') : 'CE'
const expiresAt = isLicensed
? licenseApi.get('expiresAt') // EE: get from license
: new Date(now + (365 * oneDay)) // CE: always valid (always 1 year from now)
const expired = expiresAt < now
const daysRemaining = expired ? 0 : Math.floor((expiresAt - now) / oneDay)
const expiring = !expired && daysRemaining <= PRE_EXPIRE_WARNING_DAYS
return {
type: licenseType, // 'CE', 'EE', or 'DEV'
expiresAt, // Date
expiring, // boolean flag
expired, // boolean flag
daysRemaining, // number of days remaining
PRE_EXPIRE_WARNING_DAYS // number of days before expiration to warn
}
}
/**
* Get usage and limits information for the license (all, or by resource)
* @param {'users'|'teams'|'instances'|'devices'} [resource] The name of resource usage to get. Leave null to get all usage
* @returns The usage information
*/
async function usage (resource) {
const usage = {}
if (!resource || resource === 'users') {
usage.users = {
resource: 'users',
count: await app.db.models.User.count(),
limit: licenseApi.get('users')
}
}
if (!resource || resource === 'teams') {
usage.teams = {
resource: 'teams',
count: await app.db.models.Team.count(),
limit: licenseApi.get('teams')
}
}
if (licenseModeCombinedInstances) {
if (!resource || resource === 'instances' || resource === 'devices') {
usage[resource || 'instances'] = {
resource: resource || 'instances',
count: (await app.db.models.Project.count()) + (await app.db.models.Device.count()),
limit: licenseApi.get('instances') || (licenseApi.get('projects') + licenseApi.get('devices'))
}
}
} else {
if (!resource || resource === 'instances') {
usage.instances = {
resource: 'instances',
count: await app.db.models.Project.count(),
limit: licenseApi.get('projects')
}
}
if (!resource || resource === 'devices') {
usage.devices = {
resource: 'devices',
count: await app.db.models.Device.count(),
limit: licenseApi.get('devices')
}
}
}
if (!resource || resource === 'mqttClients') {
usage.mqttClients = {
resource: 'mqttClients',
count: await app.db.models.TeamBrokerClient.count(),
limit: licenseApi.get('mqttClients')
}
}
return usage
}
async function reportUsage () {
const { users, teams, devices, instances, mqttClients } = await usage()
const logUse = (name, count, limit) => {
const logger = (count > limit ? app.log.warn : app.log.info).bind(app.log)
logger(` ${name.padEnd(13, ' ')}: ${count}/${limit}`)
}
app.log.info('Usage : count/limit')
logUse('Users', users.count, users.limit)
logUse('Teams', teams.count, teams.limit)
logUse('Instances', instances.count, instances.limit)
if (!licenseModeCombinedInstances) {
logUse('Devices', devices.count, devices.limit)
}
if (mqttClients.limit > 0 || mqttClients.count > 0) {
logUse('MQTT Clients', mqttClients.count, mqttClients.limit)
}
}
async function applyLicense (license) {
activeLicense = await loader.verifyLicense(license)
app.log.info('License verified:')
if (activeLicense.dev) {
app.log.info(' ****************************')
app.log.info(' * Development-mode License *')
app.log.info(' ****************************')
}
app.log.info(` License ID : ${activeLicense.id}`)
app.log.info(` Org : ${activeLicense.organisation}`)
app.log.info(` Valid From : ${activeLicense.validFrom.toISOString()}`)
app.log.info(` License Tier : ${activeLicense.tier}`)
if (activeLicense.expired) {
app.log.warn(` Expired : ${activeLicense.expiresAt.toISOString()}`)
} else {
app.log.info(` Expires : ${activeLicense.expiresAt.toISOString()}`)
}
if (licenseApi.get('instances') === undefined) {
// pre 2.2 license that does not combine instance and device counts
licenseModeCombinedInstances = false
} else {
licenseModeCombinedInstances = true
}
await reportUsage()
}
}, { name: 'app.licensing' })