diff --git a/lerna.json b/lerna.json index cc9bd6bd85..b82f3faeed 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.1.33-alpha.4", + "version": "1.2.12", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 3a40b37d9c..50ff14d081 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "1.1.33-alpha.4", + "@budibase/types": "^1.2.12", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index 9ae29a3cbd..d39b8426fb 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -19,6 +19,8 @@ const { csrf, internalApi, adminOnly, + builderOnly, + builderOrAdmin, joiValidator, } = require("./middleware") @@ -176,5 +178,7 @@ module.exports = { updateUserOAuth, ssoCallbackUrl, adminOnly, + builderOnly, + builderOrAdmin, joiValidator, } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 37804b31a6..51cc721ded 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -55,6 +55,7 @@ const env = { DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, SERVICE: process.env.SERVICE || "budibase", MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false, + LOG_LEVEL: process.env.LOG_LEVEL, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", _set(key: any, value: any) { diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 35777ae817..ced4630fb7 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -15,6 +15,7 @@ import auth from "./auth" import constants from "./constants" import * as dbConstants from "./db/constants" import logging from "./logging" +import pino from "./pino" // mimic the outer package exports import * as db from "./pkg/db" @@ -53,6 +54,7 @@ const core = { errors, logging, roles, + ...pino, ...errorClasses, } diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index d86af773c3..674c16aa55 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -81,7 +81,7 @@ module.exports = ( const session = await getSession(userId, sessionId) if (!session) { - error = "No session found" + error = `Session not found - ${userId} - ${sessionId}` } else { try { if (opts && opts.populateUser) { diff --git a/packages/worker/src/middleware/builderOnly.js b/packages/backend-core/src/middleware/builderOnly.js similarity index 100% rename from packages/worker/src/middleware/builderOnly.js rename to packages/backend-core/src/middleware/builderOnly.js diff --git a/packages/worker/src/middleware/builderOrAdmin.js b/packages/backend-core/src/middleware/builderOrAdmin.js similarity index 100% rename from packages/worker/src/middleware/builderOrAdmin.js rename to packages/backend-core/src/middleware/builderOrAdmin.js diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 9d94bf5763..7e7b8a2931 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -10,6 +10,8 @@ const internalApi = require("./internalApi") const datasourceGoogle = require("./passport/datasource/google") const csrf = require("./csrf") const adminOnly = require("./adminOnly") +const builderOrAdmin = require("./builderOrAdmin") +const builderOnly = require("./builderOnly") const joiValidator = require("./joi-validator") module.exports = { google, @@ -27,5 +29,7 @@ module.exports = { }, csrf, adminOnly, + builderOnly, + builderOrAdmin, joiValidator, } diff --git a/packages/backend-core/src/migrations/definitions.ts b/packages/backend-core/src/migrations/definitions.ts index 745c8718c9..34ec0f0cad 100644 --- a/packages/backend-core/src/migrations/definitions.ts +++ b/packages/backend-core/src/migrations/definitions.ts @@ -37,4 +37,8 @@ export const DEFINITIONS: MigrationDefinition[] = [ type: MigrationType.INSTALLATION, name: MigrationName.EVENT_INSTALLATION_BACKFILL, }, + { + type: MigrationType.GLOBAL, + name: MigrationName.GLOBAL_INFO_SYNC_USERS, + }, ] diff --git a/packages/backend-core/src/pino.js b/packages/backend-core/src/pino.js new file mode 100644 index 0000000000..69962b3841 --- /dev/null +++ b/packages/backend-core/src/pino.js @@ -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"), + }, +}) diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 44dc4f2d3e..30869da68e 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -203,15 +203,24 @@ exports.getAllRoles = async appId => { if (appId) { return doWithDB(appId, internal) } 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) { - const body = await db.allDocs( - getRoleParams(null, { - include_docs: true, - }) - ) - let roles = body.rows.map(row => row.doc) + let roles = [] + if (db) { + const body = await db.allDocs( + getRoleParams(null, { + include_docs: true, + }) + ) + roles = body.rows.map(row => row.doc) + } const builtinRoles = exports.getBuiltinRoles() // need to combine builtin with any DB record of them (for sake of permissions) diff --git a/packages/backend-core/src/security/sessions.js b/packages/backend-core/src/security/sessions.js index 8874b47469..a3be0a1a58 100644 --- a/packages/backend-core/src/security/sessions.js +++ b/packages/backend-core/src/security/sessions.js @@ -1,5 +1,7 @@ const redis = require("../redis/init") const { v4: uuidv4 } = require("uuid") +const { logWarn } = require("../logging") +const env = require("../environment") // a week in seconds const EXPIRY_SECONDS = 86400 * 7 @@ -33,12 +35,21 @@ async function invalidateSessions(userId, sessionIds = null) { })) } - const client = await redis.getSessionClient() - const promises = [] - for (let session of sessions) { - promises.push(client.delete(session.key)) + if (sessions && sessions.length > 0) { + const client = await redis.getSessionClient() + const promises = [] + 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) { console.error(`Error invalidating sessions: ${err}`) } diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 08e79914bc..417940aa58 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@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/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte index fbe43717ba..28cb2b2a4e 100644 --- a/packages/bbui/src/Form/Core/PickerDropdown.svelte +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -15,7 +15,6 @@ export let id = null export let placeholder = "Choose an option or type" export let disabled = false - export let readonly = false export let updateOnChange = true export let error = null export let secondaryOptions = [] @@ -35,6 +34,7 @@ export let isOptionSelected = () => false export let isPlaceholder = false export let placeholderOption = null + export let showClearIcon = true const dispatch = createEventDispatcher() let primaryOpen = false @@ -50,17 +50,11 @@ } const updateValue = newValue => { - if (readonly) { - return - } dispatch("change", newValue) } const onClickSecondary = () => { dispatch("click") - if (readonly) { - return - } secondaryOpen = true } @@ -80,24 +74,15 @@ } const onBlur = event => { - if (readonly) { - return - } focus = false updateValue(event.target.value) } const onInput = event => { - if (readonly || !updateOnChange) { - return - } updateValue(event.target.value) } const updateValueOnEnter = event => { - if (readonly) { - return - } if (event.key === "Enter") { updateValue(event.target.value) } @@ -140,11 +125,12 @@ value={primaryLabel || ""} placeholder={placeholder || ""} {disabled} - {readonly} + readonly class="spectrum-Textfield-input spectrum-InputGroup-input" class:labelPadding={iconData} + class:open={primaryOpen} /> - {#if primaryValue} + {#if primaryValue && showClearIcon} + {/if} {#if showConfirmButton} diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index c929e02d86..01a2ca4835 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -503,12 +503,6 @@ .spectrum-Table-headCell--alignRight { 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 { position: sticky; left: 0; @@ -580,13 +574,6 @@ background-color: var(--table-bg); 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 { position: sticky; left: 0; diff --git a/packages/builder/package.json b/packages/builder/package.json index d50d9c2287..3688514c28 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "license": "GPL-3.0", "private": true, "scripts": { @@ -69,10 +69,10 @@ } }, "dependencies": { - "@budibase/bbui": "1.1.33-alpha.4", - "@budibase/client": "1.1.33-alpha.4", - "@budibase/frontend-core": "1.1.33-alpha.4", - "@budibase/string-templates": "1.1.33-alpha.4", + "@budibase/bbui": "^1.2.12", + "@budibase/client": "^1.2.12", + "@budibase/frontend-core": "^1.2.12", + "@budibase/string-templates": "^1.2.12", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 1f461ebad3..37742626cd 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -14,7 +14,13 @@ import Table from "./Table.svelte" import { TableNames } from "constants" 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 { API } from "api" @@ -29,6 +35,13 @@ $: fetch = createFetch(id) $: hasCols = checkHasCols(schema) $: hasRows = !!$fetch.rows?.length + $: showError($fetch.error) + + const showError = error => { + if (error) { + notifications.error(error?.message || "Unable to fetch data.") + } + } const enrichSchema = schema => { let tempSchema = { ...schema } diff --git a/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte index 3d662ed556..e70a0aa042 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte @@ -5,6 +5,7 @@ export let selectedRows export let deleteRows + export let item = "row" const dispatch = createEventDispatcher() let modal @@ -14,12 +15,14 @@ modal?.hide() dispatch("updaterows") } + + $: text = `${item}${selectedRows?.length === 1 ? "" : "s"}` Are you sure you want to delete {selectedRows.length} - row{selectedRows.length > 1 ? "s" : ""}? + {text}? diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index 0d05e170e0..a089664d2e 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -27,7 +27,6 @@ import { AppStatus } from "constants" import Logo from "assets/bb-space-man.svg" import AccessFilter from "./_components/AcessFilter.svelte" - import { Constants } from "@budibase/frontend-core" let sortBy = "name" let template @@ -69,10 +68,6 @@ $: unlocked = lockedApps?.length === 0 $: automationErrors = getAutomationErrors(enrichedApps) - $: hasGroupsLicense = $auth.user?.license.features.includes( - Constants.Features.USER_GROUPS - ) - const enrichApps = (apps, user, sortBy) => { const enrichedApps = apps.map(app => ({ ...app, @@ -360,7 +355,7 @@ {/if}
- {#if hasGroupsLicense && $groups.length} + {#if $auth.groupsEnabled && $groups.length} {/if} -
-
- - -
- {#if userId !== $auth.user._id} -
- - +
+
+ + +
+
+ + +
+ + {#if userId !== $auth.user._id} +
+ + @@ -95,11 +92,11 @@ options={Constants.BuilderRoleDescriptions} /> - {#if hasGroupsLicense} + {#if $auth.groupsEnabled} option.name} getOptionValue={option => option._id} @@ -122,14 +119,12 @@ label { font-family: var(--font-sans); - cursor: pointer; font-weight: 600; box-sizing: border-box; overflow: hidden; border-radius: var(--border-radius-s); color: var(--ink); padding: var(--spacing-m) var(--spacing-l); - transition: all 0.2s ease 0s; display: inline-flex; text-rendering: optimizeLegibility; min-width: auto; @@ -141,10 +136,15 @@ align-items: center; justify-content: center; width: 100%; - background-color: var(--grey-2); - font-size: var(--font-size-xs); + background: var(--spectrum-global-color-gray-200); + font-size: 12px; line-height: normal; border: var(--border-transparent); + transition: background-color 130ms ease-out; + } + label:hover { + background: var(--spectrum-global-color-gray-300); + cursor: pointer; } input[type="file"] { diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte index 01dac8c222..02501f2de0 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte @@ -49,10 +49,10 @@ cancelText="Cancel" showCloseIcon={false} > - All your new users can be accessed through the autogenerated passwords. - Make not of these passwords or download the csv + + All your new users can be accessed through the autogenerated passwords. Take + note of these passwords or download the CSV file. +
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte index 4f481d374c..fe7acee6c4 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte @@ -3,14 +3,20 @@ import { Constants } from "@budibase/frontend-core" export let row - $: value = - Constants.BbRoles.find(x => x.value === users.getUserRole(row))?.label || - "Not Available" + + const TooltipMap = { + 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] || "" -
+
{value}
- - diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte index d18881d1bb..73cf5e26fa 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -8,11 +8,10 @@ Layout, Modal, ModalContent, - Icon, + Search, notifications, Pagination, - Search, - Label, + Divider, } from "@budibase/bbui" import AddUserModal from "./_components/AddUserModal.svelte" import { users, groups, auth } from "stores/portal" @@ -20,68 +19,42 @@ import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte" import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte" import AppsTableRenderer from "./_components/AppsTableRenderer.svelte" - import NameTableRenderer from "./_components/NameTableRenderer.svelte" import RoleTableRenderer from "./_components/RoleTableRenderer.svelte" import { goto } from "@roxi/routify" import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte" import PasswordModal from "./_components/PasswordModal.svelte" import ImportUsersModal from "./_components/ImportUsersModal.svelte" import { createPaginationStore } from "helpers/pagination" + import { get } from "svelte/store" 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 createUserModal, inviteConfirmationModal, onboardingTypeModal, passwordModal, importUsersModal - let pageInfo = createPaginationStore() let prevEmail = undefined, searchEmail = undefined - let selectedRows = [] let customRenderers = [ { column: "userGroups", component: GroupsTableRenderer }, { column: "apps", component: AppsTableRenderer }, - { column: "name", component: NameTableRenderer }, { column: "role", component: RoleTableRenderer }, ] - $: hasGroupsLicense = $auth.user?.license.features.includes( - Constants.Features.USER_GROUPS - ) - $: schema = { - name: {}, email: {}, role: { sortable: false, }, - ...(hasGroupsLicense && { - userGroups: { sortable: false, displayName: "User groups" }, + ...($auth.groupsEnabled && { + userGroups: { sortable: false, displayName: "Groups" }, }), apps: {}, } - $: userData = [] - $: page = $pageInfo.page $: fetchUsers(page, searchEmail) $: { @@ -104,6 +77,7 @@ } }) } + const showOnboardingTypeModal = async addUsersData => { userData = await removingDuplicities(addUsersData) if (!userData?.users?.length) return @@ -112,13 +86,13 @@ } 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 { - const res = await users.invite({ - emails: emails, - builder: false, - admin: false, - }) + const res = await users.invite(payload) notifications.success(res.message) inviteConfirmationModal.show() } catch (error) { @@ -198,6 +172,10 @@ const deleteRows = async () => { try { 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) notifications.success(`Successfully deleted ${selectedRows.length} rows`) selectedRows = [] @@ -227,23 +205,13 @@ } - + Users Add users and control who gets access to your published apps - -
- {#each accessTypes as type} -
- -
- {type.description} -
-
- {/each} -
- + +
- - -
- - -
- {#if selectedRows.length > 0} - - {/if} + Import users +
- $goto(`./${detail._id}`)} - {schema} - bind:selectedRows - data={enrichedUsers} - allowEditColumns={false} - allowEditRows={false} - allowSelectRows={true} - showHeaderBorder={false} - {customRenderers} - /> - +
$goto(`./${detail._id}`)} + {schema} + bind:selectedRows + data={enrichedUsers} + allowEditColumns={false} + allowEditRows={false} + allowSelectRows={true} + showHeaderBorder={false} + {customRenderers} + /> + @@ -320,28 +296,22 @@ display: flex; flex-direction: row; justify-content: flex-end; - margin-top: var(--spacing-xl); } - .field { + .controls { display: flex; - align-items: center; flex-direction: row; - grid-gap: var(--spacing-m); - margin-left: auto; + justify-content: space-between; + align-items: center; } - - .field > :global(*) + :global(*) { - margin-left: var(--spacing-m); - } - - .access-description { + .controls-right { display: flex; - margin-top: var(--spacing-xl); - opacity: 0.8; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: var(--spacing-xl); } - - .access-text { - margin-left: var(--spacing-m); + .controls-right :global(.spectrum-Search) { + width: 200px; } diff --git a/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte index 565dfc7aa2..5e327a8743 100644 --- a/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte +++ b/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte @@ -17,10 +17,10 @@ import { users, groups, apps, auth } from "stores/portal" import AssignmentModal from "./AssignmentModal.svelte" import { createPaginationStore } from "helpers/pagination" - import { Constants } from "@budibase/frontend-core" import { roles } from "stores/backend" export let app + let assignmentModal let appGroups = [] let appUsers = [] @@ -28,14 +28,9 @@ search = undefined let pageInfo = createPaginationStore() let fixedAppId + $: page = $pageInfo.page - - $: hasGroupsLicense = $auth.user?.license.features.includes( - Constants.Features.USER_GROUPS - ) - $: fixedAppId = apps.getProdAppID(app.devId) - $: appGroups = $groups.filter(x => { return x.apps.includes(app.appId) }) @@ -161,7 +156,7 @@ > - {#if hasGroupsLicense && appGroups.length} + {#if $auth.groupsEnabled && appGroups.length} {#each appGroups as 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) { if ($pageInfo.loading) { 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() { appData = [...appData, { id: "", role: "" }] } + + const removeItem = index => { + appData = appData.filter((x, idx) => idx !== index) + } addData(appData)} showCloseIcon={false} + disabled={!valid} > - - {#each appData as input, index} - group.name} - getPrimaryOptionValue={group => group.name} - getPrimaryOptionIcon={group => group.icon} - getPrimaryOptionColour={group => group.colour} - getSecondaryOptionLabel={role => role.name} - getSecondaryOptionValue={role => role._id} - getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)} - /> - {/each} - + {#if appData?.length} + + {#each appData as input, index} +
+
+ group.name} + getPrimaryOptionValue={group => group.name} + getPrimaryOptionIcon={group => group.icon} + getPrimaryOptionColour={group => group.colour} + getSecondaryOptionLabel={role => role.name} + getSecondaryOptionValue={role => role._id} + getSecondaryOptionColour={role => + RoleUtils.getRoleColour(role._id)} + /> +
+
+ removeItem(index)} + /> +
+
+ {/each} +
+ {/if}
Add email
+ + diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index 7075b40d51..9d2d9cf42e 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -2,6 +2,8 @@ import { derived, writable, get } from "svelte/store" import { API } from "api" import { admin } from "stores/portal" import analytics from "analytics" +import { FEATURE_FLAGS } from "helpers/featureFlags" +import { Constants } from "@budibase/frontend-core" export function createAuthStore() { const auth = writable({ @@ -10,11 +12,13 @@ export function createAuthStore() { tenantSet: false, loaded: false, postLogout: false, + groupsEnabled: false, }) const store = derived(auth, $store => { let initials = null let isAdmin = false let isBuilder = false + let groupsEnabled = false if ($store.user) { const user = $store.user if (user.firstName) { @@ -29,6 +33,9 @@ export function createAuthStore() { } isAdmin = !!user.admin?.global isBuilder = !!user.builder?.global + groupsEnabled = + user?.license.features.includes(Constants.Features.USER_GROUPS) && + user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS) } return { user: $store.user, @@ -39,6 +46,7 @@ export function createAuthStore() { initials, isAdmin, isBuilder, + groupsEnabled, } }) diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index 490d1bc9f6..7fc3704e98 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -26,12 +26,8 @@ export function createUsersStore() { return await API.getUsers() } - async function invite({ emails, builder, admin }) { - return API.inviteUsers({ - emails, - builder, - admin, - }) + async function invite(payload) { + return API.inviteUsers(payload) } async function acceptInvite(inviteCode, password) { return API.acceptInvite({ diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index efef4f97c8..655ef7b624 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -5,3 +5,4 @@ build/ docker-error.log envoy.yaml *.tar.gz +prebuilds/ diff --git a/packages/cli/package.json b/packages/cli/package.json index 390f89813e..c24c32ad15 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index 0d9348a85a..7cdc66413d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "1.1.33-alpha.4", - "@budibase/frontend-core": "1.1.33-alpha.4", - "@budibase/string-templates": "1.1.33-alpha.4", + "@budibase/bbui": "^1.2.12", + "@budibase/frontend-core": "^1.2.12", + "@budibase/string-templates": "^1.2.12", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index e52cbd5c3a..9561d43369 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "1.1.33-alpha.4", + "@budibase/bbui": "^1.2.12", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index 17223a80e6..653376aa55 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -141,20 +141,18 @@ export const buildUserEndpoints = API => ({ /** * Invites multiple users to the current tenant. - * @param email An array of email addresses - * @param builder whether the user should be a global builder - * @param admin whether the user should be a global admin + * @param users An array of users to invite */ - inviteUsers: async ({ emails, builder, admin }) => { + inviteUsers: async users => { return await API.post({ - url: "/api/global/users/inviteMultiple", - body: { - emails, + url: "/api/global/users/multi/invite", + body: users.map(user => ({ + email: user.email, userInfo: { - admin: admin ? { global: true } : undefined, - builder: builder ? { global: true } : undefined, + admin: user.admin ? { global: true } : undefined, + builder: user.admin || user.builder ? { global: true } : undefined, }, - }, + })), }) }, diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 77765f8d6e..4ad4f0fef8 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -60,25 +60,31 @@ export const TableNames = { USERS: "ta_users", } -export const BbRoles = [ - { label: "App User", value: "appUser" }, - { label: "Developer", value: "developer" }, - { label: "Admin", value: "admin" }, +export const BudibaseRoles = { + AppUser: "appUser", + Developer: "developer", + Admin: "admin", +} + +export const BudibaseRoleOptions = [ + { label: "App User", value: BudibaseRoles.AppUser }, + { label: "Developer", value: BudibaseRoles.Developer }, + { label: "Admin", value: BudibaseRoles.Admin }, ] export const BuilderRoleDescriptions = [ { - value: "appUser", + value: BudibaseRoles.AppUser, icon: "User", label: "App user - Only has access to published apps", }, { - value: "developer", + value: BudibaseRoles.Developer, icon: "Hammer", label: "Developer - Access to the app builder", }, { - value: "admin", + value: BudibaseRoles.Admin, icon: "Draw", label: "Admin - Full access", }, diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index ecd5313af5..338e6e0405 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -170,6 +170,7 @@ export default class DataFetch { rows: page.rows, info: page.info, cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null], + error: page.error, })) } @@ -182,7 +183,7 @@ export default class DataFetch { const features = get(this.featureStore) // 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 (!features.supportsSearch) { @@ -204,6 +205,7 @@ export default class DataFetch { info, hasNextPage, cursor, + error, } } @@ -345,8 +347,14 @@ export default class DataFetch { return } this.store.update($store => ({ ...$store, loading: true })) - const { rows, info } = await this.getPage() - this.store.update($store => ({ ...$store, rows, info, loading: false })) + const { rows, info, error } = await this.getPage() + this.store.update($store => ({ + ...$store, + rows, + info, + loading: false, + error, + })) } /** @@ -386,7 +394,7 @@ export default class DataFetch { cursor: nextCursor, pageNumber: $store.pageNumber + 1, })) - const { rows, info, hasNextPage, cursor } = await this.getPage() + const { rows, info, hasNextPage, cursor, error } = await this.getPage() // Update state this.store.update($store => { @@ -400,6 +408,7 @@ export default class DataFetch { info, cursors, loading: false, + error, } }) } @@ -421,7 +430,7 @@ export default class DataFetch { cursor: prevCursor, pageNumber: $store.pageNumber - 1, })) - const { rows, info } = await this.getPage() + const { rows, info, error } = await this.getPage() // Update state this.store.update($store => { @@ -430,6 +439,7 @@ export default class DataFetch { rows, info, loading: false, + error, } }) } diff --git a/packages/frontend-core/src/fetch/TableFetch.js b/packages/frontend-core/src/fetch/TableFetch.js index cf0e124020..a13b1bd186 100644 --- a/packages/frontend-core/src/fetch/TableFetch.js +++ b/packages/frontend-core/src/fetch/TableFetch.js @@ -37,6 +37,7 @@ export default class TableFetch extends DataFetch { return { rows: [], hasNextPage: false, + error, } } } diff --git a/packages/server/package.json b/packages/server/package.json index 0f07104af2..7730db03f7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -77,11 +77,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "1.1.33-alpha.4", - "@budibase/client": "1.1.33-alpha.4", - "@budibase/pro": "1.1.33-alpha.4", - "@budibase/string-templates": "1.1.33-alpha.4", - "@budibase/types": "1.1.33-alpha.4", + "@budibase/backend-core": "^1.2.12", + "@budibase/client": "^1.2.12", + "@budibase/pro": "1.2.12", + "@budibase/string-templates": "^1.2.12", + "@budibase/types": "^1.2.12", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/row/external.js b/packages/server/src/api/controllers/row/external.js index b1c322b8b6..c9f6aa2f78 100644 --- a/packages/server/src/api/controllers/row/external.js +++ b/packages/server/src/api/controllers/row/external.js @@ -128,25 +128,35 @@ exports.search = async ctx => { [params.sort]: direction, } } - const rows = await handleRequest(DataSourceOperation.READ, tableId, { - filters: query, - sort, - paginate: paginateObj, - }) - let hasNextPage = false - if (paginate && rows.length === limit) { - const nextRows = await handleRequest(DataSourceOperation.READ, tableId, { + try { + const rows = await handleRequest(DataSourceOperation.READ, tableId, { filters: query, sort, - paginate: { - limit: 1, - page: bookmark * limit + 1, - }, + paginate: paginateObj, }) - 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 () => { diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 32951cc47e..62301d57ca 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -15,6 +15,7 @@ const Sentry = require("@sentry/node") const fileSystem = require("./utilities/fileSystem") const bullboard = require("./automations/bullboard") const { logAlert } = require("@budibase/backend-core/logging") +const { pinoSettings } = require("@budibase/backend-core") const { Thread } = require("./threads") import redis from "./utilities/redis" import * as migrations from "./migrations" @@ -35,14 +36,7 @@ app.use( }) ) -app.use( - pino({ - prettyPrint: { - levelFirst: true, - }, - level: env.LOG_LEVEL || "error", - }) -) +app.use(pino(pinoSettings())) if (!env.isTest()) { const plugin = bullboard.init() diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 7e4efad84f..a0f2b764bc 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -224,8 +224,9 @@ function shouldCopySpecialColumn( FieldTypes.ARRAY, FieldTypes.FORMULA, ] + // column has been deleted, remove if (column && !fetchedColumn) { - return true + return false } const fetchedIsNumber = !fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 744803548c..62f204199b 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1094,19 +1094,18 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.1.33-alpha.4": - version "1.1.33-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.1.33-alpha.4.tgz#207ffe45d41535e59ccc21cca9892d1e41818a14" - integrity sha512-p8SZkODBF4+BhfIYWIkUtJhR04OjvkmkrVTSFWXv2NTkIbSpaJGTkx9Kao+1Dn4N3H4jU4OBdmScy+C8F5MeSw== +"@budibase/backend-core@1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.12.tgz#a09ef7c4b7d1217b65e385199f2d044d1f30f450" + integrity sha512-o6tsX9bfpMLfAEfxBI4StC6Lvt1PkJ6b0JYlgp8QiXa0WxZX7np24cOxt2fWrP3ASIMDCrzLn2e3k3eOnzvh8w== dependencies: - "@budibase/types" "1.1.33-alpha.4" + "@budibase/types" "^1.2.12" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" bcrypt "5.0.1" dotenv "16.0.1" emitter-listener "1.1.2" ioredis "4.28.0" - joi "17.6.0" jsonwebtoken "8.5.1" koa-passport "4.1.4" lodash "4.17.21" @@ -1178,13 +1177,13 @@ svelte-flatpickr "^3.2.3" svelte-portal "^1.0.0" -"@budibase/pro@1.1.33-alpha.4": - version "1.1.33-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.1.33-alpha.4.tgz#d25bc2ca73d11adfdc659e324b1e8de31c17657a" - integrity sha512-CQ3zVbom4ndzIfUznUSERQ4Bz6ZVuy4HbOYGKKkU/FjoWqrYRK1tqlhmfCNQy8P9rnKURCUf3PMoWVWSOAS24g== +"@budibase/pro@1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.12.tgz#252f8f1c6730a3395d9d340f2e843052bf0293bb" + integrity sha512-1zhMMVBCX+VX/ILPlSbI7tdsQLcrxf1W29IQD4W55AbevlFnpQs4qNVveIdXFm+GWvCJbdN5I26CXBOftbVUhA== dependencies: - "@budibase/backend-core" "1.1.33-alpha.4" - "@budibase/types" "1.1.33-alpha.4" + "@budibase/backend-core" "1.2.12" + "@budibase/types" "1.2.12" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" @@ -1207,10 +1206,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@1.1.33-alpha.4": - version "1.1.33-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.1.33-alpha.4.tgz#a8de79c385280389be8b2cc214185caddf5fe4d3" - integrity sha512-od/gbLgbJnHsVlCvBQkuJf3t/Y9VLUNRYPl3Y4IbNOylpj3rSOKVGF3jANQgkI+pOBt5ni3Xlhc7aOI3qAning== +"@budibase/types@1.2.12", "@budibase/types@^1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.12.tgz#c460d1b39116538e47e00178116ad066aa6a6f1c" + integrity sha512-EeWadHUzeLx4X27Pv6XWlpSXbeLIMvg7r+Q52kYyOZFkmjtdCNFQW5PCu1bYUw9L1Xa64t7fvRLjKiNs2xGX7g== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index ce472cf6f5..d832e5a32f 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index eaca5b3bbf..4ddb558c52 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/types/src/sdk/migrations.ts b/packages/types/src/sdk/migrations.ts index bb32d2e045..23a4d6d097 100644 --- a/packages/types/src/sdk/migrations.ts +++ b/packages/types/src/sdk/migrations.ts @@ -46,6 +46,7 @@ export enum MigrationName { EVENT_APP_BACKFILL = "event_app_backfill", EVENT_GLOBAL_BACKFILL = "event_global_backfill", EVENT_INSTALLATION_BACKFILL = "event_installation_backfill", + GLOBAL_INFO_SYNC_USERS = "global_info_sync_users", } export interface MigrationDefinition { diff --git a/packages/worker/package.json b/packages/worker/package.json index 827d323c98..9878464237 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.1.33-alpha.4", + "version": "1.2.12", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -35,10 +35,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "1.1.33-alpha.4", - "@budibase/pro": "1.1.33-alpha.4", - "@budibase/string-templates": "1.1.33-alpha.4", - "@budibase/types": "1.1.33-alpha.4", + "@budibase/backend-core": "^1.2.12", + "@budibase/pro": "1.2.12", + "@budibase/string-templates": "^1.2.12", + "@budibase/types": "^1.2.12", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 17e655edb3..1f9af3514b 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -3,17 +3,18 @@ import { checkInviteCode } from "../../../utilities/redis" import { sendEmail } from "../../../utilities/email" import { users } from "../../../sdk" import env from "../../../environment" -import { User, CloudAccount } from "@budibase/types" +import { CloudAccount, User } from "@budibase/types" import { - events, - errors, accounts, - users as usersCore, - tenancy, cache, + errors, + events, + tenancy, + users as usersCore, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" import { groups as groupUtils } from "@budibase/pro" + const MAX_USERS_UPLOAD_LIMIT = 1000 export const save = async (ctx: any) => { @@ -117,8 +118,7 @@ export const adminUser = async (ctx: any) => { export const countByApp = async (ctx: any) => { const appId = ctx.params.appId try { - const response = await users.countUsersByApp(appId) - ctx.body = response + ctx.body = await users.countUsersByApp(appId) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -126,6 +126,9 @@ export const countByApp = async (ctx: any) => { export const destroy = async (ctx: any) => { const id = ctx.params.id + if (id === ctx.user._id) { + ctx.throw(400, "Unable to delete self.") + } await users.destroy(id, ctx.user) @@ -136,6 +139,10 @@ export const destroy = async (ctx: any) => { export const bulkDelete = async (ctx: any) => { const { userIds } = ctx.request.body + if (userIds?.indexOf(ctx.user._id) !== -1) { + ctx.throw(400, "Unable to delete self.") + } + try { let usersResponse = await users.bulkDelete(userIds) @@ -207,13 +214,13 @@ export const invite = async (ctx: any) => { } export const inviteMultiple = async (ctx: any) => { - let { emails, userInfo } = ctx.request.body + let users = ctx.request.body let existing = false let existingEmail - for (let email of emails) { - if (await usersCore.getGlobalUserByEmail(email)) { + for (let user of users) { + if (await usersCore.getGlobalUserByEmail(user.email)) { existing = true - existingEmail = email + existingEmail = user.email break } } @@ -221,17 +228,18 @@ export const inviteMultiple = async (ctx: any) => { if (existing) { 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++) { - await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts) + for (let i = 0; i < users.length; i++) { + 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 = { diff --git a/packages/worker/src/api/controllers/system/migrations.ts b/packages/worker/src/api/controllers/system/migrations.ts new file mode 100644 index 0000000000..57a5f6261c --- /dev/null +++ b/packages/worker/src/api/controllers/system/migrations.ts @@ -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 +} diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.js index 281d9d097c..ca56e0c5d2 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.js @@ -106,7 +106,10 @@ router if (ctx.publicEndpoint) { 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") } return next() diff --git a/packages/worker/src/api/routes/global/roles.js b/packages/worker/src/api/routes/global/roles.js index 4e27b7d54b..d99e0e5b56 100644 --- a/packages/worker/src/api/routes/global/roles.js +++ b/packages/worker/src/api/routes/global/roles.js @@ -1,12 +1,12 @@ const Router = require("@koa/router") const controller = require("../../controllers/global/roles") -const { adminOnly } = require("@budibase/backend-core/auth") +const { builderOrAdmin } = require("@budibase/backend-core/auth") const router = Router() router - .get("/api/global/roles", adminOnly, controller.fetch) - .get("/api/global/roles/:appId", adminOnly, controller.find) - .delete("/api/global/roles/:appId", adminOnly, controller.removeAppRole) + .get("/api/global/roles", builderOrAdmin, controller.fetch) + .get("/api/global/roles/:appId", builderOrAdmin, controller.find) + .delete("/api/global/roles/:appId", builderOrAdmin, controller.removeAppRole) module.exports = router diff --git a/packages/worker/src/api/routes/global/self.js b/packages/worker/src/api/routes/global/self.js index e1af7c2146..1683a94f37 100644 --- a/packages/worker/src/api/routes/global/self.js +++ b/packages/worker/src/api/routes/global/self.js @@ -1,6 +1,6 @@ const Router = require("@koa/router") const controller = require("../../controllers/global/self") -const builderOnly = require("../../../middleware/builderOnly") +const { builderOnly } = require("@budibase/backend-core/auth") const { users } = require("../validation") const router = Router() diff --git a/packages/worker/src/api/routes/global/users.js b/packages/worker/src/api/routes/global/users.js index e62e996443..e0a221a795 100644 --- a/packages/worker/src/api/routes/global/users.js +++ b/packages/worker/src/api/routes/global/users.js @@ -6,7 +6,7 @@ const Joi = require("joi") const cloudRestricted = require("../../../middleware/cloudRestricted") const { users } = require("../validation") const selfController = require("../../controllers/global/self") -const builderOrAdmin = require("../../../middleware/builderOrAdmin") +const { builderOrAdmin } = require("@budibase/backend-core/auth") const router = Router() @@ -32,10 +32,12 @@ function buildInviteValidation() { function buildInviteMultipleValidation() { // prettier-ignore - return joiValidator.body(Joi.object({ - emails: Joi.array().required(), - userInfo: Joi.object().optional(), - }).required()) + return joiValidator.body(Joi.array().required().items( + Joi.object({ + email: Joi.string(), + userInfo: Joi.object().optional(), + }) + )) } function buildInviteAcceptValidation() { @@ -64,7 +66,7 @@ router .post("/api/global/users/search", builderOrAdmin, controller.search) .delete("/api/global/users/:id", adminOnly, controller.destroy) .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") .post( "/api/global/users/invite", @@ -79,7 +81,7 @@ router controller.invite ) .post( - "/api/global/users/inviteMultiple", + "/api/global/users/multi/invite", adminOnly, buildInviteMultipleValidation(), controller.inviteMultiple diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index 89c67bdf88..e112d4def3 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -12,6 +12,7 @@ const tenantsRoutes = require("./system/tenants") const statusRoutes = require("./system/status") const selfRoutes = require("./global/self") const licenseRoutes = require("./global/license") +const migrationRoutes = require("./system/migrations") let userGroupRoutes = api.groups exports.routes = [ @@ -29,4 +30,5 @@ exports.routes = [ selfRoutes, licenseRoutes, userGroupRoutes, + migrationRoutes, ] diff --git a/packages/worker/src/api/routes/system/migrations.ts b/packages/worker/src/api/routes/system/migrations.ts new file mode 100644 index 0000000000..5dcf90c4de --- /dev/null +++ b/packages/worker/src/api/routes/system/migrations.ts @@ -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 diff --git a/packages/worker/src/api/routes/validation/users.ts b/packages/worker/src/api/routes/validation/users.ts index e7ad4cca18..d84ae94ee6 100644 --- a/packages/worker/src/api/routes/validation/users.ts +++ b/packages/worker/src/api/routes/validation/users.ts @@ -1,4 +1,4 @@ -import joiValidator from "../../../middleware/joi-validator" +const { joiValidator } = require("@budibase/backend-core/auth") import Joi from "joi" let schema: any = { diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index f8031abacb..6fb954a1b5 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -18,7 +18,7 @@ const http = require("http") const api = require("./api") const redis = require("./utilities/redis") 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 bootstrap() @@ -30,14 +30,7 @@ app.keys = ["secret", "key"] // set up top level koa middleware app.use(koaBody({ multipart: true })) app.use(koaSession(app)) -app.use( - logger({ - prettyPrint: { - levelFirst: true, - }, - level: env.LOG_LEVEL || "error", - }) -) +app.use(logger(pinoSettings())) // authentication app.use(passport.initialize()) diff --git a/packages/worker/src/middleware/adminOnly.js b/packages/worker/src/middleware/adminOnly.js deleted file mode 100644 index 4bfdf83848..0000000000 --- a/packages/worker/src/middleware/adminOnly.js +++ /dev/null @@ -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() -} diff --git a/packages/worker/src/middleware/joi-validator.js b/packages/worker/src/middleware/joi-validator.js deleted file mode 100644 index 748ccebd89..0000000000 --- a/packages/worker/src/middleware/joi-validator.js +++ /dev/null @@ -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") -} diff --git a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts new file mode 100644 index 0000000000..cae6c6af51 --- /dev/null +++ b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts @@ -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) +} diff --git a/packages/worker/src/migrations/index.ts b/packages/worker/src/migrations/index.ts new file mode 100644 index 0000000000..6900596216 --- /dev/null +++ b/packages/worker/src/migrations/index.ts @@ -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") + } + } +} diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index ea7f2517e0..e6b3f0a21d 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -101,12 +101,11 @@ interface SaveUserOpts { bulkCreate?: boolean } -export const buildUser = async ( +const buildUser = async ( user: any, opts: SaveUserOpts = { hashPassword: true, requirePassword: true, - bulkCreate: false, }, tenantId: string, dbUser?: any @@ -185,15 +184,12 @@ export const save = async ( dbUser = await db.get(_id) } - let builtUser = await buildUser( - user, - { - hashPassword: true, - requirePassword: user.requirePassword, - }, - tenantId, - dbUser - ) + let builtUser = await buildUser(user, opts, tenantId, dbUser) + + // make sure we set the _id field for a new user + if (!_id) { + _id = builtUser._id + } try { const putOpts = { @@ -220,7 +216,7 @@ export const save = async ( await addTenant(tenantId, _id, email) await cache.user.invalidateUser(response.id) // let server know to sync user - await apps.syncUserInApps(builtUser._id) + await apps.syncUserInApps(_id) return { _id: response.id, @@ -293,7 +289,6 @@ export const bulkCreate = async ( { hashPassword: true, requirePassword: user.requirePassword, - bulkCreate: false, }, tenantId ) @@ -305,6 +300,9 @@ export const bulkCreate = async ( // Post processing of bulk added users, i.e events and cache operations 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 apps.syncUserInApps(user._id) } diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 8e682666fe..0241459cf4 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -291,19 +291,18 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.1.33-alpha.4": - version "1.1.33-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.1.33-alpha.4.tgz#207ffe45d41535e59ccc21cca9892d1e41818a14" - integrity sha512-p8SZkODBF4+BhfIYWIkUtJhR04OjvkmkrVTSFWXv2NTkIbSpaJGTkx9Kao+1Dn4N3H4jU4OBdmScy+C8F5MeSw== +"@budibase/backend-core@1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.12.tgz#a09ef7c4b7d1217b65e385199f2d044d1f30f450" + integrity sha512-o6tsX9bfpMLfAEfxBI4StC6Lvt1PkJ6b0JYlgp8QiXa0WxZX7np24cOxt2fWrP3ASIMDCrzLn2e3k3eOnzvh8w== dependencies: - "@budibase/types" "1.1.33-alpha.4" + "@budibase/types" "^1.2.12" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" bcrypt "5.0.1" dotenv "16.0.1" emitter-listener "1.1.2" ioredis "4.28.0" - joi "17.6.0" jsonwebtoken "8.5.1" koa-passport "4.1.4" lodash "4.17.21" @@ -325,21 +324,21 @@ uuid "8.3.2" zlib "1.0.5" -"@budibase/pro@1.1.33-alpha.4": - version "1.1.33-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.1.33-alpha.4.tgz#d25bc2ca73d11adfdc659e324b1e8de31c17657a" - integrity sha512-CQ3zVbom4ndzIfUznUSERQ4Bz6ZVuy4HbOYGKKkU/FjoWqrYRK1tqlhmfCNQy8P9rnKURCUf3PMoWVWSOAS24g== +"@budibase/pro@1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.12.tgz#252f8f1c6730a3395d9d340f2e843052bf0293bb" + integrity sha512-1zhMMVBCX+VX/ILPlSbI7tdsQLcrxf1W29IQD4W55AbevlFnpQs4qNVveIdXFm+GWvCJbdN5I26CXBOftbVUhA== dependencies: - "@budibase/backend-core" "1.1.33-alpha.4" - "@budibase/types" "1.1.33-alpha.4" + "@budibase/backend-core" "1.2.12" + "@budibase/types" "1.2.12" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" -"@budibase/types@1.1.33-alpha.4": - version "1.1.33-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.1.33-alpha.4.tgz#a8de79c385280389be8b2cc214185caddf5fe4d3" - integrity sha512-od/gbLgbJnHsVlCvBQkuJf3t/Y9VLUNRYPl3Y4IbNOylpj3rSOKVGF3jANQgkI+pOBt5ni3Xlhc7aOI3qAning== +"@budibase/types@1.2.12", "@budibase/types@^1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.12.tgz#c460d1b39116538e47e00178116ad066aa6a6f1c" + integrity sha512-EeWadHUzeLx4X27Pv6XWlpSXbeLIMvg7r+Q52kYyOZFkmjtdCNFQW5PCu1bYUw9L1Xa64t7fvRLjKiNs2xGX7g== "@cspotcode/source-map-consumer@0.8.0": version "0.8.0"