Merge pull request #12443 from Budibase/creator-changes
Creator changes
This commit is contained in:
commit
a7c8097fbf
33 changed files with 356 additions and 249 deletions
|
@ -160,4 +160,5 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) {
|
||||||
|
|
||||||
// utility as a lot of things need simply the builder permission
|
// utility as a lot of things need simply the builder permission
|
||||||
export const BUILDER = PermissionType.BUILDER
|
export const BUILDER = PermissionType.BUILDER
|
||||||
|
export const CREATOR = PermissionType.CREATOR
|
||||||
export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER
|
export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER
|
||||||
|
|
|
@ -146,12 +146,12 @@ export class UserDB {
|
||||||
|
|
||||||
static async allUsers() {
|
static async allUsers() {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs<User>(
|
||||||
dbUtils.getGlobalUserParams(null, {
|
dbUtils.getGlobalUserParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return response.rows.map((row: any) => row.doc)
|
return response.rows.map(row => row.doc!)
|
||||||
}
|
}
|
||||||
|
|
||||||
static async countUsersByApp(appId: string) {
|
static async countUsersByApp(appId: string) {
|
||||||
|
@ -209,13 +209,6 @@ export class UserDB {
|
||||||
throw new Error("_id or email is required")
|
throw new Error("_id or email is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
user.builder?.apps?.length &&
|
|
||||||
!(await UserDB.features.isAppBuildersEnabled())
|
|
||||||
) {
|
|
||||||
throw new Error("Unable to update app builders, please check license")
|
|
||||||
}
|
|
||||||
|
|
||||||
let dbUser: User | undefined
|
let dbUser: User | undefined
|
||||||
if (_id) {
|
if (_id) {
|
||||||
// try to get existing user from db
|
// try to get existing user from db
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { isCreator } from "./utils"
|
import { isCreator } from "./utils"
|
||||||
|
import { UserDB } from "./db"
|
||||||
|
|
||||||
type GetOpts = { cleanup?: boolean }
|
type GetOpts = { cleanup?: boolean }
|
||||||
|
|
||||||
|
@ -336,3 +337,20 @@ export function cleanseUserObject(user: User | ContextUser, base?: User) {
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addAppBuilder(user: User, appId: string) {
|
||||||
|
const prodAppId = getProdAppID(appId)
|
||||||
|
user.builder ??= {}
|
||||||
|
user.builder.creator = true
|
||||||
|
user.builder.apps ??= []
|
||||||
|
user.builder.apps.push(prodAppId)
|
||||||
|
await UserDB.save(user, { hashPassword: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeAppBuilder(user: User, appId: string) {
|
||||||
|
const prodAppId = getProdAppID(appId)
|
||||||
|
if (user.builder && user.builder.apps?.includes(prodAppId)) {
|
||||||
|
user.builder.apps = user.builder.apps.filter(id => id !== prodAppId)
|
||||||
|
}
|
||||||
|
await UserDB.save(user, { hashPassword: false })
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import "@spectrum-css/buttongroup/dist/index-vars.css"
|
import "@spectrum-css/buttongroup/dist/index-vars.css"
|
||||||
|
|
||||||
export let vertical = false
|
export let vertical = false
|
||||||
export let gap = ""
|
export let gap = "M"
|
||||||
|
|
||||||
$: gapStyle =
|
$: gapStyle =
|
||||||
gap === "L"
|
gap === "L"
|
||||||
|
|
|
@ -12,11 +12,13 @@
|
||||||
export let error = null
|
export let error = null
|
||||||
export let validate = null
|
export let validate = null
|
||||||
export let options = []
|
export let options = []
|
||||||
|
export let footer = null
|
||||||
export let isOptionEnabled = () => true
|
export let isOptionEnabled = () => true
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
export let getOptionSubtitle = option => extractProperty(option, "subtitle")
|
export let getOptionSubtitle = option => extractProperty(option, "subtitle")
|
||||||
export let getOptionColour = () => null
|
export let getOptionColour = () => null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
|
@ -100,6 +102,7 @@
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
{options}
|
{options}
|
||||||
|
{footer}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionSubtitle}
|
{getOptionSubtitle}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
export let options = []
|
export let options = []
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
|
export let getOptionSubtitle = option => option?.subtitle
|
||||||
export let isOptionSelected = () => false
|
export let isOptionSelected = () => false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 30%">
|
<div style="width: 40%">
|
||||||
<button
|
<button
|
||||||
{id}
|
{id}
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
|
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
|
||||||
|
@ -157,38 +157,43 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if open}
|
|
||||||
<div
|
|
||||||
use:clickOutside={handleOutsideClick}
|
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
|
||||||
>
|
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
|
||||||
{#each options as option, idx}
|
|
||||||
<li
|
|
||||||
class="spectrum-Menu-item"
|
|
||||||
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
|
||||||
role="option"
|
|
||||||
aria-selected="true"
|
|
||||||
tabindex="0"
|
|
||||||
on:click={() => onPick(getOptionValue(option, idx))}
|
|
||||||
>
|
|
||||||
<span class="spectrum-Menu-itemLabel">
|
|
||||||
{getOptionLabel(option, idx)}
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
|
||||||
</svg>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
use:clickOutside={handleOutsideClick}
|
||||||
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
>
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#each options as option, idx}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item"
|
||||||
|
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() => onPick(getOptionValue(option, idx))}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
{getOptionLabel(option, idx)}
|
||||||
|
{#if getOptionSubtitle(option, idx)}
|
||||||
|
<span class="subtitle-text">
|
||||||
|
{getOptionSubtitle(option, idx)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -196,7 +201,6 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-InputGroup-input {
|
.spectrum-InputGroup-input {
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
}
|
}
|
||||||
|
@ -206,7 +210,6 @@
|
||||||
.spectrum-Textfield-input {
|
.spectrum-Textfield-input {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.override-borders {
|
.override-borders {
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
|
@ -215,5 +218,18 @@
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.subtitle-text {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
display: block;
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.spectrum-Menu-checkmark {
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -224,13 +224,12 @@
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Menu-itemLabel">
|
<span class="spectrum-Menu-itemLabel">
|
||||||
{#if getOptionSubtitle(option, idx)}
|
|
||||||
<span class="subtitle-text"
|
|
||||||
>{getOptionSubtitle(option, idx)}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{getOptionLabel(option, idx)}
|
{getOptionLabel(option, idx)}
|
||||||
|
{#if getOptionSubtitle(option, idx)}
|
||||||
|
<span class="subtitle-text">
|
||||||
|
{getOptionSubtitle(option, idx)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if option.tag}
|
{#if option.tag}
|
||||||
<span class="option-tag">
|
<span class="option-tag">
|
||||||
|
@ -275,10 +274,9 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
top: 10px;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Picker-label.auto-width {
|
.spectrum-Picker-label.auto-width {
|
||||||
|
|
|
@ -10,8 +10,9 @@
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let getOptionIcon = () => null
|
export let getOptionIcon = () => null
|
||||||
export let useOptionIconImage = false
|
|
||||||
export let getOptionColour = () => null
|
export let getOptionColour = () => null
|
||||||
|
export let getOptionSubtitle = () => null
|
||||||
|
export let useOptionIconImage = false
|
||||||
export let isOptionEnabled
|
export let isOptionEnabled
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
@ -82,8 +83,9 @@
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{useOptionIconImage}
|
|
||||||
{getOptionColour}
|
{getOptionColour}
|
||||||
|
{getOptionSubtitle}
|
||||||
|
{useOptionIconImage}
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
{quiet}
|
{quiet}
|
||||||
{autofocus}
|
{autofocus}
|
||||||
{options}
|
{options}
|
||||||
|
isOptionSelected={option => option === dropdownValue}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:pick={onPick}
|
on:pick={onPick}
|
||||||
on:click
|
on:click
|
||||||
|
|
|
@ -13,9 +13,10 @@
|
||||||
export let options = []
|
export let options = []
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
|
export let getOptionSubtitle = option => option?.subtitle
|
||||||
export let getOptionIcon = option => option?.icon
|
export let getOptionIcon = option => option?.icon
|
||||||
export let useOptionIconImage = false
|
|
||||||
export let getOptionColour = option => option?.colour
|
export let getOptionColour = option => option?.colour
|
||||||
|
export let useOptionIconImage = false
|
||||||
export let isOptionEnabled
|
export let isOptionEnabled
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{getOptionColour}
|
{getOptionColour}
|
||||||
|
{getOptionSubtitle}
|
||||||
{useOptionIconImage}
|
{useOptionIconImage}
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
|
|
@ -20,73 +20,91 @@
|
||||||
export let allowedRoles = null
|
export let allowedRoles = null
|
||||||
export let allowCreator = false
|
export let allowCreator = false
|
||||||
export let fancySelect = false
|
export let fancySelect = false
|
||||||
|
export let labelPrefix = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const RemoveID = "remove"
|
const RemoveID = "remove"
|
||||||
|
|
||||||
|
$: enrichLabel = label => (labelPrefix ? `${labelPrefix} ${label}` : label)
|
||||||
$: options = getOptions(
|
$: options = getOptions(
|
||||||
$roles,
|
$roles,
|
||||||
allowPublic,
|
allowPublic,
|
||||||
allowRemove,
|
allowRemove,
|
||||||
allowedRoles,
|
allowedRoles,
|
||||||
allowCreator
|
allowCreator,
|
||||||
|
enrichLabel
|
||||||
)
|
)
|
||||||
|
|
||||||
const getOptions = (
|
const getOptions = (
|
||||||
roles,
|
roles,
|
||||||
allowPublic,
|
allowPublic,
|
||||||
allowRemove,
|
allowRemove,
|
||||||
allowedRoles,
|
allowedRoles,
|
||||||
allowCreator
|
allowCreator,
|
||||||
|
enrichLabel
|
||||||
) => {
|
) => {
|
||||||
|
// Use roles whitelist if specified
|
||||||
if (allowedRoles?.length) {
|
if (allowedRoles?.length) {
|
||||||
const filteredRoles = roles.filter(role =>
|
let options = roles
|
||||||
allowedRoles.includes(role._id)
|
.filter(role => allowedRoles.includes(role._id))
|
||||||
)
|
.map(role => ({
|
||||||
return [
|
name: enrichLabel(role.name),
|
||||||
...filteredRoles,
|
_id: role._id,
|
||||||
...(allowedRoles.includes(Constants.Roles.CREATOR)
|
}))
|
||||||
? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }]
|
if (allowedRoles.includes(Constants.Roles.CREATOR)) {
|
||||||
: []),
|
options.push({
|
||||||
]
|
|
||||||
}
|
|
||||||
let newRoles = [...roles]
|
|
||||||
|
|
||||||
if (allowCreator) {
|
|
||||||
newRoles = [
|
|
||||||
{
|
|
||||||
_id: Constants.Roles.CREATOR,
|
_id: Constants.Roles.CREATOR,
|
||||||
name: "Creator",
|
name: "Can edit",
|
||||||
tag:
|
enabled: false,
|
||||||
!$licensing.perAppBuildersEnabled &&
|
})
|
||||||
capitalise(Constants.PlanType.BUSINESS),
|
}
|
||||||
},
|
return options
|
||||||
...newRoles,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow all core roles
|
||||||
|
let options = roles.map(role => ({
|
||||||
|
name: enrichLabel(role.name),
|
||||||
|
_id: role._id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Add creator if required
|
||||||
|
if (allowCreator) {
|
||||||
|
options.unshift({
|
||||||
|
_id: Constants.Roles.CREATOR,
|
||||||
|
name: "Can edit",
|
||||||
|
tag:
|
||||||
|
!$licensing.perAppBuildersEnabled &&
|
||||||
|
capitalise(Constants.PlanType.BUSINESS),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remove option if required
|
||||||
if (allowRemove) {
|
if (allowRemove) {
|
||||||
newRoles = [
|
options.push({
|
||||||
...newRoles,
|
_id: RemoveID,
|
||||||
{
|
name: "Remove",
|
||||||
_id: RemoveID,
|
})
|
||||||
name: "Remove",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
if (allowPublic) {
|
|
||||||
return newRoles
|
// Remove public if not allowed
|
||||||
|
if (!allowPublic) {
|
||||||
|
options = options.filter(role => role._id !== Constants.Roles.PUBLIC)
|
||||||
}
|
}
|
||||||
return newRoles.filter(role => role._id !== Constants.Roles.PUBLIC)
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
const getColor = role => {
|
const getColor = role => {
|
||||||
if (allowRemove && role._id === RemoveID) {
|
// Creator and remove options have no colors
|
||||||
|
if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return RoleUtils.getRoleColour(role._id)
|
return RoleUtils.getRoleColour(role._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIcon = role => {
|
const getIcon = role => {
|
||||||
if (allowRemove && role._id === RemoveID) {
|
// Only remove option has an icon
|
||||||
|
if (role._id === RemoveID) {
|
||||||
return "Close"
|
return "Close"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -364,7 +364,10 @@
|
||||||
const payload = [
|
const payload = [
|
||||||
{
|
{
|
||||||
email: newUserEmail,
|
email: newUserEmail,
|
||||||
builder: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
builder: {
|
||||||
|
global: creationRoleType === Constants.BudibaseRoles.Admin,
|
||||||
|
creator: creationRoleType === Constants.BudibaseRoles.Creator,
|
||||||
|
},
|
||||||
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -471,10 +474,6 @@
|
||||||
await users.removeAppBuilder(userId, prodAppId)
|
await users.removeAppBuilder(userId, prodAppId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addGroupAppBuilder = async groupId => {
|
|
||||||
await groups.actions.addGroupAppBuilder(groupId, prodAppId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeGroupAppBuilder = async groupId => {
|
const removeGroupAppBuilder = async groupId => {
|
||||||
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
||||||
}
|
}
|
||||||
|
@ -495,14 +494,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInviteRoleValue = invite => {
|
const getInviteRoleValue = invite => {
|
||||||
if (invite.info?.admin?.global && invite.info?.builder?.global) {
|
if (
|
||||||
return Constants.Roles.ADMIN
|
(invite.info?.admin?.global && invite.info?.builder?.global) ||
|
||||||
}
|
invite.info?.builder?.apps?.includes(prodAppId)
|
||||||
|
) {
|
||||||
if (invite.info?.builder?.apps?.includes(prodAppId)) {
|
|
||||||
return Constants.Roles.CREATOR
|
return Constants.Roles.CREATOR
|
||||||
}
|
}
|
||||||
|
|
||||||
return invite.info.apps?.[prodAppId]
|
return invite.info.apps?.[prodAppId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,7 +509,7 @@
|
||||||
return `This user has been given ${role?.name} access from the ${user.group} group`
|
return `This user has been given ${role?.name} access from the ${user.group} group`
|
||||||
}
|
}
|
||||||
if (user.isAdminOrGlobalBuilder) {
|
if (user.isAdminOrGlobalBuilder) {
|
||||||
return "This user's role grants admin access to all apps"
|
return "Account admins can edit all apps"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -523,6 +520,18 @@
|
||||||
}
|
}
|
||||||
return user.role
|
return user.role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkAppAccess = e => {
|
||||||
|
// Ensure we don't get into an invalid combo of tenant role and app access
|
||||||
|
if (
|
||||||
|
e.detail === Constants.BudibaseRoles.AppUser &&
|
||||||
|
creationAccessType === Constants.Roles.CREATOR
|
||||||
|
) {
|
||||||
|
creationAccessType = Constants.Roles.BASIC
|
||||||
|
} else if (e.detail === Constants.BudibaseRoles.Admin) {
|
||||||
|
creationAccessType = Constants.Roles.CREATOR
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
@ -650,8 +659,9 @@
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
allowedRoles={user.isAdminOrGlobalBuilder
|
allowedRoles={user.isAdminOrGlobalBuilder
|
||||||
? [Constants.Roles.ADMIN]
|
? [Constants.Roles.CREATOR]
|
||||||
: null}
|
: null}
|
||||||
|
labelPrefix="Can use as"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -695,19 +705,16 @@
|
||||||
allowRemove={group.role}
|
allowRemove={group.role}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
allowCreator={true}
|
allowCreator={group.role === Constants.Roles.CREATOR}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
if (e.detail === Constants.Roles.CREATOR) {
|
onUpdateGroup(group, e.detail)
|
||||||
addGroupAppBuilder(group._id)
|
|
||||||
} else {
|
|
||||||
onUpdateGroup(group, e.detail)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
on:remove={() => {
|
on:remove={() => {
|
||||||
onUpdateGroup(group)
|
onUpdateGroup(group)
|
||||||
}}
|
}}
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
|
labelPrefix="Can use as"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -753,6 +760,7 @@
|
||||||
allowedRoles={user.isAdminOrGlobalBuilder
|
allowedRoles={user.isAdminOrGlobalBuilder
|
||||||
? [Constants.Roles.CREATOR]
|
? [Constants.Roles.CREATOR]
|
||||||
: null}
|
: null}
|
||||||
|
labelPrefix="Can use as"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -804,33 +812,34 @@
|
||||||
<FancySelect
|
<FancySelect
|
||||||
bind:value={creationRoleType}
|
bind:value={creationRoleType}
|
||||||
options={sdk.users.isAdmin($auth.user)
|
options={sdk.users.isAdmin($auth.user)
|
||||||
? Constants.BudibaseRoleOptionsNew
|
? Constants.BudibaseRoleOptions
|
||||||
: Constants.BudibaseRoleOptionsNew.filter(
|
: Constants.BudibaseRoleOptions.filter(
|
||||||
option => option.value !== Constants.BudibaseRoles.Admin
|
option => option.value !== Constants.BudibaseRoles.Admin
|
||||||
)}
|
)}
|
||||||
label="Access"
|
label="Role"
|
||||||
|
on:change={checkAppAccess}
|
||||||
/>
|
/>
|
||||||
{#if creationRoleType !== Constants.BudibaseRoles.Admin}
|
<span class="role-wrap">
|
||||||
<span class="role-wrap">
|
<RoleSelect
|
||||||
<RoleSelect
|
placeholder={false}
|
||||||
placeholder={false}
|
bind:value={creationAccessType}
|
||||||
bind:value={creationAccessType}
|
allowPublic={false}
|
||||||
allowPublic={false}
|
allowCreator={creationRoleType !==
|
||||||
allowCreator={true}
|
Constants.BudibaseRoles.AppUser}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
fancySelect
|
fancySelect
|
||||||
/>
|
allowedRoles={creationRoleType === Constants.BudibaseRoles.Admin
|
||||||
</span>
|
? [Constants.Roles.CREATOR]
|
||||||
{/if}
|
: null}
|
||||||
|
footer={getRoleFooter({
|
||||||
|
isAdminOrGlobalBuilder:
|
||||||
|
creationRoleType === Constants.BudibaseRoles.Admin,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</FancyForm>
|
</FancyForm>
|
||||||
{#if creationRoleType === Constants.BudibaseRoles.Admin}
|
|
||||||
<div class="admin-info">
|
|
||||||
<Icon name="Info" />
|
|
||||||
Admins will get full access to all apps and settings
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<span class="add-user">
|
<span class="add-user">
|
||||||
<Button
|
<Button
|
||||||
newStyles
|
newStyles
|
||||||
|
@ -871,16 +880,6 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-info {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
padding: var(--spacing-l) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
height: 30px;
|
|
||||||
background-color: var(--background-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.underlined {
|
.underlined {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -898,7 +897,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
width: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-entity-meta {
|
.auth-entity-meta {
|
||||||
|
@ -927,7 +925,7 @@
|
||||||
.auth-entity,
|
.auth-entity,
|
||||||
.auth-entity-header {
|
.auth-entity-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 110px;
|
grid-template-columns: 1fr 180px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
@ -958,7 +956,7 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 400px;
|
width: 440px;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { url, isActive } from "@roxi/routify"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
import { isOnlyUser } from "builderStore"
|
import { isOnlyUser } from "builderStore"
|
||||||
import { auth } from "stores/portal"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
let deleteModal
|
let deleteModal
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,24 +44,22 @@
|
||||||
url={$url("./version")}
|
url={$url("./version")}
|
||||||
active={$isActive("./version")}
|
active={$isActive("./version")}
|
||||||
/>
|
/>
|
||||||
{#if sdk.users.isGlobalBuilder($auth.user)}
|
<div class="delete-action">
|
||||||
<div class="delete-action">
|
<AbsTooltip
|
||||||
<AbsTooltip
|
position={TooltipPosition.Bottom}
|
||||||
position={TooltipPosition.Bottom}
|
text={$isOnlyUser
|
||||||
text={$isOnlyUser
|
? null
|
||||||
? null
|
: "Unavailable - another user is editing this app"}
|
||||||
: "Unavailable - another user is editing this app"}
|
>
|
||||||
>
|
<SideNavItem
|
||||||
<SideNavItem
|
text="Delete app"
|
||||||
text="Delete app"
|
disabled={!$isOnlyUser}
|
||||||
disabled={!$isOnlyUser}
|
on:click={() => {
|
||||||
on:click={() => {
|
deleteModal.show()
|
||||||
deleteModal.show()
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</AbsTooltip>
|
||||||
</AbsTooltip>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<slot />
|
<slot />
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
let activeTab = "Apps"
|
let activeTab = "Apps"
|
||||||
|
|
||||||
$: $url(), updateActiveTab($menu)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: isOnboarding = !$apps.length && sdk.users.isGlobalBuilder($auth.user)
|
$: isOnboarding = !$apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
||||||
|
|
||||||
const updateActiveTab = menu => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to new app page if no apps exists
|
// Go to new app page if no apps exists
|
||||||
if (!$apps.length && sdk.users.isGlobalBuilder($auth.user)) {
|
if (!$apps.length && sdk.users.hasBuilderPermissions($auth.user)) {
|
||||||
$redirect("./onboarding")
|
$redirect("./onboarding")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -237,7 +237,7 @@
|
||||||
{#if enrichedApps.length}
|
{#if enrichedApps.length}
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
|
{#if $auth.user && sdk.users.canCreateApps($auth.user)}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
goToApp()
|
goToApp()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading = false
|
loading = false
|
||||||
notifications.error("There was a problem creating your app")
|
notifications.error(e.message || "There was a problem creating your app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
width: "1fr",
|
width: "1fr",
|
||||||
|
displayName: "Access",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const customGroupTableRenderers = [
|
const customGroupTableRenderers = [
|
||||||
|
@ -98,7 +99,7 @@
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
$: globalRole = sdk.users.isAdmin(user) ? "admin" : "appUser"
|
$: globalRole = users.getUserRole(user)
|
||||||
|
|
||||||
const getAvailableApps = (appList, privileged, roles) => {
|
const getAvailableApps = (appList, privileged, roles) => {
|
||||||
let availableApps = appList.slice()
|
let availableApps = appList.slice()
|
||||||
|
@ -177,12 +178,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserRole({ detail }) {
|
async function updateUserRole({ detail }) {
|
||||||
if (detail === "developer") {
|
if (detail === Constants.BudibaseRoles.Developer) {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
||||||
} else if (detail === "admin") {
|
} else if (detail === Constants.BudibaseRoles.Admin) {
|
||||||
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
||||||
} else if (detail === "appUser") {
|
} else if (detail === Constants.BudibaseRoles.AppUser) {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
||||||
|
} else if (detail === Constants.BudibaseRoles.Creator) {
|
||||||
|
toggleFlags({
|
||||||
|
admin: { global: false },
|
||||||
|
builder: {
|
||||||
|
global: false,
|
||||||
|
creator: true,
|
||||||
|
apps: user?.builder?.apps || [],
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +305,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Role</Label>
|
<Label size="L">Role</Label>
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={null}
|
||||||
disabled={!sdk.users.isAdmin($auth.user)}
|
disabled={!sdk.users.isAdmin($auth.user)}
|
||||||
value={globalRole}
|
value={globalRole}
|
||||||
options={Constants.BudibaseRoleOptions}
|
options={Constants.BudibaseRoleOptions}
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
$: hasError = userData.find(x => x.error != null)
|
$: hasError = userData.find(x => x.error != null)
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userData.length
|
$: userCount = $licensing.userCount + userData.length
|
||||||
$: reached = licensing.usersLimitReached(userCount)
|
$: reached = licensing.usersLimitReached(userCount)
|
||||||
$: exceeded = licensing.usersLimitExceeded(userCount)
|
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||||
|
@ -98,7 +97,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;"
|
flex-direction: row;"
|
||||||
>
|
>
|
||||||
<div style="width: 90%">
|
<div style="flex: 1 1 auto;">
|
||||||
<InputDropdown
|
<InputDropdown
|
||||||
inputType="email"
|
inputType="email"
|
||||||
bind:inputValue={input.email}
|
bind:inputValue={input.email}
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<StatusLight square color={RoleUtils.getRoleColour(value)}>
|
{#if value === Constants.Roles.CREATOR}
|
||||||
{getRoleLabel(value)}
|
Can edit
|
||||||
</StatusLight>
|
{:else}
|
||||||
|
<StatusLight square color={RoleUtils.getRoleColour(value)}>
|
||||||
|
Can use as {getRoleLabel(value)}
|
||||||
|
</StatusLight>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
||||||
const MAX_USERS_UPLOAD_LIMIT = 1000
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
||||||
|
|
||||||
export let createUsersFromCsv
|
export let createUsersFromCsv
|
||||||
|
|
||||||
let files = []
|
let files = []
|
||||||
|
@ -22,13 +23,16 @@
|
||||||
let userEmails = []
|
let userEmails = []
|
||||||
let userGroups = []
|
let userGroups = []
|
||||||
let usersRole = null
|
let usersRole = null
|
||||||
$: invalidEmails = []
|
|
||||||
|
|
||||||
|
$: invalidEmails = []
|
||||||
$: userCount = $licensing.userCount + userEmails.length
|
$: userCount = $licensing.userCount + userEmails.length
|
||||||
$: exceed = licensing.usersLimitExceeded(userCount)
|
$: exceed = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
$: importDisabled =
|
$: importDisabled =
|
||||||
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
||||||
|
$: roleOptions = Constants.BudibaseRoleOptions.map(option => ({
|
||||||
|
...option,
|
||||||
|
label: `${option.label} - ${option.subtitle}`,
|
||||||
|
}))
|
||||||
|
|
||||||
const validEmails = userEmails => {
|
const validEmails = userEmails => {
|
||||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||||
|
@ -100,10 +104,7 @@
|
||||||
users. Upgrade your plan to add more users
|
users. Upgrade your plan to add more users
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<RadioGroup
|
<RadioGroup bind:value={usersRole} options={roleOptions} />
|
||||||
bind:value={usersRole}
|
|
||||||
options={Constants.BuilderRoleDescriptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if $licensing.groupsEnabled}
|
{#if $licensing.groupsEnabled}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
|
|
|
@ -4,17 +4,11 @@
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
const TooltipMap = {
|
$: role = Constants.BudibaseRoleOptions.find(
|
||||||
appUser: "Only has access to assigned apps",
|
|
||||||
developer: "Access to the app builder",
|
|
||||||
admin: "Full access",
|
|
||||||
}
|
|
||||||
|
|
||||||
$: role = Constants.BudibaseRoleOptionsOld.find(
|
|
||||||
x => x.value === users.getUserRole(row)
|
x => x.value === users.getUserRole(row)
|
||||||
)
|
)
|
||||||
$: value = role?.label || "Not available"
|
$: value = role?.label || "Not available"
|
||||||
$: tooltip = TooltipMap[role?.value] || ""
|
$: tooltip = role.subtitle || ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div on:click|stopPropagation title={tooltip}>
|
<div on:click|stopPropagation title={tooltip}>
|
||||||
|
|
|
@ -172,6 +172,7 @@
|
||||||
const payload = userData?.users?.map(user => ({
|
const payload = userData?.users?.map(user => ({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
builder: user.role === Constants.BudibaseRoles.Developer,
|
builder: user.role === Constants.BudibaseRoles.Developer,
|
||||||
|
creator: user.role === Constants.BudibaseRoles.Creator,
|
||||||
admin: user.role === Constants.BudibaseRoles.Admin,
|
admin: user.role === Constants.BudibaseRoles.Admin,
|
||||||
groups: userData.groups,
|
groups: userData.groups,
|
||||||
}))
|
}))
|
||||||
|
@ -190,18 +191,18 @@
|
||||||
|
|
||||||
for (const user of userData?.users ?? []) {
|
for (const user of userData?.users ?? []) {
|
||||||
const { email } = user
|
const { email } = user
|
||||||
|
|
||||||
if (
|
if (
|
||||||
newUsers.find(x => x.email === email) ||
|
newUsers.find(x => x.email === email) ||
|
||||||
currentUserEmails.includes(email)
|
currentUserEmails.includes(email)
|
||||||
)
|
) {
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
newUsers.push(user)
|
newUsers.push(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newUsers.length)
|
if (!newUsers.length) {
|
||||||
notifications.info("Duplicated! There is no new users to add.")
|
notifications.info("Duplicated! There is no new users to add.")
|
||||||
|
}
|
||||||
return { ...userData, users: newUsers }
|
return { ...userData, users: newUsers }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +267,6 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
groupsLoaded = true
|
groupsLoaded = true
|
||||||
|
|
||||||
pendingInvites = await users.getInvites()
|
pendingInvites = await users.getInvites()
|
||||||
invitesLoaded = true
|
invitesLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { API } from "api"
|
||||||
import { update } from "lodash"
|
import { update } from "lodash"
|
||||||
import { licensing } from "."
|
import { licensing } from "."
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export function createUsersStore() {
|
export function createUsersStore() {
|
||||||
const { subscribe, set } = writable({})
|
const { subscribe, set } = writable({})
|
||||||
|
@ -77,6 +78,9 @@ export function createUsersStore() {
|
||||||
case "developer":
|
case "developer":
|
||||||
body.builder = { global: true }
|
body.builder = { global: true }
|
||||||
break
|
break
|
||||||
|
case "creator":
|
||||||
|
body.builder = { creator: true, global: false }
|
||||||
|
break
|
||||||
case "admin":
|
case "admin":
|
||||||
body.admin = { global: true }
|
body.admin = { global: true }
|
||||||
body.builder = { global: true }
|
body.builder = { global: true }
|
||||||
|
@ -120,12 +124,18 @@ export function createUsersStore() {
|
||||||
return await API.removeAppBuilder({ userId, appId })
|
return await API.removeAppBuilder({ userId, appId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserRole = user =>
|
const getUserRole = user => {
|
||||||
sdk.users.isAdmin(user)
|
if (sdk.users.isAdmin(user)) {
|
||||||
? "admin"
|
return Constants.BudibaseRoles.Admin
|
||||||
: sdk.users.isBuilder(user)
|
} else if (sdk.users.isBuilder(user)) {
|
||||||
? "developer"
|
return Constants.BudibaseRoles.Developer
|
||||||
: "appUser"
|
} else if (sdk.users.hasCreatorPermissions(user)) {
|
||||||
|
return Constants.BudibaseRoles.Creator
|
||||||
|
} else {
|
||||||
|
return Constants.BudibaseRoles.AppUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const refreshUsage =
|
const refreshUsage =
|
||||||
fn =>
|
fn =>
|
||||||
async (...args) => {
|
async (...args) => {
|
||||||
|
|
|
@ -214,15 +214,23 @@ export const buildUserEndpoints = API => ({
|
||||||
inviteUsers: async users => {
|
inviteUsers: async users => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/global/users/multi/invite",
|
url: "/api/global/users/multi/invite",
|
||||||
body: users.map(user => ({
|
body: users.map(user => {
|
||||||
email: user.email,
|
let builder = undefined
|
||||||
userInfo: {
|
if (user.admin || user.builder) {
|
||||||
admin: user.admin ? { global: true } : undefined,
|
builder = { global: true }
|
||||||
builder: user.admin || user.builder ? { global: true } : undefined,
|
} else if (user.creator) {
|
||||||
userGroups: user.groups,
|
builder = { creator: true }
|
||||||
roles: user.apps ? user.apps : undefined,
|
}
|
||||||
},
|
return {
|
||||||
})),
|
email: user.email,
|
||||||
|
userInfo: {
|
||||||
|
admin: user.admin ? { global: true } : undefined,
|
||||||
|
builder,
|
||||||
|
userGroups: user.groups,
|
||||||
|
roles: user.apps ? user.apps : undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -20,42 +20,31 @@ export const TableNames = {
|
||||||
export const BudibaseRoles = {
|
export const BudibaseRoles = {
|
||||||
AppUser: "appUser",
|
AppUser: "appUser",
|
||||||
Developer: "developer",
|
Developer: "developer",
|
||||||
|
Creator: "creator",
|
||||||
Admin: "admin",
|
Admin: "admin",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BudibaseRoleOptionsOld = [
|
export const BudibaseRoleOptionsOld = [
|
||||||
{ label: "Developer", value: BudibaseRoles.Developer },
|
{
|
||||||
{ label: "Member", value: BudibaseRoles.AppUser },
|
label: "Developer",
|
||||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
value: BudibaseRoles.Developer,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
export const BudibaseRoleOptions = [
|
export const BudibaseRoleOptions = [
|
||||||
{ label: "Member", value: BudibaseRoles.AppUser },
|
|
||||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const BudibaseRoleOptionsNew = [
|
|
||||||
{
|
{
|
||||||
label: "Admin",
|
label: "Account admin",
|
||||||
value: "admin",
|
value: BudibaseRoles.Admin,
|
||||||
subtitle: "Has full access to all apps and settings in your account",
|
subtitle: "Has full access to all apps and settings in your account",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Member",
|
label: "Creator",
|
||||||
value: "appUser",
|
value: BudibaseRoles.Creator,
|
||||||
subtitle: "Can only view apps they have access to",
|
subtitle: "Can create and edit apps they have access to",
|
||||||
},
|
},
|
||||||
]
|
|
||||||
|
|
||||||
export const BuilderRoleDescriptions = [
|
|
||||||
{
|
{
|
||||||
|
label: "App user",
|
||||||
value: BudibaseRoles.AppUser,
|
value: BudibaseRoles.AppUser,
|
||||||
icon: "User",
|
subtitle: "Can only use published apps they have access to",
|
||||||
label: "App user - Only has access to published apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: BudibaseRoles.Admin,
|
|
||||||
icon: "Draw",
|
|
||||||
label: "Admin - Full access",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import {
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
|
import { sdk as sharedCoreSDK } from "@budibase/shared-core"
|
||||||
|
|
||||||
// utility function, need to do away with this
|
// utility function, need to do away with this
|
||||||
async function getLayouts() {
|
async function getLayouts() {
|
||||||
|
@ -394,6 +395,12 @@ async function appPostCreate(ctx: UserCtx, app: App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user is a creator, we need to give them access to the new app
|
||||||
|
if (sharedCoreSDK.users.hasCreatorPermissions(ctx.user)) {
|
||||||
|
const user = await users.UserDB.getUser(ctx.user._id!)
|
||||||
|
await users.addAppBuilder(user, app.appId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(ctx: UserCtx) {
|
export async function create(ctx: UserCtx) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ router
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/applications",
|
"/api/applications",
|
||||||
authorized(permissions.GLOBAL_BUILDER),
|
authorized(permissions.CREATOR),
|
||||||
applicationValidator(),
|
applicationValidator(),
|
||||||
controller.create
|
controller.create
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
roles,
|
roles,
|
||||||
users,
|
users,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types"
|
import { PermissionLevel, PermissionType, UserCtx } from "@budibase/types"
|
||||||
import builderMiddleware from "./builder"
|
import builderMiddleware from "./builder"
|
||||||
import { isWebhookEndpoint } from "./utils"
|
import { isWebhookEndpoint } from "./utils"
|
||||||
import { paramResource } from "./resourceId"
|
import { paramResource } from "./resourceId"
|
||||||
|
@ -31,13 +31,20 @@ const checkAuthorized = async (
|
||||||
) => {
|
) => {
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
const isGlobalBuilderApi = permType === PermissionType.GLOBAL_BUILDER
|
const isGlobalBuilderApi = permType === PermissionType.GLOBAL_BUILDER
|
||||||
|
const isCreatorApi = permType === PermissionType.CREATOR
|
||||||
const isBuilderApi = permType === PermissionType.BUILDER
|
const isBuilderApi = permType === PermissionType.BUILDER
|
||||||
const globalBuilder = users.isGlobalBuilder(ctx.user)
|
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
||||||
let isBuilder = appId
|
const isCreator = users.isCreator(ctx.user)
|
||||||
|
const isBuilder = appId
|
||||||
? users.isBuilder(ctx.user, appId)
|
? users.isBuilder(ctx.user, appId)
|
||||||
: users.hasBuilderPermissions(ctx.user)
|
: users.hasBuilderPermissions(ctx.user)
|
||||||
// check if this is a builder api and the user is not a builder
|
|
||||||
if ((isGlobalBuilderApi && !globalBuilder) || (isBuilderApi && !isBuilder)) {
|
// check api permission type against user
|
||||||
|
if (
|
||||||
|
(isGlobalBuilderApi && !isGlobalBuilder) ||
|
||||||
|
(isCreatorApi && !isCreator) ||
|
||||||
|
(isBuilderApi && !isBuilder)
|
||||||
|
) {
|
||||||
return ctx.throw(403, "Not Authorized")
|
return ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +155,7 @@ const authorized =
|
||||||
// to find API endpoints which are builder focused
|
// to find API endpoints which are builder focused
|
||||||
if (
|
if (
|
||||||
permType === PermissionType.BUILDER ||
|
permType === PermissionType.BUILDER ||
|
||||||
|
permType === PermissionType.CREATOR ||
|
||||||
permType === PermissionType.GLOBAL_BUILDER
|
permType === PermissionType.GLOBAL_BUILDER
|
||||||
) {
|
) {
|
||||||
await builderMiddleware(ctx)
|
await builderMiddleware(ctx)
|
||||||
|
|
|
@ -25,6 +25,10 @@ export function isGlobalBuilder(user: User | ContextUser): boolean {
|
||||||
return (isBuilder(user) && !hasAppBuilderPermissions(user)) || isAdmin(user)
|
return (isBuilder(user) && !hasAppBuilderPermissions(user)) || isAdmin(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canCreateApps(user: User | ContextUser): boolean {
|
||||||
|
return isGlobalBuilder(user) || hasCreatorPermissions(user)
|
||||||
|
}
|
||||||
|
|
||||||
// alias for hasAdminPermission, currently do the same thing
|
// alias for hasAdminPermission, currently do the same thing
|
||||||
// in future whether someone has admin permissions and whether they are
|
// in future whether someone has admin permissions and whether they are
|
||||||
// an admin for a specific resource could be separated
|
// an admin for a specific resource could be separated
|
||||||
|
@ -66,7 +70,7 @@ export function hasAppCreatorPermissions(user?: User | ContextUser): boolean {
|
||||||
return _.flow(
|
return _.flow(
|
||||||
_.get("roles"),
|
_.get("roles"),
|
||||||
_.values,
|
_.values,
|
||||||
_.find(x => ["CREATOR", "ADMIN"].includes(x)),
|
_.find(x => x === "CREATOR"),
|
||||||
x => !!x
|
x => !!x
|
||||||
)(user)
|
)(user)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +80,11 @@ export function hasBuilderPermissions(user?: User | ContextUser): boolean {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return user.builder?.global || hasAppBuilderPermissions(user)
|
return (
|
||||||
|
user.builder?.global ||
|
||||||
|
hasAppBuilderPermissions(user) ||
|
||||||
|
hasCreatorPermissions(user)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a user is capable of being an admin
|
// checks if a user is capable of being an admin
|
||||||
|
@ -87,13 +95,21 @@ export function hasAdminPermissions(user?: User | ContextUser): boolean {
|
||||||
return !!user.admin?.global
|
return !!user.admin?.global
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasCreatorPermissions(user?: User | ContextUser): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!user.builder?.creator
|
||||||
|
}
|
||||||
|
|
||||||
export function isCreator(user?: User | ContextUser): boolean {
|
export function isCreator(user?: User | ContextUser): boolean {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
isGlobalBuilder(user) ||
|
isGlobalBuilder(user!) ||
|
||||||
hasAdminPermissions(user) ||
|
hasAdminPermissions(user) ||
|
||||||
|
hasCreatorPermissions(user) ||
|
||||||
hasAppBuilderPermissions(user) ||
|
hasAppBuilderPermissions(user) ||
|
||||||
hasAppCreatorPermissions(user)
|
hasAppCreatorPermissions(user)
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface User extends Document {
|
||||||
builder?: {
|
builder?: {
|
||||||
global?: boolean
|
global?: boolean
|
||||||
apps?: string[]
|
apps?: string[]
|
||||||
|
creator?: boolean
|
||||||
}
|
}
|
||||||
admin?: {
|
admin?: {
|
||||||
global: boolean
|
global: boolean
|
||||||
|
|
|
@ -13,6 +13,7 @@ export enum PermissionType {
|
||||||
AUTOMATION = "automation",
|
AUTOMATION = "automation",
|
||||||
WEBHOOK = "webhook",
|
WEBHOOK = "webhook",
|
||||||
BUILDER = "builder",
|
BUILDER = "builder",
|
||||||
|
CREATOR = "creator",
|
||||||
GLOBAL_BUILDER = "globalBuilder",
|
GLOBAL_BUILDER = "globalBuilder",
|
||||||
QUERY = "query",
|
QUERY = "query",
|
||||||
VIEW = "view",
|
VIEW = "view",
|
||||||
|
|
|
@ -51,10 +51,22 @@ export async function removeAppRole(ctx: Ctx) {
|
||||||
const users = await sdk.users.db.allUsers()
|
const users = await sdk.users.db.allUsers()
|
||||||
const bulk = []
|
const bulk = []
|
||||||
const cacheInvalidations = []
|
const cacheInvalidations = []
|
||||||
|
const prodAppId = dbCore.getProdAppID(appId)
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
if (user.roles[appId]) {
|
let updated = false
|
||||||
cacheInvalidations.push(cache.user.invalidateUser(user._id))
|
if (user.roles[prodAppId]) {
|
||||||
delete user.roles[appId]
|
cacheInvalidations.push(cache.user.invalidateUser(user._id!))
|
||||||
|
delete user.roles[prodAppId]
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
if (user.builder && Array.isArray(user.builder?.apps)) {
|
||||||
|
const idx = user.builder.apps.indexOf(prodAppId)
|
||||||
|
if (idx !== -1) {
|
||||||
|
user.builder.apps.splice(idx, 1)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
bulk.push(user)
|
bulk.push(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue