Merge branch 'master' into chore/aws-v2-to-v3
This commit is contained in:
commit
05a44cadeb
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.2.32",
|
"version": "3.2.33",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -236,13 +236,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!role) {
|
if (!role) {
|
||||||
await groups.actions.removeApp(target._id, prodAppId)
|
await groups.removeApp(target._id, prodAppId)
|
||||||
} else {
|
} else {
|
||||||
await groups.actions.addApp(target._id, prodAppId, role)
|
await groups.addApp(target._id, prodAppId, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
await usersFetch.refresh()
|
await usersFetch.refresh()
|
||||||
await groups.actions.init()
|
await groups.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateGroup = async (group, role) => {
|
const onUpdateGroup = async (group, role) => {
|
||||||
|
@ -268,7 +268,7 @@
|
||||||
if (!group.roles) {
|
if (!group.roles) {
|
||||||
return false
|
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)
|
role: group?.builder?.apps.includes(prodAppId)
|
||||||
? Constants.Roles.CREATOR
|
? Constants.Roles.CREATOR
|
||||||
: group.roles?.[
|
: 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 => {
|
const removeGroupAppBuilder = async groupId => {
|
||||||
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
await groups.removeGroupAppBuilder(groupId, prodAppId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const initSidePanel = async sidePaneOpen => {
|
const initSidePanel = async sidePaneOpen => {
|
||||||
if (sidePaneOpen === true) {
|
if (sidePaneOpen === true) {
|
||||||
await groups.actions.init()
|
await groups.init()
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
}
|
}
|
||||||
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
||||||
return userGroups.find(group => {
|
return userGroups.find(group => {
|
||||||
return groups.actions
|
return groups
|
||||||
.getGroupAppIds(group)
|
.getGroupAppIds(group)
|
||||||
.map(role => appsStore.extractAppId(role))
|
.map(role => appsStore.extractAppId(role))
|
||||||
.includes(app.appId)
|
.includes(app.appId)
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
try {
|
try {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
await appsStore.load()
|
await appsStore.load()
|
||||||
await groups.actions.init()
|
await groups.init()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error loading apps")
|
notifications.error("Error loading apps")
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
promises.push(templates.load())
|
promises.push(templates.load())
|
||||||
}
|
}
|
||||||
|
|
||||||
promises.push(groups.actions.init())
|
promises.push(groups.init())
|
||||||
|
|
||||||
// Always load latest
|
// Always load latest
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
|
|
@ -53,9 +53,7 @@
|
||||||
$: readonly = !isAdmin || isScimGroup
|
$: readonly = !isAdmin || isScimGroup
|
||||||
$: groupApps = $appsStore.apps
|
$: groupApps = $appsStore.apps
|
||||||
.filter(app =>
|
.filter(app =>
|
||||||
groups.actions
|
groups.getGroupAppIds(group).includes(appsStore.getProdAppID(app.devId))
|
||||||
.getGroupAppIds(group)
|
|
||||||
.includes(appsStore.getProdAppID(app.devId))
|
|
||||||
)
|
)
|
||||||
.map(app => ({
|
.map(app => ({
|
||||||
...app,
|
...app,
|
||||||
|
@ -72,7 +70,7 @@
|
||||||
|
|
||||||
async function deleteGroup() {
|
async function deleteGroup() {
|
||||||
try {
|
try {
|
||||||
await groups.actions.delete(group)
|
await groups.delete(group)
|
||||||
notifications.success("User group deleted successfully")
|
notifications.success("User group deleted successfully")
|
||||||
$goto("./")
|
$goto("./")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -82,7 +80,7 @@
|
||||||
|
|
||||||
async function saveGroup(group) {
|
async function saveGroup(group) {
|
||||||
try {
|
try {
|
||||||
await groups.actions.save(group)
|
await groups.save(group)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
notifications.error(error.message)
|
notifications.error(error.message)
|
||||||
|
@ -93,7 +91,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeApp = async app => {
|
const removeApp = async app => {
|
||||||
await groups.actions.removeApp(groupId, appsStore.getProdAppID(app.devId))
|
await groups.removeApp(groupId, appsStore.getProdAppID(app.devId))
|
||||||
}
|
}
|
||||||
setContext("roles", {
|
setContext("roles", {
|
||||||
updateRole: () => {},
|
updateRole: () => {},
|
||||||
|
@ -102,7 +100,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([groups.actions.init(), roles.fetch()])
|
await Promise.all([groups.init(), roles.fetch()])
|
||||||
loaded = true
|
loaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching user group data")
|
notifications.error("Error fetching user group data")
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
return keepOpen
|
return keepOpen
|
||||||
} else {
|
} else {
|
||||||
await groups.actions.addApp(group._id, prodAppId, selectedRoleId)
|
await groups.addApp(group._id, prodAppId, selectedRoleId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -50,11 +50,11 @@
|
||||||
selected={group.users?.map(user => user._id)}
|
selected={group.users?.map(user => user._id)}
|
||||||
list={$users.data}
|
list={$users.data}
|
||||||
on:select={async e => {
|
on:select={async e => {
|
||||||
await groups.actions.addUser(groupId, e.detail)
|
await groups.addUser(groupId, e.detail)
|
||||||
onUsersUpdated()
|
onUsersUpdated()
|
||||||
}}
|
}}
|
||||||
on:deselect={async e => {
|
on:deselect={async e => {
|
||||||
await groups.actions.removeUser(groupId, e.detail)
|
await groups.removeUser(groupId, e.detail)
|
||||||
onUsersUpdated()
|
onUsersUpdated()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
async function saveGroup(group) {
|
async function saveGroup(group) {
|
||||||
try {
|
try {
|
||||||
group = await groups.actions.save(group)
|
group = await groups.save(group)
|
||||||
$goto(`./${group._id}`)
|
$goto(`./${group._id}`)
|
||||||
notifications.success(`User group created successfully`)
|
notifications.success(`User group created successfully`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
try {
|
try {
|
||||||
// always load latest
|
// always load latest
|
||||||
await licensing.init()
|
await licensing.init()
|
||||||
await groups.actions.init()
|
await groups.init()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user groups")
|
notifications.error("Error getting user groups")
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
let popover
|
let popover
|
||||||
let user, tenantOwner
|
let user, tenantOwner
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let userFieldsToUpdate = {}
|
||||||
|
|
||||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||||
|
|
||||||
|
@ -164,40 +165,45 @@
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserFirstName(evt) {
|
async function saveUser() {
|
||||||
try {
|
try {
|
||||||
await users.save({ ...user, firstName: evt.target.value })
|
await users.save({ ...user, ...userFieldsToUpdate })
|
||||||
|
userFieldsToUpdate = {}
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error updating user")
|
notifications.error("Error updating user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateUserFirstName(evt) {
|
||||||
|
userFieldsToUpdate.firstName = evt.target.value
|
||||||
|
}
|
||||||
|
|
||||||
async function updateUserLastName(evt) {
|
async function updateUserLastName(evt) {
|
||||||
try {
|
userFieldsToUpdate.lastName = evt.target.value
|
||||||
await users.save({ ...user, lastName: evt.target.value })
|
|
||||||
await fetchUser()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating user")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserRole({ detail }) {
|
async function updateUserRole({ detail }) {
|
||||||
|
let flags = {}
|
||||||
if (detail === Constants.BudibaseRoles.Developer) {
|
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) {
|
} 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) {
|
} 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) {
|
} else if (detail === Constants.BudibaseRoles.Creator) {
|
||||||
toggleFlags({
|
flags = {
|
||||||
admin: { global: false },
|
admin: { global: false },
|
||||||
builder: {
|
builder: {
|
||||||
global: false,
|
global: false,
|
||||||
creator: true,
|
creator: true,
|
||||||
apps: user?.builder?.apps || [],
|
apps: user?.builder?.apps || [],
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
userFieldsToUpdate = {
|
||||||
|
...userFieldsToUpdate,
|
||||||
|
...flags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,22 +215,13 @@
|
||||||
tenantOwner = await users.getAccountHolder()
|
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 => {
|
const addGroup = async groupId => {
|
||||||
await groups.actions.addUser(groupId, userId)
|
await groups.addUser(groupId, userId)
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeGroup = async groupId => {
|
const removeGroup = async groupId => {
|
||||||
await groups.actions.removeUser(groupId, userId)
|
await groups.removeUser(groupId, userId)
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,7 +231,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([fetchUser(), groups.actions.init(), roles.fetch()])
|
await Promise.all([fetchUser(), groups.init(), roles.fetch()])
|
||||||
loaded = true
|
loaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user groups")
|
notifications.error("Error getting user groups")
|
||||||
|
@ -296,7 +293,7 @@
|
||||||
<Input
|
<Input
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={user?.firstName}
|
value={user?.firstName}
|
||||||
on:blur={updateUserFirstName}
|
on:input={updateUserFirstName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -304,7 +301,7 @@
|
||||||
<Input
|
<Input
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={user?.lastName}
|
value={user?.lastName}
|
||||||
on:blur={updateUserLastName}
|
on:input={updateUserLastName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- don't let a user remove the privileges that let them be here -->
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
|
@ -325,6 +322,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={Object.keys(userFieldsToUpdate).length === 0}
|
||||||
|
on:click={saveUser}>Save</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if $licensing.groupsEnabled}
|
{#if $licensing.groupsEnabled}
|
||||||
<!-- User groups -->
|
<!-- User groups -->
|
||||||
|
|
|
@ -247,7 +247,7 @@
|
||||||
try {
|
try {
|
||||||
bulkSaveResponse = await users.create(await removingDuplicities(userData))
|
bulkSaveResponse = await users.create(await removingDuplicities(userData))
|
||||||
notifications.success("Successfully created user")
|
notifications.success("Successfully created user")
|
||||||
await groups.actions.init()
|
await groups.init()
|
||||||
passwordModal.show()
|
passwordModal.show()
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -317,7 +317,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.init()
|
||||||
groupsLoaded = true
|
groupsLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching user group data")
|
notifications.error("Error fetching user group data")
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
import { appsStore } from "@/stores/portal/apps"
|
import { appsStore } from "@/stores/portal/apps"
|
||||||
import { deploymentStore } from "./deployments"
|
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],
|
||||||
([$appStore, $appsStore, $deploymentStore]) => {
|
([$appStore, $appsStore, $deploymentStore]) => {
|
||||||
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
|
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
|
return app?.status === "published" && deployments.length > 0
|
||||||
}
|
}
|
||||||
)
|
)
|
|
@ -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()
|
|
|
@ -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()
|
|
@ -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()
|
|
|
@ -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()
|
|
@ -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()
|
|
|
@ -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()
|
|
@ -216,11 +216,11 @@ const deleteRowHandler = async action => {
|
||||||
const triggerAutomationHandler = async action => {
|
const triggerAutomationHandler = async action => {
|
||||||
const { fields, notificationOverride, timeout } = action.parameters
|
const { fields, notificationOverride, timeout } = action.parameters
|
||||||
try {
|
try {
|
||||||
const result = await API.triggerAutomation({
|
const result = await API.triggerAutomation(
|
||||||
automationId: action.parameters.automationId,
|
action.parameters.automationId,
|
||||||
fields,
|
fields,
|
||||||
timeout,
|
timeout
|
||||||
})
|
)
|
||||||
|
|
||||||
// Value will exist if automation is synchronous, so return it.
|
// Value will exist if automation is synchronous, so return it.
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { SearchUserGroupResponse, UserGroup } from "@budibase/types"
|
import {
|
||||||
|
SearchGroupResponse,
|
||||||
|
SearchUserGroupResponse,
|
||||||
|
UserGroup,
|
||||||
|
} from "@budibase/types"
|
||||||
import { BaseAPIClient } from "./types"
|
import { BaseAPIClient } from "./types"
|
||||||
|
|
||||||
export interface GroupEndpoints {
|
export interface GroupEndpoints {
|
||||||
|
@ -64,9 +68,10 @@ export const buildGroupsEndpoints = (API: BaseAPIClient): GroupEndpoints => {
|
||||||
* Gets all the user groups
|
* Gets all the user groups
|
||||||
*/
|
*/
|
||||||
getGroups: async () => {
|
getGroups: async () => {
|
||||||
return await API.get({
|
const res = await API.get<SearchGroupResponse>({
|
||||||
url: "/api/global/groups",
|
url: "/api/global/groups",
|
||||||
})
|
})
|
||||||
|
return res.data
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]) => {
|
||||||
|
|
|
@ -72,12 +72,14 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
||||||
const requestUser = ctx.request.body
|
const requestUser = ctx.request.body
|
||||||
|
|
||||||
// Do not allow the account holder role to be changed
|
// Do not allow the account holder role to be changed
|
||||||
const accountMetadata = await users.getExistingAccounts([requestUser.email])
|
if (
|
||||||
if (accountMetadata?.length > 0) {
|
requestUser.admin?.global !== true ||
|
||||||
if (
|
requestUser.builder?.global !== true
|
||||||
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")
|
throw Error("Cannot set role of account holder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -441,7 +443,6 @@ export const checkInvite = async (ctx: UserCtx<void, CheckInviteResponse>) => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Error getting invite from code", e)
|
console.warn("Error getting invite from code", e)
|
||||||
ctx.throw(400, "There was a problem with the invite")
|
ctx.throw(400, "There was a problem with the invite")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
|
@ -472,7 +473,6 @@ export const updateInvite = async (
|
||||||
invite = await cache.invite.getCode(code)
|
invite = await cache.invite.getCode(code)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.throw(400, "There was a problem with the invite")
|
ctx.throw(400, "There was a problem with the invite")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated = {
|
let updated = {
|
||||||
|
|
Loading…
Reference in New Issue