Merge pull request #15297 from Budibase/type-portal-licensing-store

Convert portal licensing store to TS
This commit is contained in:
Andrew Kingston 2025-01-07 14:13:40 +00:00 committed by GitHub
commit 5b50108ab2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 330 additions and 291 deletions

View File

@ -1,279 +0,0 @@
import { writable, get } from "svelte/store"
import { API } from "@/api"
import { auth, admin } from "@/stores/portal"
import { Constants } from "@budibase/frontend-core"
import { StripeStatus } from "@/components/portal/licensing/constants"
import { PlanModel } from "@budibase/types"
const UNLIMITED = -1
export const createLicensingStore = () => {
const DEFAULT = {
// navigation
goToUpgradePage: () => {},
goToPricingPage: () => {},
// the top level license
license: undefined,
isFreePlan: true,
isEnterprisePlan: true,
isBusinessPlan: true,
// features
groupsEnabled: false,
backupsEnabled: false,
brandingEnabled: false,
scimEnabled: false,
environmentVariablesEnabled: false,
budibaseAIEnabled: false,
customAIConfigsEnabled: false,
auditLogsEnabled: false,
// the currently used quotas from the db
quotaUsage: undefined,
// derived quota metrics for percentages used
usageMetrics: undefined,
// quota reset
quotaResetDaysRemaining: undefined,
quotaResetDate: undefined,
// failed payments
accountPastDue: undefined,
pastDueEndDate: undefined,
pastDueDaysRemaining: undefined,
accountDowngraded: undefined,
// user limits
userCount: undefined,
userLimit: undefined,
userLimitReached: false,
errUserLimit: false,
}
const oneDayInMilliseconds = 86400000
const store = writable(DEFAULT)
function usersLimitReached(userCount, userLimit) {
if (userLimit === UNLIMITED) {
return false
}
return userCount >= userLimit
}
function usersLimitExceeded(userCount, userLimit) {
if (userLimit === UNLIMITED) {
return false
}
return userCount > userLimit
}
async function isCloud() {
let adminStore = get(admin)
if (!adminStore.loaded) {
await admin.init()
adminStore = get(admin)
}
return adminStore.cloud
}
const actions = {
init: async () => {
actions.setNavigation()
actions.setLicense()
await actions.setQuotaUsage()
},
setNavigation: () => {
const adminStore = get(admin)
const authStore = get(auth)
const upgradeUrl = authStore?.user?.accountPortalAccess
? `${adminStore.accountPortalUrl}/portal/upgrade`
: "/builder/portal/account/upgrade"
const goToUpgradePage = () => {
window.location.href = upgradeUrl
}
const goToPricingPage = () => {
window.open("https://budibase.com/pricing/", "_blank")
}
store.update(state => {
return {
...state,
goToUpgradePage,
goToPricingPage,
}
})
},
setLicense: () => {
const license = get(auth).user.license
const planType = license?.plan.type
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
const isFreePlan = planType === Constants.PlanType.FREE
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
const isEnterpriseTrial =
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
const groupsEnabled = license.features.includes(
Constants.Features.USER_GROUPS
)
const backupsEnabled = license.features.includes(
Constants.Features.APP_BACKUPS
)
const scimEnabled = license.features.includes(Constants.Features.SCIM)
const environmentVariablesEnabled = license.features.includes(
Constants.Features.ENVIRONMENT_VARIABLES
)
const enforceableSSO = license.features.includes(
Constants.Features.ENFORCEABLE_SSO
)
const brandingEnabled = license.features.includes(
Constants.Features.BRANDING
)
const auditLogsEnabled = license.features.includes(
Constants.Features.AUDIT_LOGS
)
const syncAutomationsEnabled = license.features.includes(
Constants.Features.SYNC_AUTOMATIONS
)
const triggerAutomationRunEnabled = license.features.includes(
Constants.Features.TRIGGER_AUTOMATION_RUN
)
const perAppBuildersEnabled = license.features.includes(
Constants.Features.APP_BUILDERS
)
const budibaseAIEnabled = license.features.includes(
Constants.Features.BUDIBASE_AI
)
const customAIConfigsEnabled = license.features.includes(
Constants.Features.AI_CUSTOM_CONFIGS
)
store.update(state => {
return {
...state,
license,
isEnterprisePlan,
isFreePlan,
isBusinessPlan,
isEnterpriseTrial,
groupsEnabled,
backupsEnabled,
brandingEnabled,
budibaseAIEnabled,
customAIConfigsEnabled,
scimEnabled,
environmentVariablesEnabled,
auditLogsEnabled,
enforceableSSO,
syncAutomationsEnabled,
triggerAutomationRunEnabled,
perAppBuildersEnabled,
}
})
},
setQuotaUsage: async () => {
const quotaUsage = await API.getQuotaUsage()
store.update(state => {
return {
...state,
quotaUsage,
}
})
await actions.setUsageMetrics()
},
usersLimitReached: userCount => {
return usersLimitReached(userCount, get(store).userLimit)
},
usersLimitExceeded(userCount) {
return usersLimitExceeded(userCount, get(store).userLimit)
},
setUsageMetrics: async () => {
const usage = get(store).quotaUsage
const license = get(auth).user.license
const now = new Date()
const getMetrics = (keys, license, quota) => {
if (!license || !quota || !keys) {
return {}
}
return keys.reduce((acc, key) => {
const quotaLimit = license[key].value
const quotaUsed = (quota[key] / quotaLimit) * 100
acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1
return acc
}, {})
}
const monthlyMetrics = getMetrics(
["queries", "automations"],
license.quotas.usage.monthly,
usage.monthly.current
)
const staticMetrics = getMetrics(
["apps", "rows"],
license.quotas.usage.static,
usage.usageQuota
)
const getDaysBetween = (dateStart, dateEnd) => {
return dateEnd > dateStart
? Math.round(
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
)
: 0
}
const quotaResetDate = new Date(usage.quotaReset)
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
const accountDowngraded =
license?.billing?.subscription?.downgradeAt &&
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
license?.plan.type === Constants.PlanType.FREE
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
const downgradeAtMilliseconds =
license?.billing?.subscription?.downgradeAt
let pastDueDaysRemaining
let pastDueEndDate
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
pastDueEndDate = new Date(downgradeAtMilliseconds)
pastDueDaysRemaining = getDaysBetween(
new Date(pastDueAtMilliseconds),
pastDueEndDate
)
}
const userQuota = license.quotas.usage.static.users
const userLimit = userQuota?.value
const userCount = usage.usageQuota.users
const userLimitReached = usersLimitReached(userCount, userLimit)
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
const isCloudAccount = await isCloud()
const errUserLimit =
isCloudAccount &&
license.plan.model === PlanModel.PER_USER &&
userLimitExceeded
store.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
quotaResetDaysRemaining,
quotaResetDate,
accountDowngraded,
accountPastDue: pastDueAtMilliseconds != null,
pastDueEndDate,
pastDueDaysRemaining,
// user limits
userCount,
userLimit,
userLimitReached,
errUserLimit,
}
})
},
}
return {
subscribe: store.subscribe,
...actions,
}
}
export const licensing = createLicensingStore()

View File

@ -0,0 +1,305 @@
import { get } from "svelte/store"
import { API } from "@/api"
import { auth, admin } from "@/stores/portal"
import { Constants } from "@budibase/frontend-core"
import { StripeStatus } from "@/components/portal/licensing/constants"
import {
License,
MonthlyQuotaName,
PlanModel,
QuotaUsage,
StaticQuotaName,
} from "@budibase/types"
import { BudiStore } from "../BudiStore"
const UNLIMITED = -1
const ONE_DAY_MILLIS = 86400000
type MonthlyMetrics = { [key in MonthlyQuotaName]?: number }
type StaticMetrics = { [key in StaticQuotaName]?: number }
type UsageMetrics = MonthlyMetrics & StaticMetrics
interface LicensingState {
goToUpgradePage: () => void
goToPricingPage: () => void
// the top level license
license?: License
isFreePlan: boolean
isEnterprisePlan: boolean
isBusinessPlan: boolean
// features
groupsEnabled: boolean
backupsEnabled: boolean
brandingEnabled: boolean
scimEnabled: boolean
environmentVariablesEnabled: boolean
budibaseAIEnabled: boolean
customAIConfigsEnabled: boolean
auditLogsEnabled: boolean
// the currently used quotas from the db
quotaUsage?: QuotaUsage
// derived quota metrics for percentages used
usageMetrics?: UsageMetrics
// quota reset
quotaResetDaysRemaining?: number
quotaResetDate?: Date
// failed payments
accountPastDue: boolean
pastDueEndDate?: Date
pastDueDaysRemaining?: number
accountDowngraded: boolean
// user limits
userCount?: number
userLimit?: number
userLimitReached: boolean
errUserLimit: boolean
}
class LicensingStore extends BudiStore<LicensingState> {
constructor() {
super({
// navigation
goToUpgradePage: () => {},
goToPricingPage: () => {},
// the top level license
license: undefined,
isFreePlan: true,
isEnterprisePlan: true,
isBusinessPlan: true,
// features
groupsEnabled: false,
backupsEnabled: false,
brandingEnabled: false,
scimEnabled: false,
environmentVariablesEnabled: false,
budibaseAIEnabled: false,
customAIConfigsEnabled: false,
auditLogsEnabled: false,
// the currently used quotas from the db
quotaUsage: undefined,
// derived quota metrics for percentages used
usageMetrics: undefined,
// quota reset
quotaResetDaysRemaining: undefined,
quotaResetDate: undefined,
// failed payments
accountPastDue: false,
pastDueEndDate: undefined,
pastDueDaysRemaining: undefined,
accountDowngraded: false,
// user limits
userCount: undefined,
userLimit: undefined,
userLimitReached: false,
errUserLimit: false,
})
}
usersLimitReached(userCount: number, userLimit = get(this.store).userLimit) {
if (userLimit === UNLIMITED || userLimit === undefined) {
return false
}
return userCount >= userLimit
}
usersLimitExceeded(userCount: number, userLimit = get(this.store).userLimit) {
if (userLimit === UNLIMITED || userLimit === undefined) {
return false
}
return userCount > userLimit
}
async isCloud() {
let adminStore = get(admin)
if (!adminStore.loaded) {
await admin.init()
adminStore = get(admin)
}
return adminStore.cloud
}
async init() {
this.setNavigation()
this.setLicense()
await this.setQuotaUsage()
}
setNavigation() {
const adminStore = get(admin)
const authStore = get(auth)
const upgradeUrl = authStore?.user?.accountPortalAccess
? `${adminStore.accountPortalUrl}/portal/upgrade`
: "/builder/portal/account/upgrade"
const goToUpgradePage = () => {
window.location.href = upgradeUrl
}
const goToPricingPage = () => {
window.open("https://budibase.com/pricing/", "_blank")
}
this.update(state => {
return {
...state,
goToUpgradePage,
goToPricingPage,
}
})
}
setLicense() {
const license = get(auth).user?.license
const planType = license?.plan.type
const features = license?.features || []
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
const isFreePlan = planType === Constants.PlanType.FREE
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
const isEnterpriseTrial =
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
const groupsEnabled = features.includes(Constants.Features.USER_GROUPS)
const backupsEnabled = features.includes(Constants.Features.APP_BACKUPS)
const scimEnabled = features.includes(Constants.Features.SCIM)
const environmentVariablesEnabled = features.includes(
Constants.Features.ENVIRONMENT_VARIABLES
)
const enforceableSSO = features.includes(Constants.Features.ENFORCEABLE_SSO)
const brandingEnabled = features.includes(Constants.Features.BRANDING)
const auditLogsEnabled = features.includes(Constants.Features.AUDIT_LOGS)
const syncAutomationsEnabled = features.includes(
Constants.Features.SYNC_AUTOMATIONS
)
const triggerAutomationRunEnabled = features.includes(
Constants.Features.TRIGGER_AUTOMATION_RUN
)
const perAppBuildersEnabled = features.includes(
Constants.Features.APP_BUILDERS
)
const budibaseAIEnabled = features.includes(Constants.Features.BUDIBASE_AI)
const customAIConfigsEnabled = features.includes(
Constants.Features.AI_CUSTOM_CONFIGS
)
this.update(state => {
return {
...state,
license,
isEnterprisePlan,
isFreePlan,
isBusinessPlan,
isEnterpriseTrial,
groupsEnabled,
backupsEnabled,
brandingEnabled,
budibaseAIEnabled,
customAIConfigsEnabled,
scimEnabled,
environmentVariablesEnabled,
auditLogsEnabled,
enforceableSSO,
syncAutomationsEnabled,
triggerAutomationRunEnabled,
perAppBuildersEnabled,
}
})
}
async setQuotaUsage() {
const quotaUsage = await API.getQuotaUsage()
this.update(state => {
return {
...state,
quotaUsage,
}
})
await this.setUsageMetrics()
}
async setUsageMetrics() {
const usage = get(this.store).quotaUsage
const license = get(auth).user?.license
const now = new Date()
if (!license || !usage) {
return
}
// Process monthly metrics
const monthlyMetrics = [
MonthlyQuotaName.QUERIES,
MonthlyQuotaName.AUTOMATIONS,
].reduce((acc: MonthlyMetrics, key) => {
const limit = license.quotas.usage.monthly[key].value
const used = (usage.monthly.current?.[key] || 0 / limit) * 100
acc[key] = limit > -1 ? Math.floor(used) : -1
return acc
}, {})
// Process static metrics
const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
(acc: StaticMetrics, key) => {
const limit = license.quotas.usage.static[key].value
const used = (usage.usageQuota[key] || 0 / limit) * 100
acc[key] = limit > -1 ? Math.floor(used) : -1
return acc
},
{}
)
const getDaysBetween = (dateStart: Date, dateEnd: Date) => {
return dateEnd > dateStart
? Math.round((dateEnd.getTime() - dateStart.getTime()) / ONE_DAY_MILLIS)
: 0
}
const quotaResetDate = new Date(usage.quotaReset)
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
const accountDowngraded =
!!license.billing?.subscription?.downgradeAt &&
license.billing?.subscription?.downgradeAt <= now.getTime() &&
license.billing?.subscription?.status === StripeStatus.PAST_DUE &&
license.plan.type === Constants.PlanType.FREE
const pastDueAtMilliseconds = license.billing?.subscription?.pastDueAt
const downgradeAtMilliseconds = license.billing?.subscription?.downgradeAt
let pastDueDaysRemaining: number
let pastDueEndDate: Date
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
pastDueEndDate = new Date(downgradeAtMilliseconds)
pastDueDaysRemaining = getDaysBetween(
new Date(pastDueAtMilliseconds),
pastDueEndDate
)
}
const userQuota = license.quotas.usage.static.users
const userLimit = userQuota.value
const userCount = usage.usageQuota.users
const userLimitReached = this.usersLimitReached(userCount, userLimit)
const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
const isCloudAccount = await this.isCloud()
const errUserLimit =
isCloudAccount &&
license.plan.model === PlanModel.PER_USER &&
userLimitExceeded
this.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
quotaResetDaysRemaining,
quotaResetDate,
accountDowngraded,
accountPastDue: pastDueAtMilliseconds != null,
pastDueEndDate,
pastDueDaysRemaining,
// user limits
userCount,
userLimit,
userLimitReached,
errUserLimit,
}
})
}
}
export const licensing = new LicensingStore()

View File

@ -1,5 +1,6 @@
import { License } from "../../../sdk"
import { Account, DevInfo, User } from "../../../documents"
import { FeatureFlags } from "@budibase/types" import { FeatureFlags } from "@budibase/types"
import { DevInfo, User } from "../../../documents"
export interface GenerateAPIKeyRequest { export interface GenerateAPIKeyRequest {
userId?: string userId?: string
@ -10,4 +11,9 @@ export interface FetchAPIKeyResponse extends DevInfo {}
export interface GetGlobalSelfResponse extends User { export interface GetGlobalSelfResponse extends User {
flags?: FeatureFlags flags?: FeatureFlags
account?: Account
license: License
budibaseAccess: boolean
accountPortalAccess: boolean
csrfToken: boolean
} }

View File

@ -84,15 +84,15 @@ export async function fetchAPIKey(ctx: UserCtx<void, FetchAPIKeyResponse>) {
} }
/** /**
* Add the attributes that are session based to the current user. *
*/ */
const addSessionAttributesToUser = (ctx: any) => { const getUserSessionAttributes = (ctx: any) => ({
ctx.body.account = ctx.user.account account: ctx.user.account,
ctx.body.license = ctx.user.license license: ctx.user.license,
ctx.body.budibaseAccess = !!ctx.user.budibaseAccess budibaseAccess: !!ctx.user.budibaseAccess,
ctx.body.accountPortalAccess = !!ctx.user.accountPortalAccess accountPortalAccess: !!ctx.user.accountPortalAccess,
ctx.body.csrfToken = ctx.user.csrfToken csrfToken: ctx.user.csrfToken,
} })
export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) { export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
if (!ctx.user) { if (!ctx.user) {
@ -108,12 +108,19 @@ export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
// get the main body of the user // get the main body of the user
const user = await userSdk.db.getUser(userId) const user = await userSdk.db.getUser(userId)
ctx.body = await groups.enrichUserRolesFromGroups(user) const enrichedUser = await groups.enrichUserRolesFromGroups(user)
// add the attributes that are session based to the current user
const sessionAttributes = getUserSessionAttributes(ctx)
// add the feature flags for this tenant // add the feature flags for this tenant
ctx.body.flags = await features.flags.fetch() const flags = await features.flags.fetch()
addSessionAttributesToUser(ctx) ctx.body = {
...enrichedUser,
...sessionAttributes,
flags,
}
} }
export const syncAppFavourites = async (processedAppIds: string[]) => { export const syncAppFavourites = async (processedAppIds: string[]) => {