diff --git a/lerna.json b/lerna.json index 02674ad9af..9ac2a8d4d2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.152", + "version": "0.9.153-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index 48517d17d1..88d5ace67b 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.152", + "version": "0.9.153-alpha.1", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index 1ace65ba40..e2ad9a9300 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -20,6 +20,10 @@ const getErrorMessage = () => { return done.mock.calls[0][2].message } +const saveUser = async (user) => { + return await db.put(user) +} + describe("third party common", () => { describe("authenticateThirdParty", () => { let thirdPartyUser @@ -36,7 +40,7 @@ describe("third party common", () => { describe("validation", () => { const testValidation = async (message) => { - await authenticateThirdParty(thirdPartyUser, false, done) + await authenticateThirdParty(thirdPartyUser, false, done, saveUser) expect(done.mock.calls.length).toBe(1) expect(getErrorMessage()).toContain(message) } @@ -78,7 +82,7 @@ describe("third party common", () => { describe("when the user doesn't exist", () => { describe("when a local account is required", () => { it("returns an error message", async () => { - await authenticateThirdParty(thirdPartyUser, true, done) + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) expect(done.mock.calls.length).toBe(1) expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.") }) @@ -86,7 +90,7 @@ describe("third party common", () => { describe("when a local account isn't required", () => { it("creates and authenticates the user", async () => { - await authenticateThirdParty(thirdPartyUser, false, done) + await authenticateThirdParty(thirdPartyUser, false, done, saveUser) const user = expectUserIsAuthenticated() expectUserIsSynced(user, thirdPartyUser) expect(user.roles).toStrictEqual({}) @@ -123,7 +127,7 @@ describe("third party common", () => { }) it("syncs and authenticates the user", async () => { - await authenticateThirdParty(thirdPartyUser, true, done) + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) const user = expectUserIsAuthenticated() expectUserIsSynced(user, thirdPartyUser) @@ -139,7 +143,7 @@ describe("third party common", () => { }) it("syncs and authenticates the user", async () => { - await authenticateThirdParty(thirdPartyUser, true, done) + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) const user = expectUserIsAuthenticated() expectUserIsSynced(user, thirdPartyUser) diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index c25aa3e0b0..54a5504712 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -1,6 +1,7 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") const { generateGlobalUserID } = require("../../db/utils") +const { saveUser } = require("../../utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") @@ -14,7 +15,8 @@ const fetch = require("node-fetch") exports.authenticateThirdParty = async function ( thirdPartyUser, requireLocalAccount = true, - done + done, + saveUserFn = saveUser ) { if (!thirdPartyUser.provider) { return authError(done, "third party user provider required") @@ -71,7 +73,13 @@ exports.authenticateThirdParty = async function ( dbUser = await syncUser(dbUser, thirdPartyUser) // create or sync the user - const response = await db.put(dbUser) + let response + try { + response = await saveUserFn(dbUser, getTenantId(), false, false) + } catch (err) { + return authError(done, err) + } + dbUser._rev = response.rev // authenticate diff --git a/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index 9f271ad80e..87b67d464e 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -265,7 +265,7 @@ exports.downloadTarball = async (url, bucketName, path) => { const tmpPath = join(budibaseTempDir(), path) await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) - if (!env.isTest()) { + if (!env.isTest() && env.SELF_HOSTED) { await exports.uploadDirectory(bucketName, tmpPath, path) } // return the temporary path incase there is a use for it diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js index 668bc010ba..67dbfd5619 100644 --- a/packages/auth/src/tenancy/tenancy.js +++ b/packages/auth/src/tenancy/tenancy.js @@ -107,3 +107,13 @@ exports.lookupTenantId = async userId => { } return tenantId } + +// lookup, could be email or userId, either will return a doc +exports.getTenantUser = async identifier => { + const db = getDB(PLATFORM_INFO_DB) + try { + return await db.get(identifier) + } catch (err) { + return null + } +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 93b483c6be..f509a626c1 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -1,10 +1,24 @@ -const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils") +const { + DocumentTypes, + SEPARATOR, + ViewNames, + generateGlobalUserID, +} = require("./db/utils") const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") const { createUserEmailView } = require("./db/views") -const { Headers } = require("./constants") -const { getGlobalDB } = require("./tenancy") +const { Headers, UserStatus } = require("./constants") +const { + getGlobalDB, + updateTenantId, + getTenantUser, + tryAddTenant, +} = require("./tenancy") const environment = require("./environment") +const accounts = require("./cloud/accounts") +const { hash } = require("./hashing") +const userCache = require("./cache/user") +const env = require("./environment") const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -131,3 +145,93 @@ exports.getGlobalUserByEmail = async email => { } } } + +exports.saveUser = async ( + user, + tenantId, + hashPassword = true, + requirePassword = true +) => { + if (!tenantId) { + throw "No tenancy specified." + } + // need to set the context for this request, as specified + updateTenantId(tenantId) + // specify the tenancy incase we're making a new admin user (public) + const db = getGlobalDB(tenantId) + let { email, password, _id } = user + // make sure another user isn't using the same email + let dbUser + if (email) { + // check budibase users inside the tenant + dbUser = await exports.getGlobalUserByEmail(email) + if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { + throw `Email address ${email} already in use.` + } + + // check budibase users in other tenants + if (env.MULTI_TENANCY) { + dbUser = await getTenantUser(email) + if (dbUser != null && dbUser.tenantId !== tenantId) { + throw `Email address ${email} already in use.` + } + } + + // check root account users in account portal + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const account = await accounts.getAccount(email) + if (account && account.verified && account.tenantId !== tenantId) { + throw `Email address ${email} already in use.` + } + } + } else { + dbUser = await db.get(_id) + } + + // get the password, make sure one is defined + let hashedPassword + if (password) { + hashedPassword = hashPassword ? await hash(password) : password + } else if (dbUser) { + hashedPassword = dbUser.password + } else if (requirePassword) { + throw "Password must be specified." + } + + _id = _id || generateGlobalUserID() + user = { + createdAt: Date.now(), + ...dbUser, + ...user, + _id, + password: hashedPassword, + tenantId, + } + // make sure the roles object is always present + if (!user.roles) { + user.roles = {} + } + // add the active status to a user if its not provided + if (user.status == null) { + user.status = UserStatus.ACTIVE + } + try { + const response = await db.put({ + password: hashedPassword, + ...user, + }) + await tryAddTenant(tenantId, _id, email) + await userCache.invalidateUser(response.id) + return { + _id: response.id, + _rev: response.rev, + email, + } + } catch (err) { + if (err.status === 409) { + throw "User exists already" + } else { + throw err + } + } +} diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 170893137e..facc46b82d 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": "0.9.152", + "version": "0.9.153-alpha.1", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index ac9ac17357..9ee2044a1a 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.152", + "version": "0.9.153-alpha.1", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.152", - "@budibase/client": "^0.9.152", + "@budibase/bbui": "^0.9.153-alpha.1", + "@budibase/client": "^0.9.153-alpha.1", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.152", + "@budibase/string-templates": "^0.9.153-alpha.1", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js index dcbb747e38..49928c69a9 100644 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ b/packages/builder/src/builderStore/store/automation/Automation.js @@ -17,7 +17,7 @@ export default class Automation { this.automation.testData = data } - addBlock(block) { + addBlock(block, idx) { // Make sure to add trigger if doesn't exist if (!this.hasTrigger() && block.type === "TRIGGER") { const trigger = { id: generate(), ...block } @@ -26,10 +26,7 @@ export default class Automation { } const newBlock = { id: generate(), ...block } - this.automation.definition.steps = [ - ...this.automation.definition.steps, - newBlock, - ] + this.automation.definition.steps.splice(idx, 0, newBlock) return newBlock } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index 0a47970d28..16cc490bb2 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -104,9 +104,12 @@ const automationActions = store => ({ return state }) }, - addBlockToAutomation: block => { + addBlockToAutomation: (block, blockIdx) => { store.update(state => { - const newBlock = state.selectedAutomation.addBlock(cloneDeep(block)) + const newBlock = state.selectedAutomation.addBlock( + cloneDeep(block), + blockIdx + ) state.selectedBlock = newBlock return state }) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index b822973b62..acb945a96a 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -1,10 +1,9 @@ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index c05a103fac..fee8afd711 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -4,9 +4,9 @@ import FlowItem from "./FlowItem.svelte" import TestDataModal from "./TestDataModal.svelte" import { flip } from "svelte/animate" - import { fade, fly } from "svelte/transition" + import { fly } from "svelte/transition" import { - Detail, + Heading, Icon, ActionButton, notifications, @@ -57,26 +57,24 @@
- {automation.name} -
- + {automation.name} +
+
- +
{ testDataModal.show() }} icon="MultipleCheck" - size="S">Run testRun test
@@ -84,16 +82,11 @@ {#each blocks as block, idx (block.id)}
- {#if idx !== blocks.length - 1} -
- -
- {/if}
{/each}
@@ -114,14 +107,6 @@
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index f077ac35d7..0c0b79c3de 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -14,7 +14,6 @@ import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import ResultsModal from "./ResultsModal.svelte" import ActionModal from "./ActionModal.svelte" - import { database } from "stores/backend" import { externalActions } from "./ExternalActions" export let onSelect @@ -29,7 +28,6 @@ $: testResult = $automationStore.selectedAutomation.testResults?.steps.filter( step => step.stepId === block.stepId ) - $: instanceId = $database._id $: isTrigger = block.type === "TRIGGER" @@ -40,6 +38,10 @@ $: blockIdx = steps.findIndex(step => step.id === block.id) $: lastStep = !isTrigger && blockIdx + 1 === steps.length + $: totalBlocks = + $automationStore.selectedAutomation?.automation?.definition?.steps.length + + 1 + // Logic for hiding / showing the add button.first we check if it has a child // then we check to see whether its inputs have been commpleted $: disableAddButton = isTrigger @@ -167,13 +169,24 @@ - +
+
+ actionModal.show()} + disabled={!hasCompletedInputs} + hoverable + name="AddCircle" + size="S" +/> +{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2} +
+{/if} diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte index 56fa26ee0a..23dc63a060 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte @@ -5,24 +5,29 @@ import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" import { datasources } from "stores/backend" import { IntegrationNames } from "constants" + import cloneDeep from "lodash/cloneDeepWith" export let integration + export let modal + + // kill the reference so the input isn't saved + let config = cloneDeep(integration) function prepareData() { let datasource = {} let existingTypeCount = $datasources.list.filter( - ds => ds.source == integration.type + ds => ds.source == config.type ).length - let baseName = IntegrationNames[integration.type] + let baseName = IntegrationNames[config.type] let name = existingTypeCount == 0 ? baseName : `${baseName}-${existingTypeCount + 1}` datasource.type = "datasource" - datasource.source = integration.type - datasource.config = integration.config + datasource.source = config.type + datasource.config = config.config datasource.name = name - datasource.plus = integration.plus + datasource.plus = config.plus return datasource } @@ -48,9 +53,10 @@ saveDatasource()} - confirmText={integration.plus + onCancel={() => modal.show()} + confirmText={config.plus ? "Fetch tables from database" : "Save and continue to query"} cancelText="Back" @@ -62,10 +68,7 @@ - +