diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index d259d5b560..6237c23972 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -21,8 +21,9 @@ import { User, DatabaseQueryOpts, } from "@budibase/types" -import * as context from "../context" import { getGlobalDB } from "../context" +import * as context from "../context" +import { isCreator } from "./utils" type GetOpts = { cleanup?: boolean } @@ -286,6 +287,19 @@ export async function getUserCount() { return response.total_rows } +export async function getCreatorCount() { + let creators = 0 + async function iterate(startPage?: string) { + const page = await paginatedUsers({ bookmark: startPage }) + creators += page.data.filter(isCreator).length + if (page.hasNextPage) { + await iterate(page.nextPage) + } + } + await iterate() + return creators +} + // used to remove the builder/admin permissions, for processing the // user as an app user (they may have some specific role/group export function removePortalUserPermissions(user: User | ContextUser) { diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index af0e8e10c7..0ef4b77998 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -10,6 +10,7 @@ import { getAccountByTenantId } from "../accounts" // extract from shared-core to make easily accessible from backend-core export const isBuilder = sdk.users.isBuilder export const isAdmin = sdk.users.isAdmin +export const isCreator = sdk.users.isCreator export const isGlobalBuilder = sdk.users.isGlobalBuilder export const isAdminOrBuilder = sdk.users.isAdminOrBuilder export const hasAdminPermissions = sdk.users.hasAdminPermissions diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 5cce84edfd..0e34f2e9bb 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -72,6 +72,11 @@ export function quotas(): Quotas { value: 1, triggers: [], }, + creators: { + name: "Creators", + value: 1, + triggers: [], + }, userGroups: { name: "User Groups", value: 1, diff --git a/packages/backend-core/tests/core/utilities/structures/quotas.ts b/packages/backend-core/tests/core/utilities/structures/quotas.ts index e82117053f..8d0b05fe1e 100644 --- a/packages/backend-core/tests/core/utilities/structures/quotas.ts +++ b/packages/backend-core/tests/core/utilities/structures/quotas.ts @@ -1,6 +1,6 @@ import { MonthlyQuotaName, QuotaUsage } from "@budibase/types" -export const usage = (): QuotaUsage => { +export const usage = (users: number = 0, creators: number = 0): QuotaUsage => { return { _id: "usage_quota", quotaReset: new Date().toISOString(), @@ -58,7 +58,8 @@ export const usage = (): QuotaUsage => { usageQuota: { apps: 0, plugins: 0, - users: 0, + users, + creators, userGroups: 0, rows: 0, triggers: {}, diff --git a/packages/pro b/packages/pro index f7e7cffe42..d24c0dc3a3 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit f7e7cffe422086d9449c2075a74a378c16caff9d +Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376 diff --git a/packages/server/src/migrations/functions/syncQuotas.ts b/packages/server/src/migrations/functions/syncQuotas.ts index 67f38ba929..83a7670e78 100644 --- a/packages/server/src/migrations/functions/syncQuotas.ts +++ b/packages/server/src/migrations/functions/syncQuotas.ts @@ -3,6 +3,7 @@ import * as syncApps from "./usageQuotas/syncApps" import * as syncRows from "./usageQuotas/syncRows" import * as syncPlugins from "./usageQuotas/syncPlugins" import * as syncUsers from "./usageQuotas/syncUsers" +import * as syncCreators from "./usageQuotas/syncCreators" /** * Synchronise quotas to the state of the db. @@ -13,5 +14,6 @@ export const run = async () => { await syncRows.run() await syncPlugins.run() await syncUsers.run() + await syncCreators.run() }) } diff --git a/packages/server/src/migrations/functions/usageQuotas/syncCreators.ts b/packages/server/src/migrations/functions/usageQuotas/syncCreators.ts new file mode 100644 index 0000000000..ce53be925a --- /dev/null +++ b/packages/server/src/migrations/functions/usageQuotas/syncCreators.ts @@ -0,0 +1,13 @@ +import { users } from "@budibase/backend-core" +import { quotas } from "@budibase/pro" +import { QuotaUsageType, StaticQuotaName } from "@budibase/types" + +export const run = async () => { + const creatorCount = await users.getCreatorCount() + console.log(`Syncing creator count: ${creatorCount}`) + await quotas.setUsage( + creatorCount, + StaticQuotaName.CREATORS, + QuotaUsageType.STATIC + ) +} diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts new file mode 100644 index 0000000000..75fa9f217e --- /dev/null +++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts @@ -0,0 +1,26 @@ +import TestConfig from "../../../../tests/utilities/TestConfiguration" +import * as syncCreators from "../syncCreators" +import { quotas } from "@budibase/pro" + +describe("syncCreators", () => { + let config = new TestConfig(false) + + beforeEach(async () => { + await config.init() + }) + + afterAll(config.end) + + it("syncs creators", async () => { + return config.doInContext(null, async () => { + await config.createUser({ admin: true }) + + await syncCreators.run() + + const usageDoc = await quotas.getQuotaUsage() + // default + additional creator + const creatorsCount = 2 + expect(usageDoc.usageQuota.creators).toBe(creatorsCount) + }) + }) +}) diff --git a/packages/shared-core/src/sdk/documents/users.ts b/packages/shared-core/src/sdk/documents/users.ts index 03d86daa85..b58994aa46 100644 --- a/packages/shared-core/src/sdk/documents/users.ts +++ b/packages/shared-core/src/sdk/documents/users.ts @@ -6,6 +6,7 @@ import { InternalTable, } from "@budibase/types" import { getProdAppID } from "./applications" +import * as _ from "lodash/fp" // checks if a user is specifically a builder, given an app ID export function isBuilder(user: User | ContextUser, appId?: string): boolean { @@ -58,6 +59,18 @@ export function hasAppBuilderPermissions(user?: User | ContextUser): boolean { return !isGlobalBuilder && appLength != null && appLength > 0 } +export function hasAppCreatorPermissions(user?: User | ContextUser): boolean { + if (!user) { + return false + } + return _.flow( + _.get("roles"), + _.values, + _.find(x => ["CREATOR", "ADMIN"].includes(x)), + x => !!x + )(user) +} + // checks if a user is capable of building any app export function hasBuilderPermissions(user?: User | ContextUser): boolean { if (!user) { @@ -74,6 +87,18 @@ export function hasAdminPermissions(user?: User | ContextUser): boolean { return !!user.admin?.global } +export function isCreator(user?: User | ContextUser): boolean { + if (!user) { + return false + } + return ( + isGlobalBuilder(user) || + hasAdminPermissions(user) || + hasAppBuilderPermissions(user) || + hasAppCreatorPermissions(user) + ) +} + export function getGlobalUserID(userId?: string): string | undefined { if (typeof userId !== "string") { return userId diff --git a/packages/types/src/documents/global/quotas.ts b/packages/types/src/documents/global/quotas.ts index 61410f7435..4eb1168f7d 100644 --- a/packages/types/src/documents/global/quotas.ts +++ b/packages/types/src/documents/global/quotas.ts @@ -32,6 +32,7 @@ export interface StaticUsage { [StaticQuotaName.APPS]: number [StaticQuotaName.PLUGINS]: number [StaticQuotaName.USERS]: number + [StaticQuotaName.CREATORS]: number [StaticQuotaName.USER_GROUPS]: number [StaticQuotaName.ROWS]: number triggers: { diff --git a/packages/types/src/sdk/licensing/quota.ts b/packages/types/src/sdk/licensing/quota.ts index 73afa1ed05..85700f167b 100644 --- a/packages/types/src/sdk/licensing/quota.ts +++ b/packages/types/src/sdk/licensing/quota.ts @@ -14,6 +14,7 @@ export enum StaticQuotaName { ROWS = "rows", APPS = "apps", USERS = "users", + CREATORS = "creators", USER_GROUPS = "userGroups", PLUGINS = "plugins", } @@ -67,6 +68,7 @@ export type StaticQuotas = { [StaticQuotaName.ROWS]: Quota [StaticQuotaName.APPS]: Quota [StaticQuotaName.USERS]: Quota + [StaticQuotaName.CREATORS]: Quota [StaticQuotaName.USER_GROUPS]: Quota [StaticQuotaName.PLUGINS]: Quota }