Merge branch 'master' of github.com:Budibase/budibase into develop

This commit is contained in:
mike12345567 2022-08-04 21:35:55 +01:00
commit 44a6aa85a9
69 changed files with 783 additions and 614 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "1.1.33-alpha.4", "version": "1.2.12",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/types": "1.1.33-alpha.4", "@budibase/types": "^1.2.12",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",

View File

@ -19,6 +19,8 @@ const {
csrf, csrf,
internalApi, internalApi,
adminOnly, adminOnly,
builderOnly,
builderOrAdmin,
joiValidator, joiValidator,
} = require("./middleware") } = require("./middleware")
@ -176,5 +178,7 @@ module.exports = {
updateUserOAuth, updateUserOAuth,
ssoCallbackUrl, ssoCallbackUrl,
adminOnly, adminOnly,
builderOnly,
builderOrAdmin,
joiValidator, joiValidator,
} }

View File

@ -55,6 +55,7 @@ const env = {
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase", SERVICE: process.env.SERVICE || "budibase",
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false, MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
LOG_LEVEL: process.env.LOG_LEVEL,
DEPLOYMENT_ENVIRONMENT: DEPLOYMENT_ENVIRONMENT:
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
_set(key: any, value: any) { _set(key: any, value: any) {

View File

@ -15,6 +15,7 @@ import auth from "./auth"
import constants from "./constants" import constants from "./constants"
import * as dbConstants from "./db/constants" import * as dbConstants from "./db/constants"
import logging from "./logging" import logging from "./logging"
import pino from "./pino"
// mimic the outer package exports // mimic the outer package exports
import * as db from "./pkg/db" import * as db from "./pkg/db"
@ -53,6 +54,7 @@ const core = {
errors, errors,
logging, logging,
roles, roles,
...pino,
...errorClasses, ...errorClasses,
} }

View File

@ -81,7 +81,7 @@ module.exports = (
const session = await getSession(userId, sessionId) const session = await getSession(userId, sessionId)
if (!session) { if (!session) {
error = "No session found" error = `Session not found - ${userId} - ${sessionId}`
} else { } else {
try { try {
if (opts && opts.populateUser) { if (opts && opts.populateUser) {

View File

@ -10,6 +10,8 @@ const internalApi = require("./internalApi")
const datasourceGoogle = require("./passport/datasource/google") const datasourceGoogle = require("./passport/datasource/google")
const csrf = require("./csrf") const csrf = require("./csrf")
const adminOnly = require("./adminOnly") const adminOnly = require("./adminOnly")
const builderOrAdmin = require("./builderOrAdmin")
const builderOnly = require("./builderOnly")
const joiValidator = require("./joi-validator") const joiValidator = require("./joi-validator")
module.exports = { module.exports = {
google, google,
@ -27,5 +29,7 @@ module.exports = {
}, },
csrf, csrf,
adminOnly, adminOnly,
builderOnly,
builderOrAdmin,
joiValidator, joiValidator,
} }

View File

@ -37,4 +37,8 @@ export const DEFINITIONS: MigrationDefinition[] = [
type: MigrationType.INSTALLATION, type: MigrationType.INSTALLATION,
name: MigrationName.EVENT_INSTALLATION_BACKFILL, name: MigrationName.EVENT_INSTALLATION_BACKFILL,
}, },
{
type: MigrationType.GLOBAL,
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
},
] ]

View File

@ -0,0 +1,11 @@
const env = require("./environment")
exports.pinoSettings = () => ({
prettyPrint: {
levelFirst: true,
},
level: env.LOG_LEVEL || "error",
autoLogging: {
ignore: req => req.url.includes("/health"),
},
})

View File

@ -203,15 +203,24 @@ exports.getAllRoles = async appId => {
if (appId) { if (appId) {
return doWithDB(appId, internal) return doWithDB(appId, internal)
} else { } else {
return internal(getAppDB()) let appDB
try {
appDB = getAppDB()
} catch (error) {
// We don't have any apps, so we'll just use the built-in roles
}
return internal(appDB)
} }
async function internal(db) { async function internal(db) {
const body = await db.allDocs( let roles = []
getRoleParams(null, { if (db) {
include_docs: true, const body = await db.allDocs(
}) getRoleParams(null, {
) include_docs: true,
let roles = body.rows.map(row => row.doc) })
)
roles = body.rows.map(row => row.doc)
}
const builtinRoles = exports.getBuiltinRoles() const builtinRoles = exports.getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions) // need to combine builtin with any DB record of them (for sake of permissions)

View File

@ -1,5 +1,7 @@
const redis = require("../redis/init") const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid") const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging")
const env = require("../environment")
// a week in seconds // a week in seconds
const EXPIRY_SECONDS = 86400 * 7 const EXPIRY_SECONDS = 86400 * 7
@ -33,12 +35,21 @@ async function invalidateSessions(userId, sessionIds = null) {
})) }))
} }
const client = await redis.getSessionClient() if (sessions && sessions.length > 0) {
const promises = [] const client = await redis.getSessionClient()
for (let session of sessions) { const promises = []
promises.push(client.delete(session.key)) for (let session of sessions) {
promises.push(client.delete(session.key))
}
if (!env.isTest()) {
logWarn(
`Invalidating sessions for ${userId} - ${sessions
.map(session => session.key)
.join(", ")}`
)
}
await Promise.all(promises)
} }
await Promise.all(promises)
} catch (err) { } catch (err) {
console.error(`Error invalidating sessions: ${err}`) console.error(`Error invalidating sessions: ${err}`)
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "1.1.33-alpha.4", "@budibase/string-templates": "^1.2.12",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -15,7 +15,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let error = null export let error = null
export let secondaryOptions = [] export let secondaryOptions = []
@ -35,6 +34,7 @@
export let isOptionSelected = () => false export let isOptionSelected = () => false
export let isPlaceholder = false export let isPlaceholder = false
export let placeholderOption = null export let placeholderOption = null
export let showClearIcon = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let primaryOpen = false let primaryOpen = false
@ -50,17 +50,11 @@
} }
const updateValue = newValue => { const updateValue = newValue => {
if (readonly) {
return
}
dispatch("change", newValue) dispatch("change", newValue)
} }
const onClickSecondary = () => { const onClickSecondary = () => {
dispatch("click") dispatch("click")
if (readonly) {
return
}
secondaryOpen = true secondaryOpen = true
} }
@ -80,24 +74,15 @@
} }
const onBlur = event => { const onBlur = event => {
if (readonly) {
return
}
focus = false focus = false
updateValue(event.target.value) updateValue(event.target.value)
} }
const onInput = event => { const onInput = event => {
if (readonly || !updateOnChange) {
return
}
updateValue(event.target.value) updateValue(event.target.value)
} }
const updateValueOnEnter = event => { const updateValueOnEnter = event => {
if (readonly) {
return
}
if (event.key === "Enter") { if (event.key === "Enter") {
updateValue(event.target.value) updateValue(event.target.value)
} }
@ -140,11 +125,12 @@
value={primaryLabel || ""} value={primaryLabel || ""}
placeholder={placeholder || ""} placeholder={placeholder || ""}
{disabled} {disabled}
{readonly} readonly
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"
class:labelPadding={iconData} class:labelPadding={iconData}
class:open={primaryOpen}
/> />
{#if primaryValue} {#if primaryValue && showClearIcon}
<button <button
on:click={() => onClearPrimary()} on:click={() => onClearPrimary()}
type="reset" type="reset"
@ -198,7 +184,7 @@
</li> </li>
{/if} {/if}
{#each groupTitles as title} {#each groupTitles as title}
<div class="spectrum-Menu-item"> <div class="spectrum-Menu-item title">
<Detail>{title}</Detail> <Detail>{title}</Detail>
</div> </div>
{#if primaryOptions} {#if primaryOptions}
@ -433,4 +419,18 @@
.spectrum-Search-clearButton { .spectrum-Search-clearButton {
position: absolute; position: absolute;
} }
/* Fix focus borders to show only when opened */
.spectrum-Textfield-input {
border-color: var(--spectrum-global-color-gray-400) !important;
border-right-width: 1px;
}
.spectrum-Textfield-input.open {
border-color: var(--spectrum-global-color-blue-400) !important;
}
/* Fix being able to hover and select titles */
.spectrum-Menu-item.title {
pointer-events: none;
}
</style> </style>

View File

@ -27,6 +27,7 @@
export let primaryOptions = [] export let primaryOptions = []
export let secondaryOptions = [] export let secondaryOptions = []
export let searchTerm export let searchTerm
export let showClearIcon = true
let primaryLabel let primaryLabel
let secondaryLabel let secondaryLabel
@ -120,6 +121,7 @@
{secondaryValue} {secondaryValue}
{primaryLabel} {primaryLabel}
{secondaryLabel} {secondaryLabel}
{showClearIcon}
on:pickprimary={onPickPrimary} on:pickprimary={onPickPrimary}
on:picksecondary={onPickSecondary} on:picksecondary={onPickSecondary}
on:search={updateSearchTerm} on:search={updateSearchTerm}

View File

@ -9,11 +9,12 @@
export let avatar = false export let avatar = false
export let title = null export let title = null
export let subtitle = null export let subtitle = null
export let hoverable = false
$: initials = avatar ? title?.[0] : null $: initials = avatar ? title?.[0] : null
</script> </script>
<div class="list-item"> <div class="list-item" class:hoverable on:click>
<div class="left"> <div class="left">
{#if icon} {#if icon}
<div class="icon" style="background: {iconBackground || `transparent`};"> <div class="icon" style="background: {iconBackground || `transparent`};">
@ -39,11 +40,12 @@
.list-item { .list-item {
padding: 0 16px; padding: 0 16px;
height: 56px; height: 56px;
background: var(--spectrum-alias-background-color-tertiary); background: var(--spectrum-global-color-gray-50);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
transition: background 130ms ease-out;
} }
.list-item:not(:first-child) { .list-item:not(:first-child) {
border-top: none; border-top: none;
@ -56,6 +58,10 @@
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.hoverable:hover {
cursor: pointer;
background: var(--spectrum-global-color-gray-75);
}
.left, .left,
.right { .right {
display: flex; display: flex;

View File

@ -106,7 +106,9 @@
{/if} {/if}
{#if showCancelButton} {#if showCancelButton}
<Button group secondary on:click={close}>{cancelText}</Button> <Button group secondary newStyles on:click={close}>
{cancelText}
</Button>
{/if} {/if}
{#if showConfirmButton} {#if showConfirmButton}
<span class="confirm-wrap"> <span class="confirm-wrap">

View File

@ -503,12 +503,6 @@
.spectrum-Table-headCell--alignRight { .spectrum-Table-headCell--alignRight {
justify-content: flex-end; justify-content: flex-end;
} }
.spectrum-Table-headCell--divider {
padding-right: var(--cell-padding);
}
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
padding-left: var(--cell-padding);
}
.spectrum-Table-headCell--edit { .spectrum-Table-headCell--edit {
position: sticky; position: sticky;
left: 0; left: 0;
@ -580,13 +574,6 @@
background-color: var(--table-bg); background-color: var(--table-bg);
z-index: auto; z-index: auto;
} }
.spectrum-Table-cell--divider {
padding-right: var(--cell-padding);
}
.spectrum-Table-cell--divider + .spectrum-Table-cell {
padding-left: var(--cell-padding);
}
.spectrum-Table-cell--edit { .spectrum-Table-cell--edit {
position: sticky; position: sticky;
left: 0; left: 0;

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -69,10 +69,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "1.1.33-alpha.4", "@budibase/bbui": "^1.2.12",
"@budibase/client": "1.1.33-alpha.4", "@budibase/client": "^1.2.12",
"@budibase/frontend-core": "1.1.33-alpha.4", "@budibase/frontend-core": "^1.2.12",
"@budibase/string-templates": "1.1.33-alpha.4", "@budibase/string-templates": "^1.2.12",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -14,7 +14,13 @@
import Table from "./Table.svelte" import Table from "./Table.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import CreateEditRow from "./modals/CreateEditRow.svelte" import CreateEditRow from "./modals/CreateEditRow.svelte"
import { Pagination, Heading, Body, Layout } from "@budibase/bbui" import {
Pagination,
Heading,
Body,
Layout,
notifications,
} from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
@ -29,6 +35,13 @@
$: fetch = createFetch(id) $: fetch = createFetch(id)
$: hasCols = checkHasCols(schema) $: hasCols = checkHasCols(schema)
$: hasRows = !!$fetch.rows?.length $: hasRows = !!$fetch.rows?.length
$: showError($fetch.error)
const showError = error => {
if (error) {
notifications.error(error?.message || "Unable to fetch data.")
}
}
const enrichSchema = schema => { const enrichSchema = schema => {
let tempSchema = { ...schema } let tempSchema = { ...schema }

View File

@ -5,6 +5,7 @@
export let selectedRows export let selectedRows
export let deleteRows export let deleteRows
export let item = "row"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let modal let modal
@ -14,12 +15,14 @@
modal?.hide() modal?.hide()
dispatch("updaterows") dispatch("updaterows")
} }
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
</script> </script>
<Button icon="Delete" size="s" primary quiet on:click={modal.show}> <Button icon="Delete" size="s" primary quiet on:click={modal.show}>
Delete Delete
{selectedRows.length} {selectedRows.length}
row(s) {text}
</Button> </Button>
<ConfirmDialog <ConfirmDialog
bind:this={modal} bind:this={modal}
@ -29,5 +32,5 @@
> >
Are you sure you want to delete Are you sure you want to delete
{selectedRows.length} {selectedRows.length}
row{selectedRows.length > 1 ? "s" : ""}? {text}?
</ConfirmDialog> </ConfirmDialog>

View File

@ -27,7 +27,6 @@
import { AppStatus } from "constants" import { AppStatus } from "constants"
import Logo from "assets/bb-space-man.svg" import Logo from "assets/bb-space-man.svg"
import AccessFilter from "./_components/AcessFilter.svelte" import AccessFilter from "./_components/AcessFilter.svelte"
import { Constants } from "@budibase/frontend-core"
let sortBy = "name" let sortBy = "name"
let template let template
@ -69,10 +68,6 @@
$: unlocked = lockedApps?.length === 0 $: unlocked = lockedApps?.length === 0
$: automationErrors = getAutomationErrors(enrichedApps) $: automationErrors = getAutomationErrors(enrichedApps)
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
const enrichApps = (apps, user, sortBy) => { const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({ const enrichedApps = apps.map(app => ({
...app, ...app,
@ -360,7 +355,7 @@
</Button> </Button>
{/if} {/if}
<div class="filter"> <div class="filter">
{#if hasGroupsLicense && $groups.length} {#if $auth.groupsEnabled && $groups.length}
<AccessFilter on:change={accessFilterAction} /> <AccessFilter on:change={accessFilterAction} />
{/if} {/if}
<Select <Select

View File

@ -12,7 +12,6 @@
$: wide = $: wide =
$page.path.includes("email/:template") || $page.path.includes("email/:template") ||
($page.path.includes("users") && !$page.path.includes(":userId")) ||
($page.path.includes("groups") && !$page.path.includes(":groupId")) ($page.path.includes("groups") && !$page.path.includes(":groupId"))
</script> </script>

View File

@ -11,7 +11,6 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { groups, auth } from "stores/portal" import { groups, auth } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { Constants } from "@budibase/frontend-core"
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte" import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
import UserGroupsRow from "./_components/UserGroupsRow.svelte" import UserGroupsRow from "./_components/UserGroupsRow.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -27,10 +26,6 @@
let modal let modal
let group = cloneDeep(DefaultGroup) let group = cloneDeep(DefaultGroup)
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
async function deleteGroup(group) { async function deleteGroup(group) {
try { try {
groups.actions.delete(group) groups.actions.delete(group)
@ -54,7 +49,7 @@
onMount(async () => { onMount(async () => {
try { try {
if (hasGroupsLicense) { if ($auth.groupsEnabled) {
await groups.actions.init() await groups.actions.init()
} }
} catch (error) { } catch (error) {
@ -67,7 +62,7 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<div style="display: flex;"> <div style="display: flex;">
<Heading size="M">User groups</Heading> <Heading size="M">User groups</Heading>
{#if !hasGroupsLicense} {#if !$auth.groupsEnabled}
<Tags> <Tags>
<div class="tags"> <div class="tags">
<div class="tag"> <div class="tag">
@ -82,15 +77,15 @@
<div class="align-buttons"> <div class="align-buttons">
<Button <Button
newStyles newStyles
icon={hasGroupsLicense ? "UserGroup" : ""} icon={$auth.groupsEnabled ? "UserGroup" : ""}
cta={hasGroupsLicense} cta={$auth.groupsEnabled}
on:click={hasGroupsLicense on:click={$auth.groupsEnabled
? showCreateGroupModal ? showCreateGroupModal
: window.open("https://budibase.com/pricing/", "_blank")} : window.open("https://budibase.com/pricing/", "_blank")}
> >
{hasGroupsLicense ? "Create user group" : "Upgrade Account"} {$auth.groupsEnabled ? "Create user group" : "Upgrade Account"}
</Button> </Button>
{#if !hasGroupsLicense} {#if !$auth.groupsEnabled}
<Button <Button
newStyles newStyles
secondary secondary
@ -101,7 +96,7 @@
{/if} {/if}
</div> </div>
{#if hasGroupsLicense && $groups.length} {#if $auth.groupsEnabled && $groups.length}
<div class="groupTable"> <div class="groupTable">
{#each $groups as group} {#each $groups as group}
<div> <div>

View File

@ -18,6 +18,7 @@
Select, Select,
Modal, Modal,
notifications, notifications,
Divider,
StatusLight, StatusLight,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -41,18 +42,13 @@
let allAppList = [] let allAppList = []
let user let user
let loaded = false let loaded = false
$: fetchUser(userId)
$: fetchUser(userId)
$: fullName = $userFetch?.data?.firstName $: fullName = $userFetch?.data?.firstName
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName ? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
: "" : ""
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: nameLabel = getNameLabel($userFetch) $: nameLabel = getNameLabel($userFetch)
$: initials = getInitials(nameLabel) $: initials = getInitials(nameLabel)
$: allAppList = $apps $: allAppList = $apps
.filter(x => { .filter(x => {
if ($userFetch.data?.roles) { if ($userFetch.data?.roles) {
@ -85,7 +81,6 @@
return y._id === userId return y._id === userId
}) })
}) })
$: globalRole = $userFetch?.data?.admin?.global $: globalRole = $userFetch?.data?.admin?.global
? "admin" ? "admin"
: $userFetch?.data?.builder?.global : $userFetch?.data?.builder?.global
@ -216,15 +211,14 @@
</script> </script>
{#if loaded} {#if loaded}
<Layout gap="L" noPadding> <Layout gap="XL" noPadding>
<Layout gap="XS" noPadding> <div>
<div> <ActionButton on:click={() => $goto("./")} icon="ArrowLeft">
<ActionButton on:click={() => $goto("./")} size="S" icon="ArrowLeft"> Back
Back </ActionButton>
</ActionButton> </div>
</div>
</Layout> <Layout noPadding gap="M">
<Layout gap="XS" noPadding>
<div class="title"> <div class="title">
<div> <div>
<div style="display: flex;"> <div style="display: flex;">
@ -232,74 +226,83 @@
<div class="subtitle"> <div class="subtitle">
<Heading size="S">{nameLabel}</Heading> <Heading size="S">{nameLabel}</Heading>
{#if nameLabel !== $userFetch?.data?.email} {#if nameLabel !== $userFetch?.data?.email}
<Body size="XS">{$userFetch?.data?.email}</Body> <Body size="S">{$userFetch?.data?.email}</Body>
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
<div>
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />
</span>
<MenuItem on:click={resetPasswordModal.show} icon="Refresh"
>Force Password Reset</MenuItem
>
<MenuItem on:click={deleteModal.show} icon="Delete">Delete</MenuItem
>
</ActionMenu>
</div>
</div>
</Layout>
<Layout gap="S" noPadding>
<div class="fields">
<div class="field">
<Label size="L">First name</Label>
<Input
thin
value={$userFetch?.data?.firstName}
on:blur={updateUserFirstName}
/>
</div>
<div class="field">
<Label size="L">Last name</Label>
<Input
thin
value={$userFetch?.data?.lastName}
on:blur={updateUserLastName}
/>
</div>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id} {#if userId !== $auth.user._id}
<div class="field"> <div>
<Label size="L">Role</Label> <ActionMenu align="right">
<Select <span slot="control">
value={globalRole} <Icon hoverable name="More" />
options={Constants.BbRoles} </span>
on:change={updateUserRole} <MenuItem on:click={resetPasswordModal.show} icon="Refresh">
/> Force password reset
</MenuItem>
<MenuItem on:click={deleteModal.show} icon="Delete">
Delete
</MenuItem>
</ActionMenu>
</div> </div>
{/if} {/if}
</div> </div>
<Divider size="S" />
<Layout noPadding gap="S">
<Heading size="S">Details</Heading>
<div class="fields">
<div class="field">
<Label size="L">Email</Label>
<Input disabled value={$userFetch?.data?.email} />
</div>
<div class="field">
<Label size="L">First name</Label>
<Input
value={$userFetch?.data?.firstName}
on:blur={updateUserFirstName}
/>
</div>
<div class="field">
<Label size="L">Last name</Label>
<Input
value={$userFetch?.data?.lastName}
on:blur={updateUserLastName}
/>
</div>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<div class="field">
<Label size="L">Role</Label>
<Select
value={globalRole}
options={Constants.BudibaseRoleOptions}
on:change={updateUserRole}
/>
</div>
{/if}
</div>
</Layout>
</Layout> </Layout>
{#if hasGroupsLicense} {#if $auth.groupsEnabled}
<!-- User groups --> <!-- User groups -->
<Layout gap="XS" noPadding> <Layout gap="S" noPadding>
<div class="tableTitle"> <div class="tableTitle">
<div> <Heading size="S">User groups</Heading>
<Heading size="XS">User groups</Heading>
<Body size="S">Add or remove this user from user groups</Body>
</div>
<div bind:this={popoverAnchor}> <div bind:this={popoverAnchor}>
<Button on:click={popover.show()} icon="UserGroup" cta> <Button
Add user group on:click={popover.show()}
icon="UserGroup"
secondary
newStyles
>
Add to user group
</Button> </Button>
</div> </div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}> <Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker <UserGroupPicker
key={"name"} key={"name"}
title={"Group"} title={"User group"}
bind:searchTerm bind:searchTerm
bind:selected={selectedGroups} bind:selected={selectedGroups}
bind:filtered={filteredGroups} bind:filtered={filteredGroups}
@ -308,7 +311,6 @@
/> />
</Popover> </Popover>
</div> </div>
<List> <List>
{#if userGroups.length} {#if userGroups.length}
{#each userGroups as group} {#each userGroups as group}
@ -316,13 +318,16 @@
title={group.name} title={group.name}
icon={group.icon} icon={group.icon}
iconBackground={group.color} iconBackground={group.color}
><Icon hoverable
on:click={() => $goto(`../groups/${group._id}`)}
>
<Icon
on:click={removeGroup(group._id)} on:click={removeGroup(group._id)}
hoverable hoverable
size="L" size="S"
name="Close" name="Close"
/></ListItem />
> </ListItem>
{/each} {/each}
{:else} {:else}
<ListItem icon="UserGroup" title="No groups" /> <ListItem icon="UserGroup" title="No groups" />
@ -330,37 +335,28 @@
</List> </List>
</Layout> </Layout>
{/if} {/if}
<!-- User Apps -->
<Layout gap="S" noPadding>
<div class="appsTitle">
<Heading weight="light" size="XS">Apps</Heading>
<div style="margin-top: var(--spacing-xs)">
<Body size="S">Manage apps that this user has been assigned to</Body>
</div>
</div>
<Layout gap="S" noPadding>
<Heading size="S">Apps</Heading>
<List> <List>
{#if allAppList.length} {#if allAppList.length}
{#each allAppList as app} {#each allAppList as app}
<div <ListItem
class="pointer" title={app.name}
on:click={$goto(`../../overview/${app.devId}`)} iconBackground={app?.icon?.color || ""}
icon={app?.icon?.name || "Apps"}
hoverable
on:click={() => $goto(`../../overview/${app.devId}`)}
> >
<ListItem <div class="title ">
title={app.name} <StatusLight
iconBackground={app?.icon?.color || ""} square
icon={app?.icon?.name || "Apps"} color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
> >
<div class="title "> {getRoleLabel(getHighestRole(app.roles))}
<StatusLight </StatusLight>
square </div>
color={RoleUtils.getRoleColour(getHighestRole(app.roles))} </ListItem>
>
{getRoleLabel(getHighestRole(app.roles))}
</StatusLight>
</div>
</ListItem>
</div>
{/each} {/each}
{:else} {:else}
<ListItem icon="Apps" title="No apps" /> <ListItem icon="Apps" title="No apps" />
@ -381,16 +377,13 @@
</Modal> </Modal>
<style> <style>
.pointer {
cursor: pointer;
}
.fields { .fields {
display: grid; display: grid;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
} }
.field { .field {
display: grid; display: grid;
grid-template-columns: 32% 1fr; grid-template-columns: 120px 1fr;
align-items: center; align-items: center;
} }
@ -403,7 +396,7 @@
.tableTitle { .tableTitle {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: var(--spacing-m); align-items: flex-end;
} }
.subtitle { .subtitle {
@ -413,9 +406,4 @@
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
} }
.appsTitle {
display: flex;
flex-direction: column;
}
</style> </style>

View File

@ -13,13 +13,10 @@
import { emailValidator } from "helpers/validation" import { emailValidator } from "helpers/validation"
export let showOnboardingTypeModal export let showOnboardingTypeModal
const password = Math.random().toString(36).substring(2, 22) const password = Math.random().toString(36).substring(2, 22)
let disabled let disabled
let userGroups = [] let userGroups = []
$: errors = []
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: userData = [ $: userData = [
{ {
@ -29,6 +26,7 @@
forceResetPassword: true, forceResetPassword: true,
}, },
] ]
$: hasError = userData.find(x => x.error != null)
function removeInput(idx) { function removeInput(idx) {
userData = userData.filter((e, i) => i !== idx) userData = userData.filter((e, i) => i !== idx)
@ -41,38 +39,49 @@
role: "appUser", role: "appUser",
password: Math.random().toString(36).substring(2, 22), password: Math.random().toString(36).substring(2, 22),
forceResetPassword: true, forceResetPassword: true,
error: null,
}, },
] ]
} }
function validateInput(email, index) { function validateInput(email, index) {
if (email) { if (email) {
if (emailValidator(email) === true) { const res = emailValidator(email)
errors[index] = true if (res === true) {
return null delete userData[index].error
} else { } else {
errors[index] = false userData[index].error = res
return emailValidator(email)
} }
} else {
userData[index].error = "Please enter an email address"
} }
return userData[index].error == null
}
const onConfirm = () => {
let valid = true
userData.forEach((input, index) => {
valid = validateInput(input.email, index) && valid
})
if (!valid) {
return false
}
showOnboardingTypeModal({ users: userData, groups: userGroups })
} }
</script> </script>
<ModalContent <ModalContent
onConfirm={async () => {onConfirm}
showOnboardingTypeModal({ users: userData, groups: userGroups })}
size="M" size="M"
title="Add new user" title="Add new users"
confirmText="Add user" confirmText="Add users"
confirmDisabled={disabled} confirmDisabled={disabled}
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
disabled={errors.some(x => x === false) || disabled={hasError || !userData.length}
userData.some(x => x.email === "" || x.email === null)}
> >
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Label>Email Address</Label> <Label>Email address</Label>
{#each userData as input, index} {#each userData as input, index}
<div <div
style="display: flex; style="display: flex;
@ -84,15 +93,12 @@
inputType="email" inputType="email"
bind:inputValue={input.email} bind:inputValue={input.email}
bind:dropdownValue={input.role} bind:dropdownValue={input.role}
options={Constants.BbRoles} options={Constants.BudibaseRoleOptions}
error={validateInput(input.email, index)} error={input.error}
on:blur={() => validateInput(input.email, index)}
/> />
</div> </div>
<div <div class="icon">
class:fix-height={errors.length && !errors[index]}
class:normal-height={errors.length && !!errors[index]}
style="width: 10% "
>
<Icon <Icon
name="Close" name="Close"
hoverable hoverable
@ -107,11 +113,11 @@
</div> </div>
</Layout> </Layout>
{#if hasGroupsLicense} {#if $auth.groupsEnabled}
<Multiselect <Multiselect
bind:value={userGroups} bind:value={userGroups}
placeholder="Select User Groups" placeholder="No groups"
label="User Groups" label="Groups"
options={$groups} options={$groups}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
getOptionValue={option => option._id} getOptionValue={option => option._id}
@ -120,10 +126,9 @@
</ModalContent> </ModalContent>
<style> <style>
.fix-height { .icon {
margin-bottom: 5%; width: 10%;
} align-self: flex-start;
.normal-height { margin-top: 8px;
margin-bottom: 0%;
} }
</style> </style>

View File

@ -1,5 +1,6 @@
<script> <script>
import { Icon, Body } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
export let value export let value
</script> </script>
@ -7,17 +8,9 @@
<div class="spacing"> <div class="spacing">
<Icon name="UserGroup" /> <Icon name="UserGroup" />
</div> </div>
{#if value?.length === 0} <div class="opacity">
<div class="opacity">0</div> {value?.length || 0}
{:else if value?.length === 1} </div>
<div class="opacity">
<Body size="S">{value[0]?.name}</Body>
</div>
{:else}
<div class="opacity">
{parseInt(value?.length) || 0} groups
</div>
{/if}
</div> </div>
<style> <style>

View File

@ -7,7 +7,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { groups, auth, admin } from "stores/portal" import { groups, auth, admin } from "stores/portal"
import { emailValidator } from "../../../../../../helpers/validation" import { emailValidator } from "helpers/validation"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000
@ -22,9 +22,6 @@
let usersRole = null let usersRole = null
$: invalidEmails = [] $: invalidEmails = []
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
const validEmails = userEmails => { const validEmails = userEmails => {
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) { if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
@ -81,7 +78,7 @@
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })} onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
disabled={!userEmails.length || !validEmails(userEmails) || !usersRole} disabled={!userEmails.length || !validEmails(userEmails) || !usersRole}
> >
<Body size="S">Import your users email addrresses from a CSV</Body> <Body size="S">Import your users email addresses from a CSV file</Body>
<div class="dropzone"> <div class="dropzone">
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} /> <input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
@ -95,11 +92,11 @@
options={Constants.BuilderRoleDescriptions} options={Constants.BuilderRoleDescriptions}
/> />
{#if hasGroupsLicense} {#if $auth.groupsEnabled}
<Multiselect <Multiselect
bind:value={userGroups} bind:value={userGroups}
placeholder="Select User Groups" placeholder="No groups"
label="User Groups" label="Groups"
options={$groups} options={$groups}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
getOptionValue={option => option._id} getOptionValue={option => option._id}
@ -122,14 +119,12 @@
label { label {
font-family: var(--font-sans); font-family: var(--font-sans);
cursor: pointer;
font-weight: 600; font-weight: 600;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
color: var(--ink); color: var(--ink);
padding: var(--spacing-m) var(--spacing-l); padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex; display: inline-flex;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
min-width: auto; min-width: auto;
@ -141,10 +136,15 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
background-color: var(--grey-2); background: var(--spectrum-global-color-gray-200);
font-size: var(--font-size-xs); font-size: 12px;
line-height: normal; line-height: normal;
border: var(--border-transparent); border: var(--border-transparent);
transition: background-color 130ms ease-out;
}
label:hover {
background: var(--spectrum-global-color-gray-300);
cursor: pointer;
} }
input[type="file"] { input[type="file"] {

View File

@ -49,10 +49,10 @@
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
> >
<Body size="XS" <Body size="XS">
>All your new users can be accessed through the autogenerated passwords. All your new users can be accessed through the autogenerated passwords. Take
Make not of these passwords or download the csv</Body note of these passwords or download the CSV file.
> </Body>
<div class="container" on:click={downloadCsvFile}> <div class="container" on:click={downloadCsvFile}>
<div class="inner"> <div class="inner">

View File

@ -3,14 +3,20 @@
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
export let row export let row
$: value =
Constants.BbRoles.find(x => x.value === users.getUserRole(row))?.label || const TooltipMap = {
"Not Available" appUser: "Only has access to published apps",
developer: "Access to the app builder",
admin: "Full access",
}
$: role = Constants.BudibaseRoleOptions.find(
x => x.value === users.getUserRole(row)
)
$: value = role?.label || "Not available"
$: tooltip = TooltipMap[role?.value] || ""
</script> </script>
<div on:click|stopPropagation> <div on:click|stopPropagation title={tooltip}>
{value} {value}
</div> </div>
<style>
</style>

View File

@ -8,11 +8,10 @@
Layout, Layout,
Modal, Modal,
ModalContent, ModalContent,
Icon, Search,
notifications, notifications,
Pagination, Pagination,
Search, Divider,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import AddUserModal from "./_components/AddUserModal.svelte" import AddUserModal from "./_components/AddUserModal.svelte"
import { users, groups, auth } from "stores/portal" import { users, groups, auth } from "stores/portal"
@ -20,68 +19,42 @@
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte" import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte" import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte" import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
import NameTableRenderer from "./_components/NameTableRenderer.svelte"
import RoleTableRenderer from "./_components/RoleTableRenderer.svelte" import RoleTableRenderer from "./_components/RoleTableRenderer.svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte" import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
import PasswordModal from "./_components/PasswordModal.svelte" import PasswordModal from "./_components/PasswordModal.svelte"
import ImportUsersModal from "./_components/ImportUsersModal.svelte" import ImportUsersModal from "./_components/ImportUsersModal.svelte"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { get } from "svelte/store"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
const accessTypes = [
{
icon: "User",
description: "App user - Only has access to published apps",
},
{
icon: "Hammer",
description: "Developer - Access to the app builder",
},
{
icon: "Draw",
description: "Admin - Full access",
},
]
//let email
let enrichedUsers = [] let enrichedUsers = []
let createUserModal, let createUserModal,
inviteConfirmationModal, inviteConfirmationModal,
onboardingTypeModal, onboardingTypeModal,
passwordModal, passwordModal,
importUsersModal importUsersModal
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let prevEmail = undefined, let prevEmail = undefined,
searchEmail = undefined searchEmail = undefined
let selectedRows = [] let selectedRows = []
let customRenderers = [ let customRenderers = [
{ column: "userGroups", component: GroupsTableRenderer }, { column: "userGroups", component: GroupsTableRenderer },
{ column: "apps", component: AppsTableRenderer }, { column: "apps", component: AppsTableRenderer },
{ column: "name", component: NameTableRenderer },
{ column: "role", component: RoleTableRenderer }, { column: "role", component: RoleTableRenderer },
] ]
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: schema = { $: schema = {
name: {},
email: {}, email: {},
role: { role: {
sortable: false, sortable: false,
}, },
...(hasGroupsLicense && { ...($auth.groupsEnabled && {
userGroups: { sortable: false, displayName: "User groups" }, userGroups: { sortable: false, displayName: "Groups" },
}), }),
apps: {}, apps: {},
} }
$: userData = [] $: userData = []
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchUsers(page, searchEmail) $: fetchUsers(page, searchEmail)
$: { $: {
@ -104,6 +77,7 @@
} }
}) })
} }
const showOnboardingTypeModal = async addUsersData => { const showOnboardingTypeModal = async addUsersData => {
userData = await removingDuplicities(addUsersData) userData = await removingDuplicities(addUsersData)
if (!userData?.users?.length) return if (!userData?.users?.length) return
@ -112,13 +86,13 @@
} }
async function createUserFlow() { async function createUserFlow() {
let emails = userData?.users?.map(x => x.email) || [] const payload = userData?.users?.map(user => ({
email: user.email,
builder: user.role === Constants.BudibaseRoles.Developer,
admin: user.role === Constants.BudibaseRoles.Admin,
}))
try { try {
const res = await users.invite({ const res = await users.invite(payload)
emails: emails,
builder: false,
admin: false,
})
notifications.success(res.message) notifications.success(res.message)
inviteConfirmationModal.show() inviteConfirmationModal.show()
} catch (error) { } catch (error) {
@ -198,6 +172,10 @@
const deleteRows = async () => { const deleteRows = async () => {
try { try {
let ids = selectedRows.map(user => user._id) let ids = selectedRows.map(user => user._id)
if (ids.includes(get(auth).user._id)) {
notifications.error("You cannot delete yourself")
return
}
await users.bulkDelete(ids) await users.bulkDelete(ids)
notifications.success(`Successfully deleted ${selectedRows.length} rows`) notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = [] selectedRows = []
@ -227,23 +205,13 @@
} }
</script> </script>
<Layout noPadding> <Layout noPadding gap="M">
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading>Users</Heading> <Heading>Users</Heading>
<Body>Add users and control who gets access to your published apps</Body> <Body>Add users and control who gets access to your published apps</Body>
<div>
{#each accessTypes as type}
<div class="access-description">
<Icon name={type.icon} />
<div class="access-text">
<Body size="S">{type.description}</Body>
</div>
</div>
{/each}
</div>
</Layout> </Layout>
<Layout gap="S" noPadding> <Divider size="S" />
<div class="controls">
<ButtonGroup> <ButtonGroup>
<Button <Button
dataCy="add-user" dataCy="add-user"
@ -251,39 +219,47 @@
icon="UserAdd" icon="UserAdd"
cta>Add users</Button cta>Add users</Button
> >
<Button on:click={importUsersModal.show} icon="Import" primary <Button
>Import users</Button on:click={importUsersModal.show}
icon="Import"
secondary
newStyles
> >
Import users
<div class="field"> </Button>
<Label size="L">Search email</Label>
<Search bind:value={searchEmail} placeholder="" />
</div>
{#if selectedRows.length > 0}
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
{/if}
</ButtonGroup> </ButtonGroup>
<Table <div class="controls-right">
on:click={({ detail }) => $goto(`./${detail._id}`)} <Search bind:value={searchEmail} placeholder="Search email" />
{schema} {#if selectedRows.length > 0}
bind:selectedRows <DeleteRowsButton
data={enrichedUsers} item="user"
allowEditColumns={false} on:updaterows
allowEditRows={false} {selectedRows}
allowSelectRows={true} {deleteRows}
showHeaderBorder={false} />
{customRenderers} {/if}
/>
<div class="pagination">
<Pagination
page={$pageInfo.pageNumber}
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
goToPrevPage={pageInfo.prevPage}
goToNextPage={pageInfo.nextPage}
/>
</div> </div>
</Layout> </div>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
bind:selectedRows
data={enrichedUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={true}
showHeaderBorder={false}
{customRenderers}
/>
<div class="pagination">
<Pagination
page={$pageInfo.pageNumber}
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
goToPrevPage={pageInfo.prevPage}
goToNextPage={pageInfo.nextPage}
/>
</div>
</Layout> </Layout>
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>
@ -320,28 +296,22 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
margin-top: var(--spacing-xl);
} }
.field { .controls {
display: flex; display: flex;
align-items: center;
flex-direction: row; flex-direction: row;
grid-gap: var(--spacing-m); justify-content: space-between;
margin-left: auto; align-items: center;
} }
.controls-right {
.field > :global(*) + :global(*) {
margin-left: var(--spacing-m);
}
.access-description {
display: flex; display: flex;
margin-top: var(--spacing-xl); flex-direction: row;
opacity: 0.8; justify-content: flex-end;
align-items: center;
gap: var(--spacing-xl);
} }
.controls-right :global(.spectrum-Search) {
.access-text { width: 200px;
margin-left: var(--spacing-m);
} }
</style> </style>

View File

@ -17,10 +17,10 @@
import { users, groups, apps, auth } from "stores/portal" import { users, groups, apps, auth } from "stores/portal"
import AssignmentModal from "./AssignmentModal.svelte" import AssignmentModal from "./AssignmentModal.svelte"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { Constants } from "@budibase/frontend-core"
import { roles } from "stores/backend" import { roles } from "stores/backend"
export let app export let app
let assignmentModal let assignmentModal
let appGroups = [] let appGroups = []
let appUsers = [] let appUsers = []
@ -28,14 +28,9 @@
search = undefined search = undefined
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let fixedAppId let fixedAppId
$: page = $pageInfo.page $: page = $pageInfo.page
$: hasGroupsLicense = $auth.user?.license.features.includes(
Constants.Features.USER_GROUPS
)
$: fixedAppId = apps.getProdAppID(app.devId) $: fixedAppId = apps.getProdAppID(app.devId)
$: appGroups = $groups.filter(x => { $: appGroups = $groups.filter(x => {
return x.apps.includes(app.appId) return x.apps.includes(app.appId)
}) })
@ -161,7 +156,7 @@
> >
</div> </div>
</div> </div>
{#if hasGroupsLicense && appGroups.length} {#if $auth.groupsEnabled && appGroups.length}
<List title="User Groups"> <List title="User Groups">
{#each appGroups as group} {#each appGroups as group}
<ListItem <ListItem

View File

@ -4,22 +4,65 @@
PickerDropdown, PickerDropdown,
ActionButton, ActionButton,
Layout, Layout,
Icon,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { groups, users } from "stores/portal" import { groups, users, auth } from "stores/portal"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
export let app export let app
export let addData export let addData
export let appUsers = [] export let appUsers = []
let prevSearch = undefined, let prevSearch = undefined,
search = undefined search = undefined
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let appData = [{ id: "", role: "" }]
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchUsers(page, search) $: fetchUsers(page, search)
$: availableUsers = getAvailableUsers($users, appUsers, appData)
$: filteredGroups = $groups.filter(group => {
return !group.apps.find(appId => {
return appId === app.appId
})
})
$: valid =
appData?.length && !appData?.some(x => !x.id?.length || !x.role?.length)
$: optionSections = {
...($auth.groupsEnabled &&
filteredGroups.length && {
["User groups"]: {
data: filteredGroups,
getLabel: group => group.name,
getValue: group => group._id,
getIcon: group => group.icon,
getColour: group => group.color,
},
}),
users: {
data: availableUsers,
getLabel: user => user.email,
getValue: user => user._id,
getIcon: user => user.icon,
getColour: user => user.color,
},
}
const getAvailableUsers = (allUsers, appUsers, newUsers) => {
return (allUsers.data || []).filter(user => {
// Filter out assigned users
if (appUsers.find(x => x._id === user._id)) {
return false
}
// Filter out new users which are going to be assigned
return !newUsers.find(x => x.id === user._id)
})
}
async function fetchUsers(page, search) { async function fetchUsers(page, search) {
if ($pageInfo.loading) { if ($pageInfo.loading) {
return return
@ -39,36 +82,13 @@
} }
} }
$: filteredGroups = $groups.filter(group => {
return !group.apps.find(appId => {
return appId === app.appId
})
})
$: optionSections = {
...(filteredGroups.length && {
groups: {
data: filteredGroups,
getLabel: group => group.name,
getValue: group => group._id,
getIcon: group => group.icon,
getColour: group => group.color,
},
}),
users: {
data: $users.data.filter(u => !appUsers.find(x => x._id === u._id)),
getLabel: user => user.email,
getValue: user => user._id,
getIcon: user => user.icon,
getColour: user => user.color,
},
}
$: appData = [{ id: "", role: "" }]
function addNewInput() { function addNewInput() {
appData = [...appData, { id: "", role: "" }] appData = [...appData, { id: "", role: "" }]
} }
const removeItem = index => {
appData = appData.filter((x, idx) => idx !== index)
}
</script> </script>
<ModalContent <ModalContent
@ -78,28 +98,61 @@
cancelText="Cancel" cancelText="Cancel"
onConfirm={() => addData(appData)} onConfirm={() => addData(appData)}
showCloseIcon={false} showCloseIcon={false}
disabled={!valid}
> >
<Layout noPadding gap="XS"> {#if appData?.length}
{#each appData as input, index} <Layout noPadding gap="XS">
<PickerDropdown {#each appData as input, index}
autocomplete <div class="item">
primaryOptions={optionSections} <div class="picker">
secondaryOptions={$roles} <PickerDropdown
secondaryPlaceholder="Access" autocomplete
bind:primaryValue={input.id} showClearIcon={false}
bind:secondaryValue={input.role} primaryOptions={optionSections}
bind:searchTerm={search} secondaryOptions={$roles}
getPrimaryOptionLabel={group => group.name} secondaryPlaceholder="Access"
getPrimaryOptionValue={group => group.name} bind:primaryValue={input.id}
getPrimaryOptionIcon={group => group.icon} bind:secondaryValue={input.role}
getPrimaryOptionColour={group => group.colour} bind:searchTerm={search}
getSecondaryOptionLabel={role => role.name} getPrimaryOptionLabel={group => group.name}
getSecondaryOptionValue={role => role._id} getPrimaryOptionValue={group => group.name}
getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)} getPrimaryOptionIcon={group => group.icon}
/> getPrimaryOptionColour={group => group.colour}
{/each} getSecondaryOptionLabel={role => role.name}
</Layout> getSecondaryOptionValue={role => role._id}
getSecondaryOptionColour={role =>
RoleUtils.getRoleColour(role._id)}
/>
</div>
<div class="icon">
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeItem(index)}
/>
</div>
</div>
{/each}
</Layout>
{/if}
<div> <div>
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton> <ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
</div> </div>
</ModalContent> </ModalContent>
<style>
.item {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.picker {
width: calc(100% - 30px);
}
.icon {
width: 20px;
}
</style>

View File

@ -2,6 +2,8 @@ import { derived, writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { admin } from "stores/portal" import { admin } from "stores/portal"
import analytics from "analytics" import analytics from "analytics"
import { FEATURE_FLAGS } from "helpers/featureFlags"
import { Constants } from "@budibase/frontend-core"
export function createAuthStore() { export function createAuthStore() {
const auth = writable({ const auth = writable({
@ -10,11 +12,13 @@ export function createAuthStore() {
tenantSet: false, tenantSet: false,
loaded: false, loaded: false,
postLogout: false, postLogout: false,
groupsEnabled: false,
}) })
const store = derived(auth, $store => { const store = derived(auth, $store => {
let initials = null let initials = null
let isAdmin = false let isAdmin = false
let isBuilder = false let isBuilder = false
let groupsEnabled = false
if ($store.user) { if ($store.user) {
const user = $store.user const user = $store.user
if (user.firstName) { if (user.firstName) {
@ -29,6 +33,9 @@ export function createAuthStore() {
} }
isAdmin = !!user.admin?.global isAdmin = !!user.admin?.global
isBuilder = !!user.builder?.global isBuilder = !!user.builder?.global
groupsEnabled =
user?.license.features.includes(Constants.Features.USER_GROUPS) &&
user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS)
} }
return { return {
user: $store.user, user: $store.user,
@ -39,6 +46,7 @@ export function createAuthStore() {
initials, initials,
isAdmin, isAdmin,
isBuilder, isBuilder,
groupsEnabled,
} }
}) })

View File

@ -26,12 +26,8 @@ export function createUsersStore() {
return await API.getUsers() return await API.getUsers()
} }
async function invite({ emails, builder, admin }) { async function invite(payload) {
return API.inviteUsers({ return API.inviteUsers(payload)
emails,
builder,
admin,
})
} }
async function acceptInvite(inviteCode, password) { async function acceptInvite(inviteCode, password) {
return API.acceptInvite({ return API.acceptInvite({

View File

@ -5,3 +5,4 @@ build/
docker-error.log docker-error.log
envoy.yaml envoy.yaml
*.tar.gz *.tar.gz
prebuilds/

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "1.1.33-alpha.4", "@budibase/bbui": "^1.2.12",
"@budibase/frontend-core": "1.1.33-alpha.4", "@budibase/frontend-core": "^1.2.12",
"@budibase/string-templates": "1.1.33-alpha.4", "@budibase/string-templates": "^1.2.12",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "1.1.33-alpha.4", "@budibase/bbui": "^1.2.12",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -141,20 +141,18 @@ export const buildUserEndpoints = API => ({
/** /**
* Invites multiple users to the current tenant. * Invites multiple users to the current tenant.
* @param email An array of email addresses * @param users An array of users to invite
* @param builder whether the user should be a global builder
* @param admin whether the user should be a global admin
*/ */
inviteUsers: async ({ emails, builder, admin }) => { inviteUsers: async users => {
return await API.post({ return await API.post({
url: "/api/global/users/inviteMultiple", url: "/api/global/users/multi/invite",
body: { body: users.map(user => ({
emails, email: user.email,
userInfo: { userInfo: {
admin: admin ? { global: true } : undefined, admin: user.admin ? { global: true } : undefined,
builder: builder ? { global: true } : undefined, builder: user.admin || user.builder ? { global: true } : undefined,
}, },
}, })),
}) })
}, },

View File

@ -60,25 +60,31 @@ export const TableNames = {
USERS: "ta_users", USERS: "ta_users",
} }
export const BbRoles = [ export const BudibaseRoles = {
{ label: "App User", value: "appUser" }, AppUser: "appUser",
{ label: "Developer", value: "developer" }, Developer: "developer",
{ label: "Admin", value: "admin" }, Admin: "admin",
}
export const BudibaseRoleOptions = [
{ label: "App User", value: BudibaseRoles.AppUser },
{ label: "Developer", value: BudibaseRoles.Developer },
{ label: "Admin", value: BudibaseRoles.Admin },
] ]
export const BuilderRoleDescriptions = [ export const BuilderRoleDescriptions = [
{ {
value: "appUser", value: BudibaseRoles.AppUser,
icon: "User", icon: "User",
label: "App user - Only has access to published apps", label: "App user - Only has access to published apps",
}, },
{ {
value: "developer", value: BudibaseRoles.Developer,
icon: "Hammer", icon: "Hammer",
label: "Developer - Access to the app builder", label: "Developer - Access to the app builder",
}, },
{ {
value: "admin", value: BudibaseRoles.Admin,
icon: "Draw", icon: "Draw",
label: "Admin - Full access", label: "Admin - Full access",
}, },

View File

@ -170,6 +170,7 @@ export default class DataFetch {
rows: page.rows, rows: page.rows,
info: page.info, info: page.info,
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null], cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
error: page.error,
})) }))
} }
@ -182,7 +183,7 @@ export default class DataFetch {
const features = get(this.featureStore) const features = get(this.featureStore)
// Get the actual data // Get the actual data
let { rows, info, hasNextPage, cursor } = await this.getData() let { rows, info, hasNextPage, cursor, error } = await this.getData()
// If we don't support searching, do a client search // If we don't support searching, do a client search
if (!features.supportsSearch) { if (!features.supportsSearch) {
@ -204,6 +205,7 @@ export default class DataFetch {
info, info,
hasNextPage, hasNextPage,
cursor, cursor,
error,
} }
} }
@ -345,8 +347,14 @@ export default class DataFetch {
return return
} }
this.store.update($store => ({ ...$store, loading: true })) this.store.update($store => ({ ...$store, loading: true }))
const { rows, info } = await this.getPage() const { rows, info, error } = await this.getPage()
this.store.update($store => ({ ...$store, rows, info, loading: false })) this.store.update($store => ({
...$store,
rows,
info,
loading: false,
error,
}))
} }
/** /**
@ -386,7 +394,7 @@ export default class DataFetch {
cursor: nextCursor, cursor: nextCursor,
pageNumber: $store.pageNumber + 1, pageNumber: $store.pageNumber + 1,
})) }))
const { rows, info, hasNextPage, cursor } = await this.getPage() const { rows, info, hasNextPage, cursor, error } = await this.getPage()
// Update state // Update state
this.store.update($store => { this.store.update($store => {
@ -400,6 +408,7 @@ export default class DataFetch {
info, info,
cursors, cursors,
loading: false, loading: false,
error,
} }
}) })
} }
@ -421,7 +430,7 @@ export default class DataFetch {
cursor: prevCursor, cursor: prevCursor,
pageNumber: $store.pageNumber - 1, pageNumber: $store.pageNumber - 1,
})) }))
const { rows, info } = await this.getPage() const { rows, info, error } = await this.getPage()
// Update state // Update state
this.store.update($store => { this.store.update($store => {
@ -430,6 +439,7 @@ export default class DataFetch {
rows, rows,
info, info,
loading: false, loading: false,
error,
} }
}) })
} }

View File

@ -37,6 +37,7 @@ export default class TableFetch extends DataFetch {
return { return {
rows: [], rows: [],
hasNextPage: false, hasNextPage: false,
error,
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -77,11 +77,11 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.3", "@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "1.1.33-alpha.4", "@budibase/backend-core": "^1.2.12",
"@budibase/client": "1.1.33-alpha.4", "@budibase/client": "^1.2.12",
"@budibase/pro": "1.1.33-alpha.4", "@budibase/pro": "1.2.12",
"@budibase/string-templates": "1.1.33-alpha.4", "@budibase/string-templates": "^1.2.12",
"@budibase/types": "1.1.33-alpha.4", "@budibase/types": "^1.2.12",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -128,25 +128,35 @@ exports.search = async ctx => {
[params.sort]: direction, [params.sort]: direction,
} }
} }
const rows = await handleRequest(DataSourceOperation.READ, tableId, { try {
filters: query, const rows = await handleRequest(DataSourceOperation.READ, tableId, {
sort,
paginate: paginateObj,
})
let hasNextPage = false
if (paginate && rows.length === limit) {
const nextRows = await handleRequest(DataSourceOperation.READ, tableId, {
filters: query, filters: query,
sort, sort,
paginate: { paginate: paginateObj,
limit: 1,
page: bookmark * limit + 1,
},
}) })
hasNextPage = nextRows.length > 0 let hasNextPage = false
if (paginate && rows.length === limit) {
const nextRows = await handleRequest(DataSourceOperation.READ, tableId, {
filters: query,
sort,
paginate: {
limit: 1,
page: bookmark * limit + 1,
},
})
hasNextPage = nextRows.length > 0
}
// need wrapper object for bookmarks etc when paginating
return { rows, hasNextPage, bookmark: bookmark + 1 }
} catch (err) {
if (err.message && err.message.includes("does not exist")) {
throw new Error(
`Table updated externally, please re-fetch - ${err.message}`
)
} else {
throw err
}
} }
// need wrapper object for bookmarks etc when paginating
return { rows, hasNextPage, bookmark: bookmark + 1 }
} }
exports.validate = async () => { exports.validate = async () => {

View File

@ -15,6 +15,7 @@ const Sentry = require("@sentry/node")
const fileSystem = require("./utilities/fileSystem") const fileSystem = require("./utilities/fileSystem")
const bullboard = require("./automations/bullboard") const bullboard = require("./automations/bullboard")
const { logAlert } = require("@budibase/backend-core/logging") const { logAlert } = require("@budibase/backend-core/logging")
const { pinoSettings } = require("@budibase/backend-core")
const { Thread } = require("./threads") const { Thread } = require("./threads")
import redis from "./utilities/redis" import redis from "./utilities/redis"
import * as migrations from "./migrations" import * as migrations from "./migrations"
@ -35,14 +36,7 @@ app.use(
}) })
) )
app.use( app.use(pino(pinoSettings()))
pino({
prettyPrint: {
levelFirst: true,
},
level: env.LOG_LEVEL || "error",
})
)
if (!env.isTest()) { if (!env.isTest()) {
const plugin = bullboard.init() const plugin = bullboard.init()

View File

@ -224,8 +224,9 @@ function shouldCopySpecialColumn(
FieldTypes.ARRAY, FieldTypes.ARRAY,
FieldTypes.FORMULA, FieldTypes.FORMULA,
] ]
// column has been deleted, remove
if (column && !fetchedColumn) { if (column && !fetchedColumn) {
return true return false
} }
const fetchedIsNumber = const fetchedIsNumber =
!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER !fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER

View File

@ -1094,19 +1094,18 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.1.33-alpha.4": "@budibase/backend-core@1.2.12":
version "1.1.33-alpha.4" version "1.2.12"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.1.33-alpha.4.tgz#207ffe45d41535e59ccc21cca9892d1e41818a14" resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.12.tgz#a09ef7c4b7d1217b65e385199f2d044d1f30f450"
integrity sha512-p8SZkODBF4+BhfIYWIkUtJhR04OjvkmkrVTSFWXv2NTkIbSpaJGTkx9Kao+1Dn4N3H4jU4OBdmScy+C8F5MeSw== integrity sha512-o6tsX9bfpMLfAEfxBI4StC6Lvt1PkJ6b0JYlgp8QiXa0WxZX7np24cOxt2fWrP3ASIMDCrzLn2e3k3eOnzvh8w==
dependencies: dependencies:
"@budibase/types" "1.1.33-alpha.4" "@budibase/types" "^1.2.12"
"@techpass/passport-openidconnect" "0.3.2" "@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0" aws-sdk "2.1030.0"
bcrypt "5.0.1" bcrypt "5.0.1"
dotenv "16.0.1" dotenv "16.0.1"
emitter-listener "1.1.2" emitter-listener "1.1.2"
ioredis "4.28.0" ioredis "4.28.0"
joi "17.6.0"
jsonwebtoken "8.5.1" jsonwebtoken "8.5.1"
koa-passport "4.1.4" koa-passport "4.1.4"
lodash "4.17.21" lodash "4.17.21"
@ -1178,13 +1177,13 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/pro@1.1.33-alpha.4": "@budibase/pro@1.2.12":
version "1.1.33-alpha.4" version "1.2.12"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.1.33-alpha.4.tgz#d25bc2ca73d11adfdc659e324b1e8de31c17657a" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.12.tgz#252f8f1c6730a3395d9d340f2e843052bf0293bb"
integrity sha512-CQ3zVbom4ndzIfUznUSERQ4Bz6ZVuy4HbOYGKKkU/FjoWqrYRK1tqlhmfCNQy8P9rnKURCUf3PMoWVWSOAS24g== integrity sha512-1zhMMVBCX+VX/ILPlSbI7tdsQLcrxf1W29IQD4W55AbevlFnpQs4qNVveIdXFm+GWvCJbdN5I26CXBOftbVUhA==
dependencies: dependencies:
"@budibase/backend-core" "1.1.33-alpha.4" "@budibase/backend-core" "1.2.12"
"@budibase/types" "1.1.33-alpha.4" "@budibase/types" "1.2.12"
"@koa/router" "8.0.8" "@koa/router" "8.0.8"
joi "17.6.0" joi "17.6.0"
node-fetch "^2.6.1" node-fetch "^2.6.1"
@ -1207,10 +1206,10 @@
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/types@1.1.33-alpha.4": "@budibase/types@1.2.12", "@budibase/types@^1.2.12":
version "1.1.33-alpha.4" version "1.2.12"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.1.33-alpha.4.tgz#a8de79c385280389be8b2cc214185caddf5fe4d3" resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.12.tgz#c460d1b39116538e47e00178116ad066aa6a6f1c"
integrity sha512-od/gbLgbJnHsVlCvBQkuJf3t/Y9VLUNRYPl3Y4IbNOylpj3rSOKVGF3jANQgkI+pOBt5ni3Xlhc7aOI3qAning== integrity sha512-EeWadHUzeLx4X27Pv6XWlpSXbeLIMvg7r+Q52kYyOZFkmjtdCNFQW5PCu1bYUw9L1Xa64t7fvRLjKiNs2xGX7g==
"@bull-board/api@3.7.0": "@bull-board/api@3.7.0":
version "3.7.0" version "3.7.0"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/types", "name": "@budibase/types",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Budibase types", "description": "Budibase types",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -46,6 +46,7 @@ export enum MigrationName {
EVENT_APP_BACKFILL = "event_app_backfill", EVENT_APP_BACKFILL = "event_app_backfill",
EVENT_GLOBAL_BACKFILL = "event_global_backfill", EVENT_GLOBAL_BACKFILL = "event_global_backfill",
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill", EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
GLOBAL_INFO_SYNC_USERS = "global_info_sync_users",
} }
export interface MigrationDefinition { export interface MigrationDefinition {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.1.33-alpha.4", "version": "1.2.12",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -35,10 +35,10 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "1.1.33-alpha.4", "@budibase/backend-core": "^1.2.12",
"@budibase/pro": "1.1.33-alpha.4", "@budibase/pro": "1.2.12",
"@budibase/string-templates": "1.1.33-alpha.4", "@budibase/string-templates": "^1.2.12",
"@budibase/types": "1.1.33-alpha.4", "@budibase/types": "^1.2.12",
"@koa/router": "8.0.8", "@koa/router": "8.0.8",
"@sentry/node": "6.17.7", "@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",

View File

@ -3,17 +3,18 @@ import { checkInviteCode } from "../../../utilities/redis"
import { sendEmail } from "../../../utilities/email" import { sendEmail } from "../../../utilities/email"
import { users } from "../../../sdk" import { users } from "../../../sdk"
import env from "../../../environment" import env from "../../../environment"
import { User, CloudAccount } from "@budibase/types" import { CloudAccount, User } from "@budibase/types"
import { import {
events,
errors,
accounts, accounts,
users as usersCore,
tenancy,
cache, cache,
errors,
events,
tenancy,
users as usersCore,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users" import { checkAnyUserExists } from "../../../utilities/users"
import { groups as groupUtils } from "@budibase/pro" import { groups as groupUtils } from "@budibase/pro"
const MAX_USERS_UPLOAD_LIMIT = 1000 const MAX_USERS_UPLOAD_LIMIT = 1000
export const save = async (ctx: any) => { export const save = async (ctx: any) => {
@ -117,8 +118,7 @@ export const adminUser = async (ctx: any) => {
export const countByApp = async (ctx: any) => { export const countByApp = async (ctx: any) => {
const appId = ctx.params.appId const appId = ctx.params.appId
try { try {
const response = await users.countUsersByApp(appId) ctx.body = await users.countUsersByApp(appId)
ctx.body = response
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
} }
@ -126,6 +126,9 @@ export const countByApp = async (ctx: any) => {
export const destroy = async (ctx: any) => { export const destroy = async (ctx: any) => {
const id = ctx.params.id const id = ctx.params.id
if (id === ctx.user._id) {
ctx.throw(400, "Unable to delete self.")
}
await users.destroy(id, ctx.user) await users.destroy(id, ctx.user)
@ -136,6 +139,10 @@ export const destroy = async (ctx: any) => {
export const bulkDelete = async (ctx: any) => { export const bulkDelete = async (ctx: any) => {
const { userIds } = ctx.request.body const { userIds } = ctx.request.body
if (userIds?.indexOf(ctx.user._id) !== -1) {
ctx.throw(400, "Unable to delete self.")
}
try { try {
let usersResponse = await users.bulkDelete(userIds) let usersResponse = await users.bulkDelete(userIds)
@ -207,13 +214,13 @@ export const invite = async (ctx: any) => {
} }
export const inviteMultiple = async (ctx: any) => { export const inviteMultiple = async (ctx: any) => {
let { emails, userInfo } = ctx.request.body let users = ctx.request.body
let existing = false let existing = false
let existingEmail let existingEmail
for (let email of emails) { for (let user of users) {
if (await usersCore.getGlobalUserByEmail(email)) { if (await usersCore.getGlobalUserByEmail(user.email)) {
existing = true existing = true
existingEmail = email existingEmail = user.email
break break
} }
} }
@ -221,17 +228,18 @@ export const inviteMultiple = async (ctx: any) => {
if (existing) { if (existing) {
ctx.throw(400, `${existingEmail} already exists`) ctx.throw(400, `${existingEmail} already exists`)
} }
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = tenancy.getTenantId()
const opts: any = {
subject: "{{ company }} platform invitation",
info: userInfo,
}
for (let i = 0; i < emails.length; i++) { for (let i = 0; i < users.length; i++) {
await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts) let userInfo = users[i].userInfo
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = tenancy.getTenantId()
const opts: any = {
subject: "{{ company }} platform invitation",
info: userInfo,
}
await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts)
} }
ctx.body = { ctx.body = {

View File

@ -0,0 +1,13 @@
const { migrate, MIGRATIONS } = require("../../../migrations")
export const runMigrations = async (ctx: any) => {
const options = ctx.request.body
// don't await as can take a while, just return
migrate(options)
ctx.status = 200
}
export const fetchDefinitions = async (ctx: any) => {
ctx.body = MIGRATIONS
ctx.status = 200
}

View File

@ -106,7 +106,10 @@ router
if (ctx.publicEndpoint) { if (ctx.publicEndpoint) {
return next() return next()
} }
if ((!ctx.isAuthenticated || !ctx.user.budibaseAccess) && !ctx.internal) { if (
(!ctx.isAuthenticated || (ctx.user && !ctx.user.budibaseAccess)) &&
!ctx.internal
) {
ctx.throw(403, "Unauthorized - no public worker access") ctx.throw(403, "Unauthorized - no public worker access")
} }
return next() return next()

View File

@ -1,12 +1,12 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../../controllers/global/roles") const controller = require("../../controllers/global/roles")
const { adminOnly } = require("@budibase/backend-core/auth") const { builderOrAdmin } = require("@budibase/backend-core/auth")
const router = Router() const router = Router()
router router
.get("/api/global/roles", adminOnly, controller.fetch) .get("/api/global/roles", builderOrAdmin, controller.fetch)
.get("/api/global/roles/:appId", adminOnly, controller.find) .get("/api/global/roles/:appId", builderOrAdmin, controller.find)
.delete("/api/global/roles/:appId", adminOnly, controller.removeAppRole) .delete("/api/global/roles/:appId", builderOrAdmin, controller.removeAppRole)
module.exports = router module.exports = router

View File

@ -1,6 +1,6 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../../controllers/global/self") const controller = require("../../controllers/global/self")
const builderOnly = require("../../../middleware/builderOnly") const { builderOnly } = require("@budibase/backend-core/auth")
const { users } = require("../validation") const { users } = require("../validation")
const router = Router() const router = Router()

View File

@ -6,7 +6,7 @@ const Joi = require("joi")
const cloudRestricted = require("../../../middleware/cloudRestricted") const cloudRestricted = require("../../../middleware/cloudRestricted")
const { users } = require("../validation") const { users } = require("../validation")
const selfController = require("../../controllers/global/self") const selfController = require("../../controllers/global/self")
const builderOrAdmin = require("../../../middleware/builderOrAdmin") const { builderOrAdmin } = require("@budibase/backend-core/auth")
const router = Router() const router = Router()
@ -32,10 +32,12 @@ function buildInviteValidation() {
function buildInviteMultipleValidation() { function buildInviteMultipleValidation() {
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.array().required().items(
emails: Joi.array().required(), Joi.object({
userInfo: Joi.object().optional(), email: Joi.string(),
}).required()) userInfo: Joi.object().optional(),
})
))
} }
function buildInviteAcceptValidation() { function buildInviteAcceptValidation() {
@ -64,7 +66,7 @@ router
.post("/api/global/users/search", builderOrAdmin, controller.search) .post("/api/global/users/search", builderOrAdmin, controller.search)
.delete("/api/global/users/:id", adminOnly, controller.destroy) .delete("/api/global/users/:id", adminOnly, controller.destroy)
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete) .post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
.get("/api/global/users/count/:appId", adminOnly, controller.countByApp) .get("/api/global/users/count/:appId", builderOrAdmin, controller.countByApp)
.get("/api/global/roles/:appId") .get("/api/global/roles/:appId")
.post( .post(
"/api/global/users/invite", "/api/global/users/invite",
@ -79,7 +81,7 @@ router
controller.invite controller.invite
) )
.post( .post(
"/api/global/users/inviteMultiple", "/api/global/users/multi/invite",
adminOnly, adminOnly,
buildInviteMultipleValidation(), buildInviteMultipleValidation(),
controller.inviteMultiple controller.inviteMultiple

View File

@ -12,6 +12,7 @@ const tenantsRoutes = require("./system/tenants")
const statusRoutes = require("./system/status") const statusRoutes = require("./system/status")
const selfRoutes = require("./global/self") const selfRoutes = require("./global/self")
const licenseRoutes = require("./global/license") const licenseRoutes = require("./global/license")
const migrationRoutes = require("./system/migrations")
let userGroupRoutes = api.groups let userGroupRoutes = api.groups
exports.routes = [ exports.routes = [
@ -29,4 +30,5 @@ exports.routes = [
selfRoutes, selfRoutes,
licenseRoutes, licenseRoutes,
userGroupRoutes, userGroupRoutes,
migrationRoutes,
] ]

View File

@ -0,0 +1,19 @@
import Router from "@koa/router"
import * as migrationsController from "../../controllers/system/migrations"
import { auth } from "@budibase/backend-core"
const router = new Router()
router
.post(
"/api/system/migrations/run",
auth.internalApi,
migrationsController.runMigrations
)
.get(
"/api/system/migrations/definitions",
auth.internalApi,
migrationsController.fetchDefinitions
)
export = router

View File

@ -1,4 +1,4 @@
import joiValidator from "../../../middleware/joi-validator" const { joiValidator } = require("@budibase/backend-core/auth")
import Joi from "joi" import Joi from "joi"
let schema: any = { let schema: any = {

View File

@ -18,7 +18,7 @@ const http = require("http")
const api = require("./api") const api = require("./api")
const redis = require("./utilities/redis") const redis = require("./utilities/redis")
const Sentry = require("@sentry/node") const Sentry = require("@sentry/node")
import { events } from "@budibase/backend-core" import { events, pinoSettings } from "@budibase/backend-core"
// this will setup http and https proxies form env variables // this will setup http and https proxies form env variables
bootstrap() bootstrap()
@ -30,14 +30,7 @@ app.keys = ["secret", "key"]
// set up top level koa middleware // set up top level koa middleware
app.use(koaBody({ multipart: true })) app.use(koaBody({ multipart: true }))
app.use(koaSession(app)) app.use(koaSession(app))
app.use( app.use(logger(pinoSettings()))
logger({
prettyPrint: {
levelFirst: true,
},
level: env.LOG_LEVEL || "error",
})
)
// authentication // authentication
app.use(passport.initialize()) app.use(passport.initialize())

View File

@ -1,9 +0,0 @@
module.exports = async (ctx, next) => {
if (
!ctx.internal &&
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
) {
ctx.throw(403, "Admin user only endpoint.")
}
return next()
}

View File

@ -1,36 +0,0 @@
const Joi = require("joi")
function validate(schema, property) {
// Return a Koa middleware function
return (ctx, next) => {
if (!schema) {
return next()
}
let params = null
if (ctx[property] != null) {
params = ctx[property]
} else if (ctx.request[property] != null) {
params = ctx.request[property]
}
schema = schema.append({
createdAt: Joi.any().optional(),
updatedAt: Joi.any().optional(),
})
const { error } = schema.validate(params)
if (error) {
ctx.throw(400, `Invalid ${property} - ${error.message}`)
return
}
return next()
}
}
module.exports.body = schema => {
return validate(schema, "body")
}
module.exports.params = schema => {
return validate(schema, "params")
}

View File

@ -0,0 +1,20 @@
import { User } from "@budibase/types"
import * as sdk from "../../sdk"
/**
* Date:
* Aug 2022
*
* Description:
* Re-sync the global-db users to the global-info db users
*/
export const run = async (globalDb: any) => {
const users = (await sdk.users.allUsers()) as User[]
const promises = []
for (let user of users) {
promises.push(
sdk.users.addTenant(user.tenantId, user._id as string, user.email)
)
}
await Promise.all(promises)
}

View File

@ -0,0 +1,74 @@
import { migrations, redis } from "@budibase/backend-core"
import { Migration, MigrationOptions, MigrationName } from "@budibase/types"
import env from "../environment"
// migration functions
import * as syncUserInfo from "./functions/globalInfoSyncUsers"
/**
* Populate the migration function and additional configuration from
* the static migration definitions.
*/
export const buildMigrations = () => {
const definitions = migrations.DEFINITIONS
const workerMigrations: Migration[] = []
for (const definition of definitions) {
switch (definition.name) {
case MigrationName.GLOBAL_INFO_SYNC_USERS: {
// only needed in cloud
if (!env.SELF_HOSTED) {
workerMigrations.push({
...definition,
fn: syncUserInfo.run,
})
}
break
}
}
}
return workerMigrations
}
export const MIGRATIONS = buildMigrations()
export const migrate = async (options?: MigrationOptions) => {
if (env.SELF_HOSTED) {
await migrateWithLock(options)
} else {
await migrations.runMigrations(MIGRATIONS, options)
}
}
const migrateWithLock = async (options?: MigrationOptions) => {
// get a new lock client
const redlock = await redis.clients.getMigrationsRedlock()
// lock for 15 minutes
const ttl = 1000 * 60 * 15
let migrationLock
// acquire lock
try {
migrationLock = await redlock.lock("migrations", ttl)
} catch (e: any) {
if (e.name === "LockError") {
return
} else {
throw e
}
}
// run migrations
try {
await migrations.runMigrations(MIGRATIONS, options)
} finally {
// release lock
try {
await migrationLock.unlock()
} catch (e) {
console.error("unable to release migration lock")
}
}
}

View File

@ -101,12 +101,11 @@ interface SaveUserOpts {
bulkCreate?: boolean bulkCreate?: boolean
} }
export const buildUser = async ( const buildUser = async (
user: any, user: any,
opts: SaveUserOpts = { opts: SaveUserOpts = {
hashPassword: true, hashPassword: true,
requirePassword: true, requirePassword: true,
bulkCreate: false,
}, },
tenantId: string, tenantId: string,
dbUser?: any dbUser?: any
@ -185,15 +184,12 @@ export const save = async (
dbUser = await db.get(_id) dbUser = await db.get(_id)
} }
let builtUser = await buildUser( let builtUser = await buildUser(user, opts, tenantId, dbUser)
user,
{ // make sure we set the _id field for a new user
hashPassword: true, if (!_id) {
requirePassword: user.requirePassword, _id = builtUser._id
}, }
tenantId,
dbUser
)
try { try {
const putOpts = { const putOpts = {
@ -220,7 +216,7 @@ export const save = async (
await addTenant(tenantId, _id, email) await addTenant(tenantId, _id, email)
await cache.user.invalidateUser(response.id) await cache.user.invalidateUser(response.id)
// let server know to sync user // let server know to sync user
await apps.syncUserInApps(builtUser._id) await apps.syncUserInApps(_id)
return { return {
_id: response.id, _id: response.id,
@ -293,7 +289,6 @@ export const bulkCreate = async (
{ {
hashPassword: true, hashPassword: true,
requirePassword: user.requirePassword, requirePassword: user.requirePassword,
bulkCreate: false,
}, },
tenantId tenantId
) )
@ -305,6 +300,9 @@ export const bulkCreate = async (
// Post processing of bulk added users, i.e events and cache operations // Post processing of bulk added users, i.e events and cache operations
for (const user of usersToBulkSave) { for (const user of usersToBulkSave) {
// TODO: Refactor to bulk insert users into the info db
// instead of relying on looping tenant creation
await addTenant(tenantId, user._id, user.email)
await eventHelpers.handleSaveEvents(user, null) await eventHelpers.handleSaveEvents(user, null)
await apps.syncUserInApps(user._id) await apps.syncUserInApps(user._id)
} }

View File

@ -291,19 +291,18 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.1.33-alpha.4": "@budibase/backend-core@1.2.12":
version "1.1.33-alpha.4" version "1.2.12"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.1.33-alpha.4.tgz#207ffe45d41535e59ccc21cca9892d1e41818a14" resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.12.tgz#a09ef7c4b7d1217b65e385199f2d044d1f30f450"
integrity sha512-p8SZkODBF4+BhfIYWIkUtJhR04OjvkmkrVTSFWXv2NTkIbSpaJGTkx9Kao+1Dn4N3H4jU4OBdmScy+C8F5MeSw== integrity sha512-o6tsX9bfpMLfAEfxBI4StC6Lvt1PkJ6b0JYlgp8QiXa0WxZX7np24cOxt2fWrP3ASIMDCrzLn2e3k3eOnzvh8w==
dependencies: dependencies:
"@budibase/types" "1.1.33-alpha.4" "@budibase/types" "^1.2.12"
"@techpass/passport-openidconnect" "0.3.2" "@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0" aws-sdk "2.1030.0"
bcrypt "5.0.1" bcrypt "5.0.1"
dotenv "16.0.1" dotenv "16.0.1"
emitter-listener "1.1.2" emitter-listener "1.1.2"
ioredis "4.28.0" ioredis "4.28.0"
joi "17.6.0"
jsonwebtoken "8.5.1" jsonwebtoken "8.5.1"
koa-passport "4.1.4" koa-passport "4.1.4"
lodash "4.17.21" lodash "4.17.21"
@ -325,21 +324,21 @@
uuid "8.3.2" uuid "8.3.2"
zlib "1.0.5" zlib "1.0.5"
"@budibase/pro@1.1.33-alpha.4": "@budibase/pro@1.2.12":
version "1.1.33-alpha.4" version "1.2.12"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.1.33-alpha.4.tgz#d25bc2ca73d11adfdc659e324b1e8de31c17657a" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.12.tgz#252f8f1c6730a3395d9d340f2e843052bf0293bb"
integrity sha512-CQ3zVbom4ndzIfUznUSERQ4Bz6ZVuy4HbOYGKKkU/FjoWqrYRK1tqlhmfCNQy8P9rnKURCUf3PMoWVWSOAS24g== integrity sha512-1zhMMVBCX+VX/ILPlSbI7tdsQLcrxf1W29IQD4W55AbevlFnpQs4qNVveIdXFm+GWvCJbdN5I26CXBOftbVUhA==
dependencies: dependencies:
"@budibase/backend-core" "1.1.33-alpha.4" "@budibase/backend-core" "1.2.12"
"@budibase/types" "1.1.33-alpha.4" "@budibase/types" "1.2.12"
"@koa/router" "8.0.8" "@koa/router" "8.0.8"
joi "17.6.0" joi "17.6.0"
node-fetch "^2.6.1" node-fetch "^2.6.1"
"@budibase/types@1.1.33-alpha.4": "@budibase/types@1.2.12", "@budibase/types@^1.2.12":
version "1.1.33-alpha.4" version "1.2.12"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.1.33-alpha.4.tgz#a8de79c385280389be8b2cc214185caddf5fe4d3" resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.12.tgz#c460d1b39116538e47e00178116ad066aa6a6f1c"
integrity sha512-od/gbLgbJnHsVlCvBQkuJf3t/Y9VLUNRYPl3Y4IbNOylpj3rSOKVGF3jANQgkI+pOBt5ni3Xlhc7aOI3qAning== integrity sha512-EeWadHUzeLx4X27Pv6XWlpSXbeLIMvg7r+Q52kYyOZFkmjtdCNFQW5PCu1bYUw9L1Xa64t7fvRLjKiNs2xGX7g==
"@cspotcode/source-map-consumer@0.8.0": "@cspotcode/source-map-consumer@0.8.0":
version "0.8.0" version "0.8.0"