From eb04c89182884b260b3322106dd8829696bab391 Mon Sep 17 00:00:00 2001 From: Keith Ellis Date: Tue, 31 Oct 2023 16:34:02 -0400 Subject: [PATCH 01/13] Add check to fix startup if CLUSTER_MODE is enable --- packages/server/src/environment.ts | 1 + packages/server/src/startup.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts index a1701535ce..b0a35eedf5 100644 --- a/packages/server/src/environment.ts +++ b/packages/server/src/environment.ts @@ -32,6 +32,7 @@ const environment = { REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_CLUSTERED: process.env.REDIS_CLUSTERED, HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS, + CLUSTER_MODE: process.env.CLUSTER_MODE, API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts index b4a287d2d4..fd07c5f530 100644 --- a/packages/server/src/startup.ts +++ b/packages/server/src/startup.ts @@ -58,7 +58,7 @@ export async function startup(app?: any, server?: any) { return } STARTUP_RAN = true - if (server) { + if (server && env.CLUSTER_MODE?.toLowerCase() !== "true") { console.log(`Budibase running on ${JSON.stringify(server.address())}`) env._set("PORT", server.address().port) } From 5f585a426dceb922e548b9f5cff43cb0d85b7288 Mon Sep 17 00:00:00 2001 From: kellis5137 Date: Thu, 2 Nov 2023 11:29:30 -0400 Subject: [PATCH 02/13] Update startup.ts to use simpler flag check --- packages/server/src/startup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts index fd07c5f530..82cb3502ef 100644 --- a/packages/server/src/startup.ts +++ b/packages/server/src/startup.ts @@ -58,7 +58,7 @@ export async function startup(app?: any, server?: any) { return } STARTUP_RAN = true - if (server && env.CLUSTER_MODE?.toLowerCase() !== "true") { + if (server && !env.CLUSTER_MODE) { console.log(`Budibase running on ${JSON.stringify(server.address())}`) env._set("PORT", server.address().port) } From 883ac0d008c432fdb36b6d4bc1222050bd8228bf Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 8 Nov 2023 16:27:20 +0000 Subject: [PATCH 03/13] Dedupe and improve logic around selecting available datasources for saving and duplicating rows --- .../actions/DuplicateRow.svelte | 65 +++------------ .../ButtonActionEditor/actions/SaveRow.svelte | 58 ++------------ .../ButtonActionEditor/actions/utils.js | 79 +++++++++++++++++++ 3 files changed, 97 insertions(+), 105 deletions(-) create mode 100644 packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte index 18711497ff..94364367a8 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte @@ -2,27 +2,20 @@ import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { tables, viewsV2 } from "stores/backend" - import { - getContextProviderComponents, - getSchemaForDatasourcePlus, - } from "builderStore/dataBinding" + import { getSchemaForDatasourcePlus } from "builderStore/dataBinding" import SaveFields from "./SaveFields.svelte" + import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils" export let parameters export let bindings = [] + export let nested - $: formComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "form" - ) - $: schemaComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "schema" - ) - $: providerOptions = getProviderOptions(formComponents, schemaComponents) - $: schemaFields = getSchemaFields($currentAsset, parameters?.tableId) + $: providerOptions = getDatasourceLikeProviders({ + asset: $currentAsset, + componentId: $store.selectedComponentId, + nested, + }) + $: schemaFields = getSchemaFields(parameters?.tableId) $: tableOptions = $tables.list.map(table => ({ label: table.name, resourceId: table._id, @@ -33,44 +26,8 @@ })) $: options = [...(tableOptions || []), ...(viewOptions || [])] - // Gets a context definition of a certain type from a component definition - const extractComponentContext = (component, contextType) => { - const def = store.actions.components.getDefinition(component?._component) - if (!def) { - return null - } - const contexts = Array.isArray(def.context) ? def.context : [def.context] - return contexts.find(context => context?.type === contextType) - } - - // Gets options for valid context keys which provide valid data to submit - const getProviderOptions = (formComponents, schemaComponents) => { - const formContexts = formComponents.map(component => ({ - component, - context: extractComponentContext(component, "form"), - })) - const schemaContexts = schemaComponents.map(component => ({ - component, - context: extractComponentContext(component, "schema"), - })) - const allContexts = formContexts.concat(schemaContexts) - - return allContexts.map(({ component, context }) => { - let runtimeBinding = component._id - if (context.suffix) { - runtimeBinding += `-${context.suffix}` - } - return { - label: component._instanceName, - value: runtimeBinding, - } - }) - } - - const getSchemaFields = (asset, tableId) => { - const { schema } = getSchemaForDatasourcePlus(tableId) - delete schema._id - delete schema._rev + const getSchemaFields = resourceId => { + const { schema } = getSchemaForDatasourcePlus(resourceId) return Object.values(schema || {}) } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index 27b6463ffa..9f70272d78 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -2,29 +2,19 @@ import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { tables, viewsV2 } from "stores/backend" - import { - getContextProviderComponents, - getSchemaForDatasourcePlus, - } from "builderStore/dataBinding" + import { getSchemaForDatasourcePlus } from "builderStore/dataBinding" import SaveFields from "./SaveFields.svelte" + import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils" export let parameters export let bindings = [] export let nested - $: formComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "form", - { includeSelf: nested } - ) - $: schemaComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "schema", - { includeSelf: nested } - ) - $: providerOptions = getProviderOptions(formComponents, schemaComponents) + $: providerOptions = getDatasourceLikeProviders({ + asset: $currentAsset, + componentId: $store.selectedComponentId, + nested, + }) $: schemaFields = getSchemaFields(parameters?.tableId) $: tableOptions = $tables.list.map(table => ({ label: table.name, @@ -36,40 +26,6 @@ })) $: options = [...(tableOptions || []), ...(viewOptions || [])] - // Gets a context definition of a certain type from a component definition - const extractComponentContext = (component, contextType) => { - const def = store.actions.components.getDefinition(component?._component) - if (!def) { - return null - } - const contexts = Array.isArray(def.context) ? def.context : [def.context] - return contexts.find(context => context?.type === contextType) - } - - // Gets options for valid context keys which provide valid data to submit - const getProviderOptions = (formComponents, schemaComponents) => { - const formContexts = formComponents.map(component => ({ - component, - context: extractComponentContext(component, "form"), - })) - const schemaContexts = schemaComponents.map(component => ({ - component, - context: extractComponentContext(component, "schema"), - })) - const allContexts = formContexts.concat(schemaContexts) - - return allContexts.map(({ component, context }) => { - let runtimeBinding = component._id - if (context.suffix) { - runtimeBinding += `-${context.suffix}` - } - return { - label: component._instanceName, - value: runtimeBinding, - } - }) - } - const getSchemaFields = resourceId => { const { schema } = getSchemaForDatasourcePlus(resourceId) return Object.values(schema || {}) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js new file mode 100644 index 0000000000..371671f455 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js @@ -0,0 +1,79 @@ +import { getContextProviderComponents } from "builderStore/dataBinding" +import { store } from "builderStore" + +// Generates bindings for all components that provider "datasource like" +// contexts. This includes "form" contexts and "schema" contexts. This is used +// by various button actions as candidates for whole "row" objects. +// Some examples are saving rows or duplicating rows. +export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => { + // Get all form context providers + const formComponents = getContextProviderComponents( + asset, + componentId, + "form", + { includeSelf: nested } + ) + + // Get all schema context providers + const schemaComponents = getContextProviderComponents( + asset, + componentId, + "schema", + { includeSelf: nested } + ) + + // Generate contexts for all form providers + const formContexts = formComponents.map(component => ({ + component, + context: extractComponentContext(component, "form"), + })) + + // Generate contexts for all schema providers + const schemaContexts = schemaComponents.map(component => ({ + component, + context: extractComponentContext(component, "schema"), + })) + + // Check for duplicate contexts by the same component. In this case, attempt + // to label contexts with their suffixes + schemaContexts.forEach(schemaContext => { + // Check if we have a form context for this component + const id = schemaContext.component._id + const existing = formContexts.find(x => x.component._id === id) + if (existing) { + if (existing.context.suffix) { + existing.readableSuffix = ` (${existing.context.suffix})` + } + if (schemaContext.context.suffix) { + schemaContext.readableSuffix = ` (${schemaContext.context.suffix})` + } + } + }) + + // Generate bindings for all contexts + const allContexts = formContexts.concat(schemaContexts) + return allContexts.map(({ component, context, readableSuffix }) => { + let readableBinding = component._instanceName + let runtimeBinding = component._id + if (context.suffix) { + runtimeBinding += `-${context.suffix}` + } + if (readableSuffix) { + readableBinding += readableSuffix + } + return { + label: readableBinding, + value: runtimeBinding, + } + }) +} + +// Gets a context definition of a certain type from a component definition +const extractComponentContext = (component, contextType) => { + const def = store.actions.components.getDefinition(component?._component) + if (!def) { + return null + } + const contexts = Array.isArray(def.context) ? def.context : [def.context] + return contexts.find(context => context?.type === contextType) +} From 6b7e410f0d48ee6ba39bffce9db3ec14e3c5cb0a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 9 Nov 2023 09:47:19 +0000 Subject: [PATCH 04/13] Update casing --- .../settings/controls/ButtonActionEditor/actions/utils.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js index 371671f455..aa076fdd3e 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js @@ -1,5 +1,6 @@ import { getContextProviderComponents } from "builderStore/dataBinding" import { store } from "builderStore" +import { capitalise } from "helpers" // Generates bindings for all components that provider "datasource like" // contexts. This includes "form" contexts and "schema" contexts. This is used @@ -42,10 +43,12 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => { const existing = formContexts.find(x => x.component._id === id) if (existing) { if (existing.context.suffix) { - existing.readableSuffix = ` (${existing.context.suffix})` + const suffix = capitalise(existing.context.suffix) + existing.readableSuffix = ` - ${suffix}` } if (schemaContext.context.suffix) { - schemaContext.readableSuffix = ` (${schemaContext.context.suffix})` + const suffix = capitalise(schemaContext.context.suffix) + schemaContext.readableSuffix = ` - ${suffix}` } } }) From 223a82f71746ada05e186a362f860b2906626b1c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 9 Nov 2023 11:07:32 +0000 Subject: [PATCH 05/13] Ensure metadata is not null before registering datasources for automatic hot reloading --- .../client/src/components/context/Provider.svelte | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/client/src/components/context/Provider.svelte b/packages/client/src/components/context/Provider.svelte index 19a5c3a462..3fffaaf7fa 100644 --- a/packages/client/src/components/context/Provider.svelte +++ b/packages/client/src/components/context/Provider.svelte @@ -45,12 +45,13 @@ // Register any "refresh datasource" actions with a singleton store // so we can easily refresh data at all levels for any datasource if (type === ActionTypes.RefreshDatasource) { - const { dataSource } = metadata || {} - dataSourceStore.actions.registerDataSource( - dataSource, - instanceId, - callback - ) + if (metadata?.dataSource) { + dataSourceStore.actions.registerDataSource( + metadata.dataSource, + instanceId, + callback + ) + } } }) } From 1f3e56fdc152f6768fc26737b4b04f42471bfbcc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 9 Nov 2023 11:10:52 +0000 Subject: [PATCH 06/13] Allow grids to provide a refresh datasource action --- .../src/components/app/GridBlock.svelte | 20 ++++++++++++++----- packages/client/src/sdk.js | 2 ++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 801e1a4d0a..0b1c12524a 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -27,8 +27,12 @@ builderStore, notificationStore, enrichButtonActions, + ActionTypes, + createContextStore, } = getContext("sdk") + let grid + $: columnWhitelist = columns?.map(col => col.name) $: schemaOverrides = getSchemaOverrides(columns) $: enrichedButtons = enrichButtons(buttons) @@ -53,11 +57,16 @@ text: settings.text, type: settings.type, onClick: async row => { - // We add a fake context binding in here, which allows us to pretend - // that the grid provides a "schema" binding - that lets us use the - // clicked row in things like save row actions - const enrichedContext = { ...get(context), [get(component).id]: row } - const fn = enrichButtonActions(settings.onClick, enrichedContext) + // Create a fake, ephemeral context to run the buttons actions with + const id = get(component).id + const gridContext = createContextStore(context) + gridContext.actions.provideData(id, row) + gridContext.actions.provideAction( + id, + ActionTypes.RefreshDatasource, + () => grid?.getContext()?.rows.actions.refreshData() + ) + const fn = enrichButtonActions(settings.onClick, get(gridContext)) return await fn?.({ row }) }, })) @@ -69,6 +78,7 @@ class:in-builder={$builderStore.inBuilder} > Date: Thu, 9 Nov 2023 11:22:50 +0000 Subject: [PATCH 07/13] Update button actions to support suffixed actions from blocks --- .../builder/src/builderStore/dataBinding.js | 32 ++++++++++++++++--- .../actions/ChangeFormStep.svelte | 12 ++++--- .../actions/ClearForm.svelte | 12 ++++--- .../actions/RefreshDataProvider.svelte | 12 ++++--- .../actions/ScrollTo.svelte | 32 +++++++++++++------ .../actions/UpdateFieldValue.svelte | 32 +++++++++++++------ .../actions/ValidateForm.svelte | 12 ++++--- 7 files changed, 101 insertions(+), 43 deletions(-) diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 8445bf9e6d..246590a22e 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -228,7 +228,12 @@ export const getContextProviderComponents = ( /** * Gets all data provider components above a component. */ -export const getActionProviderComponents = (asset, componentId, actionType) => { +export const getActionProviders = ( + asset, + componentId, + actionType, + options = { includeSelf: false } +) => { if (!asset || !componentId) { return [] } @@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => { // Get the component tree leading up to this component, ignoring the component // itself const path = findComponentPath(asset.props, componentId) - path.pop() + if (!options?.includeSelf) { + path.pop() + } - // Filter by only data provider components - return path.filter(component => { + // Find matching contexts and generate bindings + let providers = [] + path.forEach(component => { const def = store.actions.components.getDefinition(component._component) - return def?.actions?.includes(actionType) + const actions = (def?.actions || []).map(action => { + return typeof action === "string" ? { type: action } : action + }) + const action = actions.find(x => x.type === actionType) + if (action) { + let runtimeBinding = component._id + if (action.suffix) { + runtimeBinding += `-${action.suffix}` + } + providers.push({ + readableBinding: component._instanceName, + runtimeBinding, + }) + } }) + return providers } /** diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte index 81a2119474..5905d240dc 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte @@ -1,17 +1,19 @@ @@ -17,8 +19,8 @@ x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} /> diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte index 49a93d71dd..e73884495d 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte @@ -1,22 +1,36 @@
@@ -24,8 +38,8 @@ x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} /> x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} />
From 8d1bcfd8b529179b9061a0ba489659af3c0823b2 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 9 Nov 2023 12:38:31 +0000 Subject: [PATCH 08/13] Expose additional actions and context from blocks to utilise new capabilities --- packages/client/manifest.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 378d8c3493..806d1aff44 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6106,6 +6106,24 @@ "defaultValue": "spectrum--medium" } ], + "actions": [ + { + "type": "ValidateForm", + "suffix": "form" + }, + { + "type": "ClearForm", + "suffix": "form" + }, + { + "type": "UpdateFieldValue", + "suffix": "form" + }, + { + "type": "ScrollTo", + "suffix": "form" + } + ], "context": [ { "type": "form", @@ -6361,7 +6379,8 @@ ], "context": { "type": "schema" - } + }, + "actions": ["RefreshDatasource"] }, "bbreferencefield": { "devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels", From 103e933df80ff27dae8c430a3a0c61edd361419b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 9 Nov 2023 12:38:40 +0000 Subject: [PATCH 09/13] Ensure button group component has default props --- packages/client/src/components/app/ButtonGroup.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/app/ButtonGroup.svelte b/packages/client/src/components/app/ButtonGroup.svelte index 87b0990701..4954704b1b 100644 --- a/packages/client/src/components/app/ButtonGroup.svelte +++ b/packages/client/src/components/app/ButtonGroup.svelte @@ -3,9 +3,9 @@ import Block from "../Block.svelte" export let buttons = [] - export let direction - export let hAlign - export let vAlign + export let direction = "row" + export let hAlign = "left" + export let vAlign = "top" export let gap = "S" From 5653d78038c6fb62917c1e11171316264d05772d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Nov 2023 10:31:51 +0000 Subject: [PATCH 10/13] Update pro --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index e202f415d9..2cf6f28380 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit e202f415d9fa540d08cc2ba6e27394fbc22f357b +Subproject commit 2cf6f28380d3ab22128b8a889d622fd5adfa31fc From ddd84820136583cc48f6b59f6a441d303c429524 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 21 Nov 2023 17:30:11 +0000 Subject: [PATCH 11/13] Updating bb admin user creation so that it can be used incase in self host a user gets locked out, the environment variables can be used to create a simple user to access the system. --- packages/backend-core/src/users/db.ts | 35 ++++++++++- packages/backend-core/src/users/users.ts | 44 ++++++++----- packages/server/src/startup.ts | 61 ++++++++++--------- .../server/src/utilities/workerRequests.ts | 4 +- .../src/api/controllers/global/users.ts | 31 +++------- 5 files changed, 106 insertions(+), 69 deletions(-) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index bd85097bbd..61b9c186b8 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -3,7 +3,7 @@ import * as eventHelpers from "./events" import * as accounts from "../accounts" import * as accountSdk from "../accounts" import * as cache from "../cache" -import { getGlobalDB, getIdentity, getTenantId } from "../context" +import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context" import * as dbUtils from "../db" import { EmailUnavailableError, HTTPError } from "../errors" import * as platform from "../platform" @@ -11,12 +11,10 @@ import * as sessions from "../security/sessions" import * as usersCore from "./users" import { Account, - AllDocsResponse, BulkUserCreated, BulkUserDeleted, isSSOAccount, isSSOUser, - RowResponse, SaveUserOpts, User, UserStatus, @@ -488,6 +486,37 @@ export class UserDB { await sessions.invalidateSessions(userId, { reason: "deletion" }) } + static async createAdminUser( + email: string, + password: string, + tenantId: string, + opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean } + ) { + const user: User = { + email: email, + password: password, + createdAt: Date.now(), + roles: {}, + builder: { + global: true, + }, + admin: { + global: true, + }, + tenantId, + } + if (opts?.ssoId) { + user.ssoId = opts.ssoId + } + // always bust checklist beforehand, if an error occurs but can proceed, don't get + // stuck in a cycle + await cache.bustCache(cache.CacheKey.CHECKLIST) + return await UserDB.save(user, { + hashPassword: opts?.hashPassword, + requirePassword: opts?.requirePassword, + }) + } + static async getGroups(groupIds: string[]) { return await this.groups.getBulk(groupIds) } diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 9f4a41f6df..6aed45371a 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -43,7 +43,7 @@ function removeUserPassword(users: User | User[]) { return users } -export const isSupportedUserSearch = (query: SearchQuery) => { +export function isSupportedUserSearch(query: SearchQuery) { const allowed = [ { op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.EQUAL, key: "_id" }, @@ -68,10 +68,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => { return true } -export const bulkGetGlobalUsersById = async ( +export async function bulkGetGlobalUsersById( userIds: string[], opts?: GetOpts -) => { +) { const db = getGlobalDB() let users = ( await db.allDocs({ @@ -85,7 +85,7 @@ export const bulkGetGlobalUsersById = async ( return users } -export const getAllUserIds = async () => { +export async function getAllUserIds() { const db = getGlobalDB() const startKey = `${DocumentType.USER}${SEPARATOR}` const response = await db.allDocs({ @@ -95,7 +95,7 @@ export const getAllUserIds = async () => { return response.rows.map(row => row.id) } -export const bulkUpdateGlobalUsers = async (users: User[]) => { +export async function bulkUpdateGlobalUsers(users: User[]) { const db = getGlobalDB() return (await db.bulkDocs(users)) as BulkDocsResponse } @@ -113,10 +113,10 @@ export async function getById(id: string, opts?: GetOpts): Promise { * Given an email address this will use a view to search through * all the users to find one with this email address. */ -export const getGlobalUserByEmail = async ( +export async function getGlobalUserByEmail( email: String, opts?: GetOpts -): Promise => { +): Promise { if (email == null) { throw "Must supply an email address to view" } @@ -139,11 +139,23 @@ export const getGlobalUserByEmail = async ( return user } -export const searchGlobalUsersByApp = async ( +export async function doesUserExist(email: string) { + try { + const user = await getGlobalUserByEmail(email) + if (Array.isArray(user) || user != null) { + return true + } + } catch (err) { + return false + } + return false +} + +export async function searchGlobalUsersByApp( appId: any, opts: DatabaseQueryOpts, getOpts?: GetOpts -) => { +) { if (typeof appId !== "string") { throw new Error("Must provide a string based app ID") } @@ -167,10 +179,10 @@ export const searchGlobalUsersByApp = async ( Return any user who potentially has access to the application Admins, developers and app users with the explicitly role. */ -export const searchGlobalUsersByAppAccess = async ( +export async function searchGlobalUsersByAppAccess( appId: any, opts?: { limit?: number } -) => { +) { const roleSelector = `roles.${appId}` let orQuery: any[] = [ @@ -205,7 +217,7 @@ export const searchGlobalUsersByAppAccess = async ( return resp.rows } -export const getGlobalUserByAppPage = (appId: string, user: User) => { +export function getGlobalUserByAppPage(appId: string, user: User) { if (!user) { return } @@ -215,11 +227,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { /** * Performs a starts with search on the global email view. */ -export const searchGlobalUsersByEmail = async ( +export async function searchGlobalUsersByEmail( email: string | unknown, opts: any, getOpts?: GetOpts -) => { +) { if (typeof email !== "string") { throw new Error("Must provide a string to search by") } @@ -242,12 +254,12 @@ export const searchGlobalUsersByEmail = async ( } const PAGE_LIMIT = 8 -export const paginatedUsers = async ({ +export async function paginatedUsers({ bookmark, query, appId, limit, -}: SearchUsersRequest = {}) => { +}: SearchUsersRequest = {}) { const db = getGlobalDB() const pageSize = limit ?? PAGE_LIMIT const pageLimit = pageSize + 1 diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts index b4a287d2d4..06b8157850 100644 --- a/packages/server/src/startup.ts +++ b/packages/server/src/startup.ts @@ -1,11 +1,13 @@ import env from "./environment" import * as redis from "./utilities/redis" +import { generateApiKey, getChecklist } from "./utilities/workerRequests" import { - createAdminUser, - generateApiKey, - getChecklist, -} from "./utilities/workerRequests" -import { events, installation, logging, tenancy } from "@budibase/backend-core" + events, + installation, + logging, + tenancy, + users, +} from "@budibase/backend-core" import fs from "fs" import { watch } from "./watch" import * as automations from "./automations" @@ -110,34 +112,37 @@ export async function startup(app?: any, server?: any) { // check and create admin user if required // this must be run after the api has been initialised due to // the app user sync + const bbAdminEmail = env.BB_ADMIN_USER_EMAIL, + bbAdminPassword = env.BB_ADMIN_USER_PASSWORD if ( env.SELF_HOSTED && !env.MULTI_TENANCY && - env.BB_ADMIN_USER_EMAIL && - env.BB_ADMIN_USER_PASSWORD + bbAdminEmail && + bbAdminPassword ) { - const checklist = await getChecklist() - if (!checklist?.adminUser?.checked) { - try { - const tenantId = tenancy.getTenantId() - const user = await createAdminUser( - env.BB_ADMIN_USER_EMAIL, - env.BB_ADMIN_USER_PASSWORD, - tenantId - ) - // Need to set up an API key for automated integration tests - if (env.isTest()) { - await generateApiKey(user._id) - } + const tenantId = tenancy.getTenantId() + await tenancy.doInTenant(tenantId, async () => { + const exists = await users.doesUserExist(bbAdminEmail) + const checklist = await getChecklist() + if (!checklist?.adminUser?.checked || !exists) { + try { + const user = await users.UserDB.createAdminUser( + bbAdminEmail, + bbAdminPassword, + tenantId, + { hashPassword: true, requirePassword: true } + ) + // Need to set up an API key for automated integration tests + if (env.isTest()) { + await generateApiKey(user._id!) + } - console.log( - "Admin account automatically created for", - env.BB_ADMIN_USER_EMAIL - ) - } catch (e) { - logging.logAlert("Error creating initial admin user. Exiting.", e) - shutdown(server) + console.log("Admin account automatically created for", bbAdminEmail) + } catch (e) { + logging.logAlert("Error creating initial admin user. Exiting.", e) + shutdown(server) + } } - } + }) } } diff --git a/packages/server/src/utilities/workerRequests.ts b/packages/server/src/utilities/workerRequests.ts index fa9fde7297..81db7df3c3 100644 --- a/packages/server/src/utilities/workerRequests.ts +++ b/packages/server/src/utilities/workerRequests.ts @@ -167,7 +167,9 @@ export async function createAdminUser( return checkResponse(response, "create admin user") } -export async function getChecklist() { +export async function getChecklist(): Promise<{ + adminUser: { checked: boolean } +}> { const response = await fetch( checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"), request(undefined, { method: "GET" }) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 82a1578c88..58979ec799 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -120,28 +120,17 @@ export const adminUser = async ( ) } - const user: User = { - email: email, - password: password, - createdAt: Date.now(), - roles: {}, - builder: { - global: true, - }, - admin: { - global: true, - }, - tenantId, - ssoId, - } try { - // always bust checklist beforehand, if an error occurs but can proceed, don't get - // stuck in a cycle - await cache.bustCache(cache.CacheKey.CHECKLIST) - const finalUser = await userSdk.db.save(user, { - hashPassword, - requirePassword, - }) + const finalUser = await userSdk.db.createAdminUser( + email, + password, + tenantId, + { + ssoId, + hashPassword, + requirePassword, + } + ) // events let account: CloudAccount | undefined From 18a275ecb1c0fb320089468d1987bf27ccd8b24d Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 21 Nov 2023 17:44:46 +0000 Subject: [PATCH 12/13] Removing unused function. --- packages/server/src/utilities/workerRequests.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/server/src/utilities/workerRequests.ts b/packages/server/src/utilities/workerRequests.ts index 81db7df3c3..56ceff226c 100644 --- a/packages/server/src/utilities/workerRequests.ts +++ b/packages/server/src/utilities/workerRequests.ts @@ -155,18 +155,6 @@ export async function readGlobalUser(ctx: Ctx): Promise { return checkResponse(response, "get user", { ctx }) } -export async function createAdminUser( - email: string, - password: string, - tenantId: string -) { - const response = await fetch( - checkSlashesInUrl(env.WORKER_URL + "/api/global/users/init"), - request(undefined, { method: "POST", body: { email, password, tenantId } }) - ) - return checkResponse(response, "create admin user") -} - export async function getChecklist(): Promise<{ adminUser: { checked: boolean } }> { From bbe263175521d500b12c6bb8844f9881dc5c5cf5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 22 Nov 2023 11:57:18 +0000 Subject: [PATCH 13/13] Update the healthcheck.sh script to use the correct path for CouchDB, delete extraneous build-target-paths.sh script. --- hosting/scripts/build-target-paths.sh | 24 ------------------------ hosting/single/healthcheck.sh | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 hosting/scripts/build-target-paths.sh diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh deleted file mode 100644 index 34227011f4..0000000000 --- a/hosting/scripts/build-target-paths.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -echo ${TARGETBUILD} > /buildtarget.txt -if [[ "${TARGETBUILD}" = "aas" ]]; then - # Azure AppService uses /home for persistent data & SSH on port 2222 - DATA_DIR="${DATA_DIR:-/home}" - WEBSITES_ENABLE_APP_SERVICE_STORAGE=true - mkdir -p $DATA_DIR/{search,minio,couch} - mkdir -p $DATA_DIR/couch/{dbs,views} - chown -R couchdb:couchdb $DATA_DIR/couch/ - apt update - apt-get install -y openssh-server - echo "root:Docker!" | chpasswd - mkdir -p /tmp - chmod +x /tmp/ssh_setup.sh \ - && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) - cp /etc/sshd_config /etc/ssh/sshd_config - /etc/init.d/ssh restart - sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini -else - sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini -fi \ No newline at end of file diff --git a/hosting/single/healthcheck.sh b/hosting/single/healthcheck.sh index 592b3e94fa..12e340062c 100644 --- a/hosting/single/healthcheck.sh +++ b/hosting/single/healthcheck.sh @@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) - healthy=false fi -if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then +if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then echo 'ERROR: CouchDB is not running'; healthy=false fi