Merge branch 'master' into chore/aws-v2-to-v3

This commit is contained in:
Michael Drury 2025-01-07 17:00:04 +00:00 committed by GitHub
commit 05a44cadeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 599 additions and 551 deletions

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.32",
"version": "3.2.33",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -236,13 +236,13 @@
}
if (!role) {
await groups.actions.removeApp(target._id, prodAppId)
await groups.removeApp(target._id, prodAppId)
} else {
await groups.actions.addApp(target._id, prodAppId, role)
await groups.addApp(target._id, prodAppId, role)
}
await usersFetch.refresh()
await groups.actions.init()
await groups.init()
}
const onUpdateGroup = async (group, role) => {
@ -268,7 +268,7 @@
if (!group.roles) {
return false
}
return groups.actions.getGroupAppIds(group).includes(appId)
return groups.getGroupAppIds(group).includes(appId)
})
}
@ -299,7 +299,7 @@
role: group?.builder?.apps.includes(prodAppId)
? Constants.Roles.CREATOR
: group.roles?.[
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
groups.getGroupAppIds(group).find(x => x === prodAppId)
],
}
}
@ -485,12 +485,12 @@
}
const removeGroupAppBuilder = async groupId => {
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
await groups.removeGroupAppBuilder(groupId, prodAppId)
}
const initSidePanel = async sidePaneOpen => {
if (sidePaneOpen === true) {
await groups.actions.init()
await groups.init()
}
loaded = true
}

View File

@ -53,7 +53,7 @@
}
if (!Object.keys(user?.roles).length && user?.userGroups) {
return userGroups.find(group => {
return groups.actions
return groups
.getGroupAppIds(group)
.map(role => appsStore.extractAppId(role))
.includes(app.appId)
@ -86,7 +86,7 @@
try {
await organisation.init()
await appsStore.load()
await groups.actions.init()
await groups.init()
} catch (error) {
notifications.error("Error loading apps")
}

View File

@ -24,7 +24,7 @@
promises.push(templates.load())
}
promises.push(groups.actions.init())
promises.push(groups.init())
// Always load latest
await Promise.all(promises)

View File

@ -53,9 +53,7 @@
$: readonly = !isAdmin || isScimGroup
$: groupApps = $appsStore.apps
.filter(app =>
groups.actions
.getGroupAppIds(group)
.includes(appsStore.getProdAppID(app.devId))
groups.getGroupAppIds(group).includes(appsStore.getProdAppID(app.devId))
)
.map(app => ({
...app,
@ -72,7 +70,7 @@
async function deleteGroup() {
try {
await groups.actions.delete(group)
await groups.delete(group)
notifications.success("User group deleted successfully")
$goto("./")
} catch (error) {
@ -82,7 +80,7 @@
async function saveGroup(group) {
try {
await groups.actions.save(group)
await groups.save(group)
} catch (error) {
if (error.message) {
notifications.error(error.message)
@ -93,7 +91,7 @@
}
const removeApp = async app => {
await groups.actions.removeApp(groupId, appsStore.getProdAppID(app.devId))
await groups.removeApp(groupId, appsStore.getProdAppID(app.devId))
}
setContext("roles", {
updateRole: () => {},
@ -102,7 +100,7 @@
onMount(async () => {
try {
await Promise.all([groups.actions.init(), roles.fetch()])
await Promise.all([groups.init(), roles.fetch()])
loaded = true
} catch (error) {
notifications.error("Error fetching user group data")

View File

@ -23,7 +23,7 @@
return keepOpen
} else {
await groups.actions.addApp(group._id, prodAppId, selectedRoleId)
await groups.addApp(group._id, prodAppId, selectedRoleId)
}
}
</script>

View File

@ -50,11 +50,11 @@
selected={group.users?.map(user => user._id)}
list={$users.data}
on:select={async e => {
await groups.actions.addUser(groupId, e.detail)
await groups.addUser(groupId, e.detail)
onUsersUpdated()
}}
on:deselect={async e => {
await groups.actions.removeUser(groupId, e.detail)
await groups.removeUser(groupId, e.detail)
onUsersUpdated()
}}
/>

View File

@ -60,7 +60,7 @@
async function saveGroup(group) {
try {
group = await groups.actions.save(group)
group = await groups.save(group)
$goto(`./${group._id}`)
notifications.success(`User group created successfully`)
} catch (error) {
@ -83,7 +83,7 @@
try {
// always load latest
await licensing.init()
await groups.actions.init()
await groups.init()
} catch (error) {
notifications.error("Error getting user groups")
}

View File

@ -87,6 +87,7 @@
let popover
let user, tenantOwner
let loaded = false
let userFieldsToUpdate = {}
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
@ -164,40 +165,45 @@
return label
}
async function updateUserFirstName(evt) {
async function saveUser() {
try {
await users.save({ ...user, firstName: evt.target.value })
await users.save({ ...user, ...userFieldsToUpdate })
userFieldsToUpdate = {}
await fetchUser()
} catch (error) {
notifications.error("Error updating user")
}
}
async function updateUserFirstName(evt) {
userFieldsToUpdate.firstName = evt.target.value
}
async function updateUserLastName(evt) {
try {
await users.save({ ...user, lastName: evt.target.value })
await fetchUser()
} catch (error) {
notifications.error("Error updating user")
}
userFieldsToUpdate.lastName = evt.target.value
}
async function updateUserRole({ detail }) {
let flags = {}
if (detail === Constants.BudibaseRoles.Developer) {
toggleFlags({ admin: { global: false }, builder: { global: true } })
flags = { admin: { global: false }, builder: { global: true } }
} else if (detail === Constants.BudibaseRoles.Admin) {
toggleFlags({ admin: { global: true }, builder: { global: true } })
flags = { admin: { global: true }, builder: { global: true } }
} else if (detail === Constants.BudibaseRoles.AppUser) {
toggleFlags({ admin: { global: false }, builder: { global: false } })
flags = { admin: { global: false }, builder: { global: false } }
} else if (detail === Constants.BudibaseRoles.Creator) {
toggleFlags({
flags = {
admin: { global: false },
builder: {
global: false,
creator: true,
apps: user?.builder?.apps || [],
},
})
}
}
userFieldsToUpdate = {
...userFieldsToUpdate,
...flags,
}
}
@ -209,22 +215,13 @@
tenantOwner = await users.getAccountHolder()
}
async function toggleFlags(detail) {
try {
await users.save({ ...user, ...detail })
await fetchUser()
} catch (error) {
notifications.error("Error updating user")
}
}
const addGroup = async groupId => {
await groups.actions.addUser(groupId, userId)
await groups.addUser(groupId, userId)
await fetchUser()
}
const removeGroup = async groupId => {
await groups.actions.removeUser(groupId, userId)
await groups.removeUser(groupId, userId)
await fetchUser()
}
@ -234,7 +231,7 @@
onMount(async () => {
try {
await Promise.all([fetchUser(), groups.actions.init(), roles.fetch()])
await Promise.all([fetchUser(), groups.init(), roles.fetch()])
loaded = true
} catch (error) {
notifications.error("Error getting user groups")
@ -296,7 +293,7 @@
<Input
disabled={readonly}
value={user?.firstName}
on:blur={updateUserFirstName}
on:input={updateUserFirstName}
/>
</div>
<div class="field">
@ -304,7 +301,7 @@
<Input
disabled={readonly}
value={user?.lastName}
on:blur={updateUserLastName}
on:input={updateUserLastName}
/>
</div>
<!-- don't let a user remove the privileges that let them be here -->
@ -325,6 +322,13 @@
{/if}
</div>
</Layout>
<div>
<Button
cta
disabled={Object.keys(userFieldsToUpdate).length === 0}
on:click={saveUser}>Save</Button
>
</div>
{#if $licensing.groupsEnabled}
<!-- User groups -->

View File

@ -247,7 +247,7 @@
try {
bulkSaveResponse = await users.create(await removingDuplicities(userData))
notifications.success("Successfully created user")
await groups.actions.init()
await groups.init()
passwordModal.show()
await fetch.refresh()
} catch (error) {
@ -317,7 +317,7 @@
onMount(async () => {
try {
await groups.actions.init()
await groups.init()
groupsLoaded = true
} catch (error) {
notifications.error("Error fetching user group data")

View File

@ -1,13 +1,16 @@
import { appStore } from "./app"
import { appsStore } from "@/stores/portal/apps"
import { deploymentStore } from "./deployments"
import { derived } from "svelte/store"
import { derived, type Readable } from "svelte/store"
import { DeploymentProgressResponse, DeploymentStatus } from "@budibase/types"
export const appPublished = derived(
export const appPublished: Readable<boolean> = derived(
[appStore, appsStore, deploymentStore],
([$appStore, $appsStore, $deploymentStore]) => {
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
const deployments = $deploymentStore.filter(x => x.status === "SUCCESS")
const deployments = $deploymentStore.filter(
(x: DeploymentProgressResponse) => x.status === DeploymentStatus.SUCCESS
)
return app?.status === "published" && deployments.length > 0
}
)

View File

@ -1,88 +0,0 @@
import { derived, writable, get } from "svelte/store"
import { API } from "@/api"
import { RoleUtils } from "@budibase/frontend-core"
export function createRolesStore() {
const store = writable([])
const enriched = derived(store, $store => {
return $store.map(role => ({
...role,
// Ensure we have new metadata for all roles
uiMetadata: {
displayName: role.uiMetadata?.displayName || role.name,
color:
role.uiMetadata?.color || "var(--spectrum-global-color-magenta-400)",
description: role.uiMetadata?.description || "Custom role",
},
}))
})
function setRoles(roles) {
store.set(
roles.sort((a, b) => {
const priorityA = RoleUtils.getRolePriority(a._id)
const priorityB = RoleUtils.getRolePriority(b._id)
if (priorityA !== priorityB) {
return priorityA > priorityB ? -1 : 1
}
const nameA = a.uiMetadata?.displayName || a.name
const nameB = b.uiMetadata?.displayName || b.name
return nameA < nameB ? -1 : 1
})
)
}
const actions = {
fetch: async () => {
const roles = await API.getRoles()
setRoles(roles)
},
fetchByAppId: async appId => {
const { roles } = await API.getRolesForApp(appId)
setRoles(roles)
},
delete: async role => {
await API.deleteRole(role._id, role._rev)
await actions.fetch()
},
save: async role => {
const savedRole = await API.saveRole(role)
await actions.fetch()
return savedRole
},
replace: (roleId, role) => {
// Handles external updates of roles
if (!roleId) {
return
}
// Handle deletion
if (!role) {
store.update(state => state.filter(x => x._id !== roleId))
return
}
// Add new role
const index = get(store).findIndex(x => x._id === role._id)
if (index === -1) {
store.update(state => [...state, role])
}
// Update existing role
else if (role) {
store.update(state => {
state[index] = role
return [...state]
})
}
},
}
return {
subscribe: enriched.subscribe,
...actions,
}
}
export const roles = createRolesStore()

View File

@ -0,0 +1,94 @@
import { derived, get, type Writable } from "svelte/store"
import { API } from "@/api"
import { RoleUtils } from "@budibase/frontend-core"
import { DerivedBudiStore } from "../BudiStore"
import { Role } from "@budibase/types"
export class RoleStore extends DerivedBudiStore<Role[], Role[]> {
constructor() {
const makeDerivedStore = (store: Writable<Role[]>) =>
derived(store, $store => {
return $store.map((role: Role) => ({
...role,
// Ensure we have new metadata for all roles
uiMetadata: {
displayName: role.uiMetadata?.displayName || role.name,
color:
role.uiMetadata?.color ||
"var(--spectrum-global-color-magenta-400)",
description: role.uiMetadata?.description || "Custom role",
},
}))
})
super([], makeDerivedStore)
}
setRoles = (roles: Role[]) => {
this.set(
roles.sort((a, b) => {
// Ensure we have valid IDs for priority comparison
const priorityA = RoleUtils.getRolePriority(a._id)
const priorityB = RoleUtils.getRolePriority(b._id)
if (priorityA !== priorityB) {
return priorityA > priorityB ? -1 : 1
}
const nameA = a.uiMetadata?.displayName || a.name
const nameB = b.uiMetadata?.displayName || b.name
return nameA < nameB ? -1 : 1
})
)
}
fetch = async () => {
const roles = await API.getRoles()
this.setRoles(roles)
}
fetchByAppId = async (appId: string) => {
const { roles } = await API.getRolesForApp(appId)
this.setRoles(roles)
}
delete = async (role: Role) => {
if (!role._id || !role._rev) {
return
}
await API.deleteRole(role._id, role._rev)
await this.fetch()
}
save = async (role: Role) => {
const savedRole = await API.saveRole(role)
await this.fetch()
return savedRole
}
replace = (roleId: string, role?: Role) => {
// Handles external updates of roles
if (!roleId) {
return
}
// Handle deletion
if (!role) {
this.update(state => state.filter(x => x._id !== roleId))
return
}
// Add new role
const index = get(this).findIndex(x => x._id === role._id)
if (index === -1) {
this.update(state => [...state, role])
}
// Update existing role
else if (role) {
this.update(state => {
state[index] = role
return [...state]
})
}
}
}
export const roles = new RoleStore()

View File

@ -1,103 +0,0 @@
import { writable, get } from "svelte/store"
import { API } from "@/api"
import { licensing } from "@/stores/portal"
export function createGroupsStore() {
const store = writable([])
const updateStore = group => {
store.update(state => {
const currentIdx = state.findIndex(gr => gr._id === group._id)
if (currentIdx >= 0) {
state.splice(currentIdx, 1, group)
} else {
state.push(group)
}
return state
})
}
const getGroup = async groupId => {
const group = await API.getGroup(groupId)
updateStore(group)
}
const actions = {
init: async () => {
// only init if there is a groups license, just to be sure but the feature will be blocked
// on the backend anyway
if (get(licensing).groupsEnabled) {
const groups = await API.getGroups()
store.set(groups.data)
}
},
get: getGroup,
save: async group => {
const { ...dataToSave } = group
delete dataToSave.scimInfo
delete dataToSave.userGroups
const response = await API.saveGroup(dataToSave)
group._id = response._id
group._rev = response._rev
updateStore(group)
return group
},
delete: async group => {
await API.deleteGroup(group._id, group._rev)
store.update(state => {
state = state.filter(state => state._id !== group._id)
return state
})
},
addUser: async (groupId, userId) => {
await API.addUsersToGroup(groupId, userId)
// refresh the group enrichment
await getGroup(groupId)
},
removeUser: async (groupId, userId) => {
await API.removeUsersFromGroup(groupId, userId)
// refresh the group enrichment
await getGroup(groupId)
},
addApp: async (groupId, appId, roleId) => {
await API.addAppsToGroup(groupId, [{ appId, roleId }])
// refresh the group roles
await getGroup(groupId)
},
removeApp: async (groupId, appId) => {
await API.removeAppsFromGroup(groupId, [{ appId }])
// refresh the group roles
await getGroup(groupId)
},
getGroupAppIds: group => {
let groupAppIds = Object.keys(group?.roles || {})
if (group?.builder?.apps) {
groupAppIds = groupAppIds.concat(group.builder.apps)
}
return groupAppIds
},
addGroupAppBuilder: async (groupId, appId) => {
return await API.addGroupAppBuilder(groupId, appId)
},
removeGroupAppBuilder: async (groupId, appId) => {
return await API.removeGroupAppBuilder(groupId, appId)
},
}
return {
subscribe: store.subscribe,
actions,
}
}
export const groups = createGroupsStore()

View File

@ -0,0 +1,96 @@
import { get } from "svelte/store"
import { API } from "@/api"
import { licensing } from "@/stores/portal"
import { UserGroup } from "@budibase/types"
import { BudiStore } from "../BudiStore"
class GroupStore extends BudiStore<UserGroup[]> {
constructor() {
super([])
}
updateStore = (group: UserGroup) => {
this.update(state => {
const currentIdx = state.findIndex(gr => gr._id === group._id)
if (currentIdx >= 0) {
state.splice(currentIdx, 1, group)
} else {
state.push(group)
}
return state
})
}
async init() {
// Only init if there is a groups license, just to be sure but the feature will be blocked
// on the backend anyway
if (get(licensing).groupsEnabled) {
const groups = await API.getGroups()
this.set(groups)
}
}
private async refreshGroup(groupId: string) {
const group = await API.getGroup(groupId)
this.updateStore(group)
}
async save(group: UserGroup) {
const { ...dataToSave } = group
delete dataToSave.scimInfo
const response = await API.saveGroup(dataToSave)
group._id = response._id
group._rev = response._rev
this.updateStore(group)
return group
}
async delete(group: UserGroup) {
await API.deleteGroup(group._id!, group._rev!)
this.update(groups => {
const index = groups.findIndex(g => g._id === group._id)
if (index !== -1) {
groups.splice(index, 1)
}
return groups
})
}
async addUser(groupId: string, userId: string) {
await API.addUsersToGroup(groupId, [userId])
await this.refreshGroup(groupId)
}
async removeUser(groupId: string, userId: string) {
await API.removeUsersFromGroup(groupId, [userId])
await this.refreshGroup(groupId)
}
async addApp(groupId: string, appId: string, roleId: string) {
await API.addAppsToGroup(groupId, [{ appId, roleId }])
await this.refreshGroup(groupId)
}
async removeApp(groupId: string, appId: string) {
await API.removeAppsFromGroup(groupId, [{ appId }])
await this.refreshGroup(groupId)
}
getGroupAppIds(group: UserGroup) {
let groupAppIds = Object.keys(group?.roles || {})
if (group?.builder?.apps) {
groupAppIds = groupAppIds.concat(group.builder.apps)
}
return groupAppIds
}
async addGroupAppBuilder(groupId: string, appId: string) {
return await API.addGroupAppBuilder(groupId, appId)
}
async removeGroupAppBuilder(groupId: string, appId: string) {
return await API.removeGroupAppBuilder(groupId, appId)
}
}
export const groups = new GroupStore()

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

@ -216,11 +216,11 @@ const deleteRowHandler = async action => {
const triggerAutomationHandler = async action => {
const { fields, notificationOverride, timeout } = action.parameters
try {
const result = await API.triggerAutomation({
automationId: action.parameters.automationId,
const result = await API.triggerAutomation(
action.parameters.automationId,
fields,
timeout,
})
timeout
)
// Value will exist if automation is synchronous, so return it.
if (result.value) {

View File

@ -1,4 +1,8 @@
import { SearchUserGroupResponse, UserGroup } from "@budibase/types"
import {
SearchGroupResponse,
SearchUserGroupResponse,
UserGroup,
} from "@budibase/types"
import { BaseAPIClient } from "./types"
export interface GroupEndpoints {
@ -64,9 +68,10 @@ export const buildGroupsEndpoints = (API: BaseAPIClient): GroupEndpoints => {
* Gets all the user groups
*/
getGroups: async () => {
return await API.get({
const res = await API.get<SearchGroupResponse>({
url: "/api/global/groups",
})
return res.data
},
/**

View File

@ -1,5 +1,6 @@
import { License } from "../../../sdk"
import { Account, DevInfo, User } from "../../../documents"
import { FeatureFlags } from "@budibase/types"
import { DevInfo, User } from "../../../documents"
export interface GenerateAPIKeyRequest {
userId?: string
@ -10,4 +11,9 @@ export interface FetchAPIKeyResponse extends DevInfo {}
export interface GetGlobalSelfResponse extends User {
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) => {
ctx.body.account = ctx.user.account
ctx.body.license = ctx.user.license
ctx.body.budibaseAccess = !!ctx.user.budibaseAccess
ctx.body.accountPortalAccess = !!ctx.user.accountPortalAccess
ctx.body.csrfToken = ctx.user.csrfToken
}
const getUserSessionAttributes = (ctx: any) => ({
account: ctx.user.account,
license: ctx.user.license,
budibaseAccess: !!ctx.user.budibaseAccess,
accountPortalAccess: !!ctx.user.accountPortalAccess,
csrfToken: ctx.user.csrfToken,
})
export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
if (!ctx.user) {
@ -108,12 +108,19 @@ export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
// get the main body of the user
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
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[]) => {

View File

@ -72,12 +72,14 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
const requestUser = ctx.request.body
// Do not allow the account holder role to be changed
const accountMetadata = await users.getExistingAccounts([requestUser.email])
if (accountMetadata?.length > 0) {
if (
requestUser.admin?.global !== true ||
requestUser.builder?.global !== true
) {
const accountMetadata = await users.getExistingAccounts([
requestUser.email,
])
if (accountMetadata?.length > 0) {
throw Error("Cannot set role of account holder")
}
}
@ -441,7 +443,6 @@ export const checkInvite = async (ctx: UserCtx<void, CheckInviteResponse>) => {
} catch (e) {
console.warn("Error getting invite from code", e)
ctx.throw(400, "There was a problem with the invite")
return
}
ctx.body = {
email: invite.email,
@ -472,7 +473,6 @@ export const updateInvite = async (
invite = await cache.invite.getCode(code)
} catch (e) {
ctx.throw(400, "There was a problem with the invite")
return
}
let updated = {