Skip to content

Commit

Permalink
Update licensing to combine device and instance limits
Browse files Browse the repository at this point in the history
  • Loading branch information
knolleary committed Mar 12, 2024
1 parent 4ade4c3 commit 1790cec
Show file tree
Hide file tree
Showing 26 changed files with 382 additions and 176 deletions.
5 changes: 2 additions & 3 deletions forge/db/models/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ module.exports = {
// if the product is licensed, we permit overage
const isLicensed = app.license.active()
if (isLicensed !== true) {
const deviceLimit = app.license.get('devices')
const deviceCount = await M.Device.count()
if (deviceCount >= deviceLimit) {
const { devices } = await app.license.usage('devices')
if (devices.count >= devices.limit) {
throw new Error('license limit reached')
}
}
Expand Down
11 changes: 5 additions & 6 deletions forge/db/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,16 @@ module.exports = {
// if the product is licensed, we permit overage
const isLicensed = app.license.active()
if (isLicensed !== true) {
const projectLimit = app.license.get('projects')
const projectCount = await M.Project.count()
if (projectCount >= projectLimit) {
const { instances } = await app.license.usage('instances')
if (instances.count >= instances.limit) {
throw new Error('license limit reached')
}
}
},
afterCreate: async (project, opts) => {
const { projects } = await app.license.usage('projects')
if (projects.count > projects.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, projects)
const { instances } = await app.license.usage('instances')
if (instances.count > instances.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, instances)
}
},
afterDestroy: async (project, opts) => {
Expand Down
6 changes: 3 additions & 3 deletions forge/housekeeper/tasks/licenseOverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ module.exports = {
startup: true,
schedule: '@weekly', // Run once a week, sunday midnight
run: async function (app) {
const { users, teams, projects, devices } = await app.license.usage()
const { users, teams, instances, devices } = await app.license.usage()
if (users?.count > users?.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, users)
}
if (teams?.count > teams?.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, teams)
}
if (projects?.count > projects?.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, projects)
if (instances?.count > instances?.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, instances)
}
if (devices?.count > devices?.limit) {
await app.auditLog.Platform.platform.license.overage('system', null, devices)
Expand Down
76 changes: 51 additions & 25 deletions forge/licensing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,38 @@ const fp = require('fastify-plugin')
const loader = require('./loader')

module.exports = fp(async function (app, opts) {
// Dev License:
/*
Dev License:
License Details:
{
"id": "a428c0da-51a4-4e75-bb90-e8932b498dda",
"ver": "2024-03-04",
"iss": "FlowForge Inc.",
"sub": "FlowForge Inc. Development",
"nbf": 1662422400,
"exp": 7986902399,
"nbf": 1709510400,
"exp": 7986816000,
"note": "Development-mode Only. Not for production",
"users": 150,
"teams": 50,
"projects": 50,
"devices": 50,
"instances": 100,
"tier": "enterprise",
"dev": true
}
License:
---
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0MjhjMGRhLTUxYTQtNGU3NS1iYjkwLWU4OTMyYjQ5OGRkYSIsInZlciI6IjIwMjQtMDMtMDQiLCJpc3MiOiJGbG93Rm9yZ2UgSW5jLiIsInN1YiI6IkZsb3dGb3JnZSBJbmMuIERldmVsb3BtZW50IiwibmJmIjoxNzA5NTEwNDAwLCJleHAiOjc5ODY4MTYwMDAsIm5vdGUiOiJEZXZlbG9wbWVudC1tb2RlIE9ubHkuIE5vdCBmb3IgcHJvZHVjdGlvbiIsInVzZXJzIjoxNTAsInRlYW1zIjo1MCwiaW5zdGFuY2VzIjoxMDAsInRpZXIiOiJlbnRlcnByaXNlIiwiZGV2Ijp0cnVlLCJpYXQiOjE3MDk1NjI5NTZ9.f9ZDE-IelVeM53JsHWyHd9FggZ30CRFCJ_jhxebALwt--TFnmL5d7f9CBd9g6fmGjro_y0ZINBJKkzYPSeXKrw
---
*/
// const devLicense = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbG93Rm9yZ2UgSW5jLiIsInN1YiI6IkZsb3dGb3JnZSBJbmMuIERldmVsb3BtZW50IiwibmJmIjoxNjYyNDIyNDAwLCJleHAiOjc5ODY5MDIzOTksIm5vdGUiOiJEZXZlbG9wbWVudC1tb2RlIE9ubHkuIE5vdCBmb3IgcHJvZHVjdGlvbiIsInVzZXJzIjoxNTAsInRlYW1zIjo1MCwicHJvamVjdHMiOjUwLCJkZXZpY2VzIjo1MCwiZGV2Ijp0cnVlLCJpYXQiOjE2NjI0ODI5ODd9.e8Jeppq4aURwWYz-rEpnXs9RY2Y7HF7LJ6rMtMZWdw2Xls6-iyaiKV1TyzQw5sUBAhdUSZxgtiFH5e_cNJgrUg'

// 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: 150,
teams: 50,
projects: 50,
devices: 50
users: 5,
teams: 5,
instances: 5
}

let userLicense = await app.settings.get('license')
Expand Down Expand Up @@ -107,7 +115,7 @@ module.exports = fp(async function (app, opts) {

/**
* Get usage and limits information for the license (all, or by resource)
* @param {'users'|'teams'|'projects'|'devices'} [resource] The name of resource usage to get. Leave null to get all usage
* @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) {
Expand All @@ -126,34 +134,46 @@ module.exports = fp(async function (app, opts) {
limit: licenseApi.get('teams')
}
}
if (!resource || resource === 'projects') {
usage.projects = {
resource: 'projects',
count: await app.db.models.Project.count(),
limit: licenseApi.get('projects')
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'))
}
}
}
if (!resource || resource === 'devices') {
usage.devices = {
resource: 'devices',
count: await app.db.models.Device.count(),
limit: 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')
}
}
}
return usage
}

async function reportUsage () {
const { users, teams, projects, devices } = await usage()
const { users, teams, devices, instances } = await usage()
const logUse = (name, count, limit) => {
const logger = (count > limit ? app.log.warn : app.log.info).bind(app.log)
logger(`${name}: ${count}/${limit}`)
}
app.log.info('Usage : count/limit')
logUse(' Users ', users.count, users.limit)
logUse(' Teams ', teams.count, teams.limit)
logUse(' Projects ', projects.count, projects.limit)
logUse(' Devices ', devices.count, devices.limit)
logUse(' Instances ', instances.count, instances.limit)
if (!licenseModeCombinedInstances) {
logUse(' Devices ', devices.count, devices.limit)
}
}

async function applyLicense (license) {
Expand All @@ -173,6 +193,12 @@ module.exports = fp(async function (app, opts) {
} 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' })
11 changes: 5 additions & 6 deletions forge/licensing/license-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ const { v4: uuidv4 } = require('uuid')

const licenseHolder = await promptly.prompt('License holder name: ')

const maxUsers = parseInt(await promptly.prompt('Max allowed users: ', { default: '150' }))
const maxTeams = parseInt(await promptly.prompt('Max allowed teams: ', { default: '50' }))
const maxProjects = parseInt(await promptly.prompt('Max allowed projects: ', { default: '50' }))
const maxDevices = parseInt(await promptly.prompt('Max allowed devices: ', { default: '50' }))
const maxUsers = parseInt(await promptly.prompt('Max allowed users: ', { default: '5' }))
const maxTeams = parseInt(await promptly.prompt('Max allowed teams: ', { default: '5' }))
const maxInstances = parseInt(await promptly.prompt('Max allowed instances (hosted + devices): ', { default: '5' }))

const licenseNotes = devLicense
? 'Development-mode Only. Not for production'
Expand Down Expand Up @@ -96,15 +95,15 @@ const { v4: uuidv4 } = require('uuid')

const licenseDetails = {
id: licenseId,
ver: '2024-03-04', // Used to determined the format of the license.
iss: 'FlowForge Inc.', // DO NOT CHANGE
sub: licenseHolder, // Name of the license holder
nbf: validFrom,
exp: expiry, // Expiry of the license in epoch seconds
note: licenseNotes, // Freeform text to associate with license
users: maxUsers,
teams: maxTeams,
projects: maxProjects,
devices: maxDevices,
instances: maxInstances,
tier: licenseTier
}

Expand Down
9 changes: 7 additions & 2 deletions forge/licensing/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ class LicenseDetails {
constructor (license, claims) {
// this.license = license;
this.id = claims.id
this.version = claims.ver || ''
this.note = claims.note
this.organisation = claims.sub
this.validFrom = new Date(claims.nbf * 1000)
this.expiresAt = new Date(claims.exp * 1000)
this.dev = claims.dev
this.users = claims.users || 0
this.projects = claims.projects || 0
this.devices = claims.devices || 0
if (Object.hasOwn(claims, 'instances')) {
this.instances = claims.instances || 0
} else {
this.projects = claims.projects || 0
this.devices = claims.devices || 0
}
this.teams = claims.teams || 0
this.tier = claims.tier || 'enterprise'
Object.freeze(this)
Expand Down
6 changes: 4 additions & 2 deletions forge/routes/api/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ module.exports = async function (app) {
maxTeams: license.teams,
teamsByType: {},
instanceCount: 0,
maxInstances: license.projects,
maxInstances: license.projects || license.instances,
instancesByState: {},
deviceCount: await app.db.models.Device.count(),
maxDevices: license.devices,
devicesByMode: {}
}
if (Object.hasOwn(license, 'devices')) {
result.maxDevices = license.devices
}
userCount.forEach(u => {
result.userCount += u.count
if (u.admin === 1) {
Expand Down
62 changes: 31 additions & 31 deletions forge/routes/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,16 +492,6 @@ async function init (app, opts) {
}
}, async (request, reply) => {
try {
if (app.settings.get('user:team:auto-create')) {
const teamLimit = app.license.get('teams')
const teamCount = await app.db.models.Team.count()
if (teamCount >= teamLimit) {
const resp = { code: 'team_limit_reached', error: 'Unable to auto create user team: license limit reached' }
await app.auditLog.User.account.verify.verifyToken(request.session.User, resp)
reply.code(400).send(resp)
return
}
}
let sessionUser
if (request.sid) {
request.session = await app.db.controllers.Session.getOrExpire(request.sid)
Expand All @@ -517,29 +507,39 @@ async function init (app, opts) {
return
}

// only create a personal team if no other teams exist
if (app.settings.get('user:team:auto-create') && !((await app.db.models.Team.forUser(verifiedUser)).length)) {
let teamTypeId = app.settings.get('user:team:auto-create:teamType')

if (!teamTypeId) {
// No team type set - pick the 'first' one based on 'order'
const teamTypes = await app.db.models.TeamType.findAll({ where: { active: true }, order: [['order', 'ASC']], limit: 1 })
teamTypeId = teamTypes[0].id
} else {
teamTypeId = app.db.models.TeamType.decodeHashid(teamTypeId)
}
const teamProperties = {
name: `Team ${verifiedUser.name}`,
slug: verifiedUser.username,
TeamTypeId: teamTypeId
if (app.settings.get('user:team:auto-create')) {
const teamLimit = app.license.get('teams')
const teamCount = await app.db.models.Team.count()
if (teamCount >= teamLimit) {
const resp = { code: 'team_limit_reached', error: 'Unable to auto create user team: license limit reached' }
await app.auditLog.User.account.verify.verifyToken(verifiedUser, resp)
reply.code(400).send(resp)
return
}
const team = await app.db.controllers.Team.createTeamForUser(teamProperties, verifiedUser)
await app.auditLog.Platform.platform.team.created(request.session?.User || verifiedUser, null, team)
await app.auditLog.User.account.verify.autoCreateTeam(request.session?.User || verifiedUser, null, team)
// only create a personal team if no other teams exist
if (!((await app.db.models.Team.forUser(verifiedUser)).length)) {
let teamTypeId = app.settings.get('user:team:auto-create:teamType')

if (!teamTypeId) {
// No team type set - pick the 'first' one based on 'order'
const teamTypes = await app.db.models.TeamType.findAll({ where: { active: true }, order: [['order', 'ASC']], limit: 1 })
teamTypeId = teamTypes[0].id
} else {
teamTypeId = app.db.models.TeamType.decodeHashid(teamTypeId)
}
const teamProperties = {
name: `Team ${verifiedUser.name}`,
slug: verifiedUser.username,
TeamTypeId: teamTypeId
}
const team = await app.db.controllers.Team.createTeamForUser(teamProperties, verifiedUser)
await app.auditLog.Platform.platform.team.created(request.session?.User || verifiedUser, null, team)
await app.auditLog.User.account.verify.autoCreateTeam(request.session?.User || verifiedUser, null, team)

if (app.license.active() && app.billing) {
// This checks to see if the team should be in trial mode
await app.billing.setupTrialTeamSubscription(team, verifiedUser)
if (app.license.active() && app.billing) {
// This checks to see if the team should be in trial mode
await app.billing.setupTrialTeamSubscription(team, verifiedUser)
}
}
}

Expand Down
Loading

0 comments on commit 1790cec

Please sign in to comment.