diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index a64be6b319..7fb4eefc8b 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -45,10 +45,22 @@ export class Role implements RoleDoc { inherits?: string version?: string permissions = {} + displayName?: string + color?: string + description?: string - constructor(id: string, name: string, permissionId: string) { + constructor( + id: string, + displayName: string, + description: string, + color: string, + permissionId: string + ) { this._id = id - this.name = name + this.name = id + this.displayName = displayName + this.color = color + this.description = description this.permissionId = permissionId // version for managing the ID - removing the role_ when responding this.version = RoleIDVersion.NAME @@ -63,21 +75,39 @@ export class Role implements RoleDoc { const BUILTIN_ROLES = { ADMIN: new Role( BUILTIN_IDS.ADMIN, - "Admin", + "App admin", + "Can do everything", + "var(--spectrum-global-color-static-red-400)", BuiltinPermissionID.ADMIN ).addInheritance(BUILTIN_IDS.POWER), POWER: new Role( BUILTIN_IDS.POWER, - "Power", + "App power user", + "An app user with more access", + "var(--spectrum-global-color-static-orange-400)", BuiltinPermissionID.POWER ).addInheritance(BUILTIN_IDS.BASIC), BASIC: new Role( BUILTIN_IDS.BASIC, - "Basic", + "App user", + "Any logged in user", + "var(--spectrum-global-color-static-green-400)", BuiltinPermissionID.WRITE ).addInheritance(BUILTIN_IDS.PUBLIC), - PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public", BuiltinPermissionID.PUBLIC), - BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder", BuiltinPermissionID.ADMIN), + PUBLIC: new Role( + BUILTIN_IDS.PUBLIC, + "Public user", + "Accessible to anyone", + "var(--spectrum-global-color-static-blue-400)", + BuiltinPermissionID.PUBLIC + ), + BUILDER: new Role( + BUILTIN_IDS.BUILDER, + "Builder user", + "Users that can edit this app", + "var(--spectrum-global-color-static-magenta-600)", + BuiltinPermissionID.ADMIN + ), } export function getBuiltinRoles(): { [key: string]: RoleDoc } { diff --git a/packages/builder/src/components/backend/RoleEditor/Controls.svelte b/packages/builder/src/components/backend/RoleEditor/Controls.svelte index aa0b0112d8..94c82dcd65 100644 --- a/packages/builder/src/components/backend/RoleEditor/Controls.svelte +++ b/packages/builder/src/components/backend/RoleEditor/Controls.svelte @@ -2,29 +2,28 @@ import { Button, Helpers, ActionButton } from "@budibase/bbui" import { useSvelteFlow, Position } from "@xyflow/svelte" import { getContext, tick } from "svelte" - import { autoLayout } from "./layout" + import { autoLayout, roleToNode } from "./layout" import { ZoomDuration } from "./constants" + import { getSequentialName } from "helpers/duplicate" + import { roles } from "stores/builder" + import { Roles } from "constants/backend" const { nodes, edges } = getContext("flow") const flow = useSvelteFlow() const addRole = async () => { - nodes.update(state => [ - ...state, - { - id: Helpers.uuid(), - sourcePosition: Position.Right, - targetPosition: Position.Left, - type: "role", - data: { - displayName: "New role", - description: "Custom role", - custom: true, - color: "var(--spectrum-global-color-gray-700)", - }, - position: { x: 0, y: 0 }, - }, - ]) + const role = { + name: Helpers.uuid(), + displayName: getSequentialName($nodes, "New role ", { + getName: x => x.data.displayName, + }), + color: "var(--spectrum-global-color-gray-700)", + description: "Custom role", + permissionId: "write", + inherits: Roles.BASIC, + } + const savedRole = await roles.save(role) + nodes.update(state => [...state, roleToNode(savedRole)]) await doAutoLayout() } @@ -36,7 +35,6 @@ flow.fitView({ maxZoom: 1, duration: ZoomDuration, - includeHiddenNodes: true, }) } diff --git a/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte b/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte index 42c18934cf..9904a8877b 100644 --- a/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte +++ b/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte @@ -4,7 +4,7 @@ import { SvelteFlow, Background, BackgroundVariant } from "@xyflow/svelte" import "@xyflow/svelte/dist/style.css" import RoleNode from "./RoleNode.svelte" - import { defaultLayout, autoLayout } from "./layout" + import { rolesToNodes, autoLayout } from "./layout" import { onMount, setContext } from "svelte" import Controls from "./Controls.svelte" @@ -14,7 +14,7 @@ setContext("flow", { nodes, edges }) onMount(() => { - const layout = autoLayout(defaultLayout()) + const layout = autoLayout(rolesToNodes()) nodes.set(layout.nodes) edges.set(layout.edges) }) diff --git a/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte b/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte index aaf228638d..42a107ee2e 100644 --- a/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte +++ b/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte @@ -12,7 +12,8 @@ import { Roles } from "constants/backend" import { NodeWidth, NodeHeight } from "./constants" import { getContext, tick } from "svelte" - import { autoLayout } from "./layout" + import { autoLayout, nodeToRole } from "./layout" + import { roles } from "stores/builder" export let data export let isConnectable @@ -27,9 +28,8 @@ let tempDescription let tempColor - $: color = data.color || RoleUtils.getRoleColour(id) - const deleteNode = async () => { + await roles.delete(nodeToRole({ id, data })) flow.deleteElements({ nodes: [{ id }], }) @@ -40,11 +40,12 @@ const openPopover = () => { tempDisplayName = data.displayName tempDescription = data.description - tempColor = color + tempColor = data.color modal.show() } - const saveChanges = () => { + const saveChanges = async () => { + await roles.save(nodeToRole({ id, data })) flow.updateNodeData(id, { displayName: tempDisplayName, description: tempDescription, @@ -63,7 +64,7 @@
diff --git a/packages/builder/src/components/backend/RoleEditor/layout.js b/packages/builder/src/components/backend/RoleEditor/layout.js index 4bb6d1bb05..b8a5ae7d5a 100644 --- a/packages/builder/src/components/backend/RoleEditor/layout.js +++ b/packages/builder/src/components/backend/RoleEditor/layout.js @@ -6,15 +6,36 @@ import { Roles } from "constants/backend" import { get } from "svelte/store" import { Helpers } from "@budibase/bbui" +// Converts a role doc into a node structure +export const roleToNode = role => ({ + id: role._id, + sourcePosition: Position.Right, + targetPosition: Position.Left, + type: "role", + position: { x: 0, y: 0 }, + data: { + displayName: role.displayName || role.name, + description: role.description || "Custom role", + color: role.color || "var(--spectrum-global-color-static-magenta-400)", + custom: !role._id.match(/[A-Z]+/), + }, +}) + +// Converts a node structure back into a role doc +export const nodeToRole = node => { + const role = get(roles).find(x => x._id === node.id) + return { + ...role, + displayName: node.data.displayName, + color: node.data.color, + description: node.data.description, + } +} + // Generates a flow compatible structure of nodes and edges from the current roles -export const defaultLayout = () => { +export const rolesToNodes = () => { const ignoredRoles = [Roles.PUBLIC] const $roles = get(roles) - const descriptions = { - [Roles.BASIC]: "Basic user", - [Roles.POWER]: "Power user", - [Roles.ADMIN]: "Can do everything", - } let nodes = [] let edges = [] @@ -23,19 +44,11 @@ export const defaultLayout = () => { if (ignoredRoles.includes(role._id)) { continue } - nodes.push({ - id: role._id, - sourcePosition: Position.Right, - targetPosition: Position.Left, - type: "role", - data: { - displayName: role.displayName || role.name || "", - description: descriptions[role._id] || "Custom role", - color: role.color, - custom: !role._id.match(/[A-Z]+/), - }, - }) + // Add node for this role + nodes.push(roleToNode(role)) + + // Add edges for this role let inherits = [] if (role.inherits) { inherits = Array.isArray(role.inherits) ? role.inherits : [role.inherits] @@ -60,40 +73,6 @@ export const defaultLayout = () => { } } -// Converts the flow structure of ndes and edges back into an array of roles -export const layoutToRoles = ({ nodes, edges }) => { - // Clone and wipe existing inheritance - let newRoles = Helpers.cloneDeep(get(roles)).map(role => { - return { ...role, inherits: [] } - }) - - // Copy over names and colours - for (let node of nodes) { - let role = newRoles.find(x => x._id === node.id) - if (role) { - role.name = node.data.label - role.color = node.data.color - } else { - // New role - } - } - - // Build inheritance - for (let edge of edges) { - let role = newRoles.find(x => x._id === edge.target) - if (role) { - role.inherits.push(edge.source) - } else { - // New role - } - } - - // Ensure basic is correct - newRoles.find(x => x._id === Roles.BASIC).inherits = [Roles.BASIC] - - return newRoles -} - // Updates positions of nodes and edges into a nice graph structure const dagreLayout = ({ nodes, edges }) => { const dagreGraph = new dagre.graphlib.Graph() diff --git a/packages/builder/src/components/common/RoleIcon.svelte b/packages/builder/src/components/common/RoleIcon.svelte index 1bd6ba49bc..567a7c0b5d 100644 --- a/packages/builder/src/components/common/RoleIcon.svelte +++ b/packages/builder/src/components/common/RoleIcon.svelte @@ -1,12 +1,15 @@ diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index 4605b0c182..72495fe3c9 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -99,7 +99,7 @@ if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) { return null } - return RoleUtils.getRoleColour(role._id) + return role.color || "var(--spectrum-global-color-static-magenta-400)" } const getIcon = role => { diff --git a/packages/builder/src/components/design/settings/controls/RoleSelect.svelte b/packages/builder/src/components/design/settings/controls/RoleSelect.svelte index 5b5daac183..973a239bd8 100644 --- a/packages/builder/src/components/design/settings/controls/RoleSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/RoleSelect.svelte @@ -14,7 +14,8 @@ options={$roles} getOptionLabel={role => role.name} getOptionValue={role => role._id} - getOptionColour={role => RoleUtils.getRoleColour(role._id)} + getOptionColour={role => + role.color || "var(--spectrum-global-color-static-magenta-400)"} {placeholder} {error} /> diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/RoleIndicator.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/RoleIndicator.svelte index 4b7f26709c..994a56e833 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/RoleIndicator.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/RoleIndicator.svelte @@ -8,7 +8,7 @@ let showTooltip = false - $: color = RoleUtils.getRoleColour(roleId) + $: color = role.color || "var(--spectrum-global-color-static-magenta-400)" $: role = $roles.find(role => role._id === roleId) $: tooltip = roleId === Roles.PUBLIC diff --git a/packages/builder/src/pages/builder/portal/users/users/_components/AppRoleTableRenderer.svelte b/packages/builder/src/pages/builder/portal/users/users/_components/AppRoleTableRenderer.svelte index 0f19bb3e1f..85d94ba350 100644 --- a/packages/builder/src/pages/builder/portal/users/users/_components/AppRoleTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/_components/AppRoleTableRenderer.svelte @@ -6,8 +6,9 @@ export let value + $: role = $roles.find(x => x._id === roleId) + const getRoleLabel = roleId => { - const role = $roles.find(x => x._id === roleId) return roleId === Constants.Roles.CREATOR ? capitalise(Constants.Roles.CREATOR.toLowerCase()) : role?.name || "Custom role" @@ -17,7 +18,10 @@ {#if value === Constants.Roles.CREATOR} Can edit {:else} - + Can use as {getRoleLabel(value)} {/if} diff --git a/packages/builder/src/stores/builder/roles.js b/packages/builder/src/stores/builder/roles.js index 8f1bfdc177..a438abac56 100644 --- a/packages/builder/src/stores/builder/roles.js +++ b/packages/builder/src/stores/builder/roles.js @@ -1,14 +1,6 @@ import { writable } from "svelte/store" import { API } from "api" import { RoleUtils } from "@budibase/frontend-core" -import { Roles } from "constants/backend" - -const ROLE_NAMES = { - [Roles.ADMIN]: "App admin", - [Roles.POWER]: "App power user", - [Roles.BASIC]: "App user", - [Roles.PUBLIC]: "Public user", -} export function createRolesStore() { const { subscribe, update, set } = writable([]) @@ -25,16 +17,7 @@ export function createRolesStore() { const actions = { fetch: async () => { - let roles = await API.getRoles() - - // Update labels - for (let [roleId, name] of Object.entries(ROLE_NAMES)) { - const idx = roles.findIndex(x => x._id === roleId) - if (idx !== -1) { - roles[idx].name = name - } - } - + const roles = await API.getRoles() setRoles(roles) }, fetchByAppId: async appId => { @@ -51,7 +34,13 @@ export function createRolesStore() { save: async role => { const savedRole = await API.saveRole(role) await actions.fetch() - return savedRole + + // When saving a role we get back an _id prefixed by role_, but the API does not want this + // in future requests + return { + ...savedRole, + _id: savedRole._id.replace("role_", ""), + } }, } diff --git a/packages/frontend-core/src/utils/roles.js b/packages/frontend-core/src/utils/roles.js index 1ae9d3ac14..913d452e7c 100644 --- a/packages/frontend-core/src/utils/roles.js +++ b/packages/frontend-core/src/utils/roles.js @@ -7,20 +7,7 @@ const RolePriorities = { [Roles.BASIC]: 2, [Roles.PUBLIC]: 1, } -const RoleColours = { - [Roles.ADMIN]: "var(--spectrum-global-color-static-red-400)", - [Roles.CREATOR]: "var(--spectrum-global-color-static-magenta-600)", - [Roles.POWER]: "var(--spectrum-global-color-static-orange-400)", - [Roles.BASIC]: "var(--spectrum-global-color-static-green-400)", - [Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)", -} export const getRolePriority = role => { return RolePriorities[role] ?? 0 } - -export const getRoleColour = roleId => { - return ( - RoleColours[roleId] ?? "var(--spectrum-global-color-static-magenta-400)" - ) -} diff --git a/packages/server/src/api/controllers/role.ts b/packages/server/src/api/controllers/role.ts index 3398c8102c..9c7fb2859a 100644 --- a/packages/server/src/api/controllers/role.ts +++ b/packages/server/src/api/controllers/role.ts @@ -62,7 +62,16 @@ export async function find(ctx: UserCtx) { export async function save(ctx: UserCtx) { const db = context.getAppDB() - let { _id, name, inherits, permissionId, version } = ctx.request.body + let { + _id, + name, + displayName, + description, + color, + inherits, + permissionId, + version, + } = ctx.request.body let isCreate = false const isNewVersion = version === roles.RoleIDVersion.NAME @@ -88,7 +97,13 @@ export async function save(ctx: UserCtx) { ctx.throw(400, "Cannot change custom role name") } - const role = new roles.Role(_id, name, permissionId).addInheritance(inherits) + const role = new roles.Role( + _id, + displayName || name, + description || "Custom role", + color || "var(--spectrum-global-color-static-magenta-400)", + permissionId + ).addInheritance(inherits) if (ctx.request.body._rev) { role._rev = ctx.request.body._rev } diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index 5e2a585b4a..dd943f1282 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -208,6 +208,9 @@ export function roleValidator() { name: Joi.string() .regex(/^[a-zA-Z0-9_]*$/) .required(), + displayName: Joi.string().optional(), + color: Joi.string().optional(), + description: Joi.string().optional(), // this is the base permission ID (for now a built in) permissionId: Joi.string() .valid(...Object.values(permissions.BuiltinPermissionID)) diff --git a/packages/types/src/api/web/role.ts b/packages/types/src/api/web/role.ts index c37dee60e0..7782c33215 100644 --- a/packages/types/src/api/web/role.ts +++ b/packages/types/src/api/web/role.ts @@ -4,6 +4,9 @@ export interface SaveRoleRequest { _id?: string _rev?: string name: string + displayName?: string + color?: string + description?: string inherits: string permissionId: string version: string diff --git a/packages/types/src/documents/app/role.ts b/packages/types/src/documents/app/role.ts index f32ba810b0..0169da90c2 100644 --- a/packages/types/src/documents/app/role.ts +++ b/packages/types/src/documents/app/role.ts @@ -6,4 +6,7 @@ export interface Role extends Document { permissions: { [key: string]: string[] } version?: string name: string + displayName?: string + color?: string + description?: string }