diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 42a0c0a273..475bd4f66a 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -23,6 +23,15 @@ jobs: build: runs-on: ubuntu-latest + services: + couchdb: + image: ibmcom/couchdb3 + env: + COUCHDB_PASSWORD: budibase + COUCHDB_USER: budibase + ports: + - 4567:5984 + strategy: matrix: node-version: [14.x] @@ -53,13 +62,6 @@ jobs: name: codecov-umbrella verbose: true - # TODO: parallelise this - - name: Cypress run - uses: cypress-io/github-action@v2 - with: - install: false - command: yarn test:e2e:ci - - name: QA Core Integration Tests run: | cd qa-core diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index a15504d58c..5c4004cb57 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -76,6 +76,7 @@ affinity: {} globals: appVersion: "latest" budibaseEnv: PRODUCTION + tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS" enableAnalytics: "1" sentryDSN: "" posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU" diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index cf82e6701b..e02b33d771 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -1,6 +1,6 @@ #!/bin/bash declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD") -declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL") +declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL" "TENANT_FEATURE_FLAGS" "ACCOUNT_PORTAL_URL") # Check the env vars set in Dockerfile have come through, AAS seems to drop them [[ -z "${APP_PORT}" ]] && export APP_PORT=4001 [[ -z "${ARCHITECTURE}" ]] && export ARCHITECTURE=amd @@ -10,6 +10,8 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME [[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000 [[ -z "${NODE_ENV}" ]] && export NODE_ENV=production [[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU +[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS" +[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app [[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379 [[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1 [[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002 diff --git a/lerna.json b/lerna.json index 87ea522b04..ada2a59135 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index ddad708266..3e51598bb7 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "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.4.3-alpha.2", + "@budibase/types": "1.4.8-alpha.10", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index 62f4e8820f..45ca675fa6 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -20,6 +20,7 @@ export enum ViewName { AUTOMATION_LOGS = "automation_logs", ACCOUNT_BY_EMAIL = "account_by_email", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", + USER_BY_GROUP = "by_group_user", } export const DeprecatedViews = { diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index c337d26eaa..f0fff918fc 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -36,154 +36,91 @@ async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) { } } -export const createNewUserEmailView = async () => { - const db = getGlobalDB() +export async function createView(db: any, viewJs: string, viewName: string) { let designDoc try { - designDoc = await db.get(DESIGN_DB) + designDoc = (await db.get(DESIGN_DB)) as DesignDocument } catch (err) { // no design doc, make one designDoc = DesignDoc() } const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }`, + map: viewJs, } designDoc.views = { ...designDoc.views, - [ViewName.USER_BY_EMAIL]: view, + [viewName]: view, } await db.put(designDoc) } +export const createNewUserEmailView = async () => { + const db = getGlobalDB() + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` + await createView(db, viewJs, ViewName.USER_BY_EMAIL) +} + export const createAccountEmailView = async () => { + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` await doWithDB( StaticDatabases.PLATFORM_INFO.name, async (db: PouchDB.Database) => { - let designDoc - try { - designDoc = await db.get(DESIGN_DB) - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.ACCOUNT_BY_EMAIL]: view, - } - await db.put(designDoc) + await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) } ) } export const createUserAppView = async () => { const db = getGlobalDB() as PouchDB.Database - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { - for (let prodAppId of Object.keys(doc.roles)) { - let emitted = prodAppId + "${SEPARATOR}" + doc._id - emit(emitted, null) - } + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { + for (let prodAppId of Object.keys(doc.roles)) { + let emitted = prodAppId + "${SEPARATOR}" + doc._id + emit(emitted, null) } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.USER_BY_APP]: view, - } - await db.put(designDoc) + } + }` + await createView(db, viewJs, ViewName.USER_BY_APP) } export const createApiKeyView = async () => { const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - designDoc = DesignDoc() - } - const view = { - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) { - emit(doc.apiKey, doc.userId) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.BY_API_KEY]: view, - } - await db.put(designDoc) + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) { + emit(doc.apiKey, doc.userId) + } + }` + await createView(db, viewJs, ViewName.BY_API_KEY) } export const createUserBuildersView = async () => { const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - map: `function(doc) { - if (doc.builder && doc.builder.global === true) { - emit(doc._id, doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.USER_BY_BUILDERS]: view, - } - await db.put(designDoc) + const viewJs = `function(doc) { + if (doc.builder && doc.builder.global === true) { + emit(doc._id, doc._id) + } + }` + await createView(db, viewJs, ViewName.USER_BY_BUILDERS) } export const createPlatformUserView = async () => { + const viewJs = `function(doc) { + if (doc.tenantId) { + emit(doc._id.toLowerCase(), doc._id) + } + }` await doWithDB( StaticDatabases.PLATFORM_INFO.name, async (db: PouchDB.Database) => { - let designDoc - try { - designDoc = await db.get(DESIGN_DB) - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc.tenantId) { - emit(doc._id.toLowerCase(), doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.PLATFORM_USERS_LOWERCASE]: view, - } - await db.put(designDoc) + await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) } ) } @@ -196,7 +133,7 @@ export const queryView = async ( viewName: ViewName, params: PouchDB.Query.Options, db: PouchDB.Database, - CreateFuncByName: any, + createFunc: any, opts?: QueryViewOptions ): Promise => { try { @@ -213,10 +150,9 @@ export const queryView = async ( } } catch (err: any) { if (err != null && err.name === "not_found") { - const createFunc = CreateFuncByName[viewName] await removeDeprecated(db, viewName) await createFunc() - return queryView(viewName, params, db, CreateFuncByName, opts) + return queryView(viewName, params, db, createFunc, opts) } else { throw err } @@ -228,7 +164,7 @@ export const queryPlatformView = async ( params: PouchDB.Query.Options, opts?: QueryViewOptions ): Promise => { - const CreateFuncByName = { + const CreateFuncByName: any = { [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, } @@ -236,7 +172,8 @@ export const queryPlatformView = async ( return doWithDB( StaticDatabases.PLATFORM_INFO.name, async (db: PouchDB.Database) => { - return queryView(viewName, params, db, CreateFuncByName, opts) + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) } ) } @@ -247,7 +184,7 @@ export const queryGlobalView = async ( db?: PouchDB.Database, opts?: QueryViewOptions ): Promise => { - const CreateFuncByName = { + const CreateFuncByName: any = { [ViewName.USER_BY_EMAIL]: createNewUserEmailView, [ViewName.BY_API_KEY]: createApiKeyView, [ViewName.USER_BY_BUILDERS]: createUserBuildersView, @@ -257,5 +194,6 @@ export const queryGlobalView = async ( if (!db) { db = getGlobalDB() as PouchDB.Database } - return queryView(viewName, params, db, CreateFuncByName, opts) + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) } diff --git a/packages/backend-core/src/events/processors/LoggingProcessor.ts b/packages/backend-core/src/events/processors/LoggingProcessor.ts index a517fba09c..d41a82fbb4 100644 --- a/packages/backend-core/src/events/processors/LoggingProcessor.ts +++ b/packages/backend-core/src/events/processors/LoggingProcessor.ts @@ -23,9 +23,11 @@ export default class LoggingProcessor implements EventProcessor { return } let timestampString = getTimestampString(timestamp) - console.log( - `[audit] [tenant=${identity.tenantId}] [identityType=${identity.type}] [identity=${identity.id}] ${timestampString} ${event} ` - ) + let message = `[audit] [tenant=${identity.tenantId}] [identityType=${identity.type}] [identity=${identity.id}] ${timestampString} ${event} ` + if (env.isDev()) { + message = message + `[debug: [properties=${JSON.stringify(properties)}] ]` + } + console.log(message) } async identify(identity: Identity, timestamp?: string | number) { diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts index d300873725..b4fd0d1469 100644 --- a/packages/backend-core/src/events/publishers/group.ts +++ b/packages/backend-core/src/events/publishers/group.ts @@ -40,9 +40,9 @@ export async function usersAdded(count: number, group: UserGroup) { await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) } -export async function usersDeleted(emails: string[], group: UserGroup) { +export async function usersDeleted(count: number, group: UserGroup) { const properties: GroupUsersDeletedEvent = { - count: emails.length, + count, groupId: group._id as string, } await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) diff --git a/packages/backend-core/src/events/publishers/license.ts b/packages/backend-core/src/events/publishers/license.ts index 1adc71652e..84472e408f 100644 --- a/packages/backend-core/src/events/publishers/license.ts +++ b/packages/backend-core/src/events/publishers/license.ts @@ -1,27 +1,78 @@ import { publishEvent } from "../events" import { Event, - License, LicenseActivatedEvent, - LicenseDowngradedEvent, - LicenseUpdatedEvent, - LicenseUpgradedEvent, + LicensePlanChangedEvent, + LicenseTierChangedEvent, + PlanType, + Account, + LicensePortalOpenedEvent, + LicenseCheckoutSuccessEvent, + LicenseCheckoutOpenedEvent, + LicensePaymentFailedEvent, + LicensePaymentRecoveredEvent, } from "@budibase/types" -// TODO -export async function updgraded(license: License) { - const properties: LicenseUpgradedEvent = {} - await publishEvent(Event.LICENSE_UPGRADED, properties) +export async function tierChanged(account: Account, from: number, to: number) { + const properties: LicenseTierChangedEvent = { + accountId: account.accountId, + to, + from, + } + await publishEvent(Event.LICENSE_TIER_CHANGED, properties) } -// TODO -export async function downgraded(license: License) { - const properties: LicenseDowngradedEvent = {} - await publishEvent(Event.LICENSE_DOWNGRADED, properties) +export async function planChanged( + account: Account, + from: PlanType, + to: PlanType +) { + const properties: LicensePlanChangedEvent = { + accountId: account.accountId, + to, + from, + } + await publishEvent(Event.LICENSE_PLAN_CHANGED, properties) } -// TODO -export async function activated(license: License) { - const properties: LicenseActivatedEvent = {} +export async function activated(account: Account) { + const properties: LicenseActivatedEvent = { + accountId: account.accountId, + } await publishEvent(Event.LICENSE_ACTIVATED, properties) } + +export async function checkoutOpened(account: Account) { + const properties: LicenseCheckoutOpenedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_CHECKOUT_OPENED, properties) +} + +export async function checkoutSuccess(account: Account) { + const properties: LicenseCheckoutSuccessEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_CHECKOUT_SUCCESS, properties) +} + +export async function portalOpened(account: Account) { + const properties: LicensePortalOpenedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_PORTAL_OPENED, properties) +} + +export async function paymentFailed(account: Account) { + const properties: LicensePaymentFailedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_PAYMENT_FAILED, properties) +} + +export async function paymentRecovered(account: Account) { + const properties: LicensePaymentRecoveredEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_PAYMENT_RECOVERED, properties) +} diff --git a/packages/backend-core/src/featureFlags/index.js b/packages/backend-core/src/featureFlags/index.js index b328839fda..8a8162d0ba 100644 --- a/packages/backend-core/src/featureFlags/index.js +++ b/packages/backend-core/src/featureFlags/index.js @@ -53,7 +53,7 @@ exports.getTenantFeatureFlags = tenantId => { return flags } -exports.FeatureFlag = { +exports.TenantFeatureFlag = { LICENSING: "LICENSING", GOOGLE_SHEETS: "GOOGLE_SHEETS", USER_GROUPS: "USER_GROUPS", diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 2c234bd4b8..83b23b479d 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -18,6 +18,7 @@ import * as logging from "./logging" import pino from "./pino" import * as middleware from "./middleware" import plugins from "./plugin" +import encryption from "./security/encryption" // mimic the outer package exports import * as db from "./pkg/db" @@ -60,6 +61,7 @@ const core = { ...pino, ...errorClasses, middleware, + encryption, } export = core diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 983aebf676..33c9123b63 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -78,7 +78,7 @@ function isBuiltin(role) { */ exports.builtinRoleToNumber = id => { const builtins = exports.getBuiltinRoles() - const MAX = Object.values(BUILTIN_IDS).length + 1 + const MAX = Object.values(builtins).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { return MAX } @@ -94,6 +94,22 @@ exports.builtinRoleToNumber = id => { return count } +/** + * Converts any role to a number, but has to be async to get the roles from db. + */ +exports.roleToNumber = async id => { + if (exports.isBuiltin(id)) { + return exports.builtinRoleToNumber(id) + } + const hierarchy = await exports.getUserRoleHierarchy(id) + for (let role of hierarchy) { + if (isBuiltin(role.inherits)) { + return exports.builtinRoleToNumber(role.inherits) + 1 + } + } + return 0 +} + /** * Returns whichever builtin roleID is lower. */ @@ -172,7 +188,7 @@ async function getAllUserRoles(userRoleId) { * to determine if a user can access something that requires a specific role. * @param {string} userRoleId The user's role ID, this can be found in their access token. * @param {object} opts Various options, such as whether to only retrieve the IDs (default true). - * @returns {Promise} returns an ordered array of the roles, with the first being their + * @returns {Promise} returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => { diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index a100888212..ad5c6b5287 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -121,7 +121,7 @@ export const getTenantUser = async ( return response } -export const isUserInAppTenant = (appId: string, user: any) => { +export const isUserInAppTenant = (appId: string, user?: any) => { let userTenantId if (user) { userTenantId = user.tenantId || DEFAULT_TENANT_ID diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 0793eeb1d9..44f04749c9 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -6,7 +6,24 @@ import { } from "./db/utils" import { queryGlobalView } from "./db/views" import { UNICODE_MAX } from "./db/constants" -import { User } from "@budibase/types" +import { BulkDocsResponse, User } from "@budibase/types" +import { getGlobalDB } from "./context" +import PouchDB from "pouchdb" + +export const bulkGetGlobalUsersById = async (userIds: string[]) => { + const db = getGlobalDB() as PouchDB.Database + return ( + await db.allDocs({ + keys: userIds, + include_docs: true, + }) + ).rows.map(row => row.doc) as User[] +} + +export const bulkUpdateGlobalUsers = async (users: User[]) => { + const db = getGlobalDB() as PouchDB.Database + return (await db.bulkDocs(users)) as BulkDocsResponse +} /** * Given an email address this will use a view to search through diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 8de3f1ced6..94f9f9a2da 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.4.3-alpha.2", + "version": "1.4.8-alpha.10", "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.4.3-alpha.2", + "@budibase/string-templates": "1.4.8-alpha.10", "@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 28cb2b2a4e..1607876b46 100644 --- a/packages/bbui/src/Form/Core/PickerDropdown.svelte +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -9,13 +9,13 @@ import StatusLight from "../../StatusLight/StatusLight.svelte" import Detail from "../../Typography/Detail.svelte" import Search from "./Search.svelte" + import IconAvatar from "../../Icon/IconAvatar.svelte" export let primaryLabel = "" export let primaryValue = null export let id = null export let placeholder = "Choose an option or type" export let disabled = false - export let updateOnChange = true export let error = null export let secondaryOptions = [] export let primaryOptions = [] @@ -204,19 +204,11 @@ })} > {#if primaryOptions[title].getIcon(option)} -
-
- -
-
+ {:else if getPrimaryOptionColour(option, idx)} {/if} - {primaryOptions[title].getLabel(option)} - +
+ +
+ + diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte index c9e4e397e2..40d3c5541c 100644 --- a/packages/bbui/src/List/ListItem.svelte +++ b/packages/bbui/src/List/ListItem.svelte @@ -1,11 +1,12 @@ - +
Headers @@ -61,7 +61,7 @@
- +
Authentication @@ -73,7 +73,7 @@ - +
Variables diff --git a/packages/builder/src/components/common/DashCard.svelte b/packages/builder/src/components/common/DashCard.svelte index d5d9d2ff37..40c7133c42 100644 --- a/packages/builder/src/components/common/DashCard.svelte +++ b/packages/builder/src/components/common/DashCard.svelte @@ -30,13 +30,14 @@ background: var(--spectrum-alias-background-color-primary); border-radius: var(--border-radius-s); overflow: hidden; - min-height: 150px; + min-height: 170px; } .dash-card-header { padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400); border-bottom: 1px solid var(--spectrum-global-color-gray-300); display: flex; justify-content: space-between; + transition: background-color 130ms ease-out; } .dash-card-body { padding: var(--spacing-xl) calc(var(--spacing-xl) * 2); diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index a3f75fd4eb..aa39e5cb60 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -1,13 +1,23 @@ {#if $auth.isAdmin} diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte index 733d7eee92..dab0bfdd90 100644 --- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte @@ -311,7 +311,7 @@ {#if providers.google} - +
@@ -350,7 +350,7 @@ {/if} {#if providers.oidc} - +
diff --git a/packages/builder/src/pages/builder/portal/manage/email/index.svelte b/packages/builder/src/pages/builder/portal/manage/email/index.svelte index 812aa5b014..71583222da 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/index.svelte @@ -132,7 +132,7 @@ values below and click activate. - + {#if smtpConfig} SMTP @@ -186,7 +186,7 @@ Reset
- + Templates diff --git a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte index 17c16c639b..b0ff17cc53 100644 --- a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte @@ -5,13 +5,16 @@ Button, Layout, Heading, - Body, Icon, Popover, notifications, List, ListItem, StatusLight, + Divider, + ActionMenu, + MenuItem, + Modal, } from "@budibase/bbui" import UserGroupPicker from "components/settings/UserGroupPicker.svelte" import { createPaginationStore } from "helpers/pagination" @@ -19,91 +22,32 @@ import { onMount } from "svelte" import { RoleUtils } from "@budibase/frontend-core" import { roles } from "stores/backend" + import ConfirmDialog from "components/common/ConfirmDialog.svelte" + import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte" + import GroupIcon from "./_components/GroupIcon.svelte" export let groupId let popoverAnchor let popover let searchTerm = "" - let selectedUsers = [] let prevSearch = undefined let pageInfo = createPaginationStore() let loaded = false + let editModal + let deleteModal $: page = $pageInfo.page $: fetchUsers(page, searchTerm) $: group = $groups.find(x => x._id === groupId) - - async function addAll() { - selectedUsers = [...selectedUsers, ...filtered.map(u => u._id)] - - let reducedUserObjects = filtered.map(u => { - return { - _id: u._id, - email: u.email, - } - }) - group.users = [...reducedUserObjects, ...group.users] - - await groups.actions.save(group) - - $users.data.forEach(async user => { - let userToEdit = await users.get(user._id) - let userGroups = userToEdit.userGroups || [] - userGroups.push(groupId) - await users.save({ - ...userToEdit, - userGroups, - }) - }) - } - - async function selectUser(id) { - let selectedUser = selectedUsers.includes(id) - if (selectedUser) { - selectedUsers = selectedUsers.filter(id => id !== selectedUser) - let newUsers = group.users.filter(user => user._id !== id) - group.users = newUsers - } else { - let enrichedUser = $users.data - .filter(user => user._id === id) - .map(u => { - return { - _id: u._id, - email: u.email, - } - })[0] - selectedUsers = [...selectedUsers, id] - group.users.push(enrichedUser) + $: filtered = $users.data + $: groupApps = $apps.filter(app => + groups.actions.getGroupAppIds(group).includes(`app_${app.appId}`) + ) + $: { + if (loaded && !group?._id) { + $goto("./") } - - await groups.actions.save(group) - - let user = await users.get(id) - - let userGroups = user.userGroups || [] - userGroups.push(groupId) - await users.save({ - ...user, - userGroups, - }) - } - $: filtered = - $users.data?.filter(x => !group?.users.map(y => y._id).includes(x._id)) || - [] - - $: groupApps = $apps.filter(x => group.apps.includes(x.appId)) - async function removeUser(id) { - let newUsers = group.users.filter(user => user._id !== id) - group.users = newUsers - let user = await users.get(id) - - await users.save({ - ...user, - userGroups: [], - }) - - await groups.actions.save(group) } async function fetchUsers(page, search) { @@ -131,6 +75,24 @@ return role?.name || "Custom role" } + async function deleteGroup() { + try { + await groups.actions.delete(group) + notifications.success("User group deleted successfully") + $goto("./") + } catch (error) { + notifications.error(`Failed to delete user group`) + } + } + + async function saveGroup(group) { + try { + await groups.actions.save(group) + } catch (error) { + notifications.error(`Failed to save user group`) + } + } + onMount(async () => { try { await Promise.all([groups.actions.init(), apps.load(), roles.fetch()]) @@ -142,119 +104,137 @@ {#if loaded} - +
- $goto("../groups")} - size="S" - icon="ArrowLeft" - > + $goto("../groups")} icon="ArrowLeft"> Back
-
-
-
-
- + + +
+
+ +
+ {group?.name}
-
- {group?.name} +
+ + + + + editModal.show()}> + Edit + + deleteModal.show()}> + Delete + +
-
- -
- - - -
- - {#if group?.users.length} - {#each group.users as user} - removeUser(user?._id)} - hoverable - size="L" - name="Close" - /> - {/each} - {:else} - - {/if} - -
- Apps -
- Manage apps that this User group has been assigned to -
-
+ - - {#if groupApps.length} - {#each groupApps as app} - -
- +
+ Users +
+ +
+ + user._id)} + list={$users.data} + on:select={e => groups.actions.addUser(groupId, e.detail)} + on:deselect={e => groups.actions.removeUser(groupId, e.detail)} + /> + +
+ + {#if group?.users.length} + {#each group.users as user} + $goto(`../users/${user._id}`)} + hoverable > - {getRoleLabel(app.appId)} -
-
-
- {/each} - {:else} - - {/if} -
+ { + groups.actions.removeUser(groupId, user._id) + e.stopPropagation() + }} + hoverable + size="S" + name="Close" + /> + + {/each} + {:else} + + {/if} + +
+ + + + Apps + + {#if groupApps.length} + {#each groupApps as app} + $goto(`../../overview/${app.devId}`)} + hoverable + > +
+ + {getRoleLabel(app.appId)} + +
+
+ {/each} + {:else} + + {/if} +
+
{/if} - diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupAppsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupAppsTableRenderer.svelte new file mode 100644 index 0000000000..51f4d7f77c --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupAppsTableRenderer.svelte @@ -0,0 +1,24 @@ + + +
+
+ +
+ {count} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupIcon.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupIcon.svelte new file mode 100644 index 0000000000..c207501b55 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupIcon.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupNameTableRenderer.svelte similarity index 50% rename from packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte rename to packages/builder/src/pages/builder/portal/manage/groups/_components/GroupNameTableRenderer.svelte index a4b65c4d62..e14458d12a 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/GroupNameTableRenderer.svelte @@ -1,20 +1,13 @@
{#if value} -
- x[0]) - .join("")} - /> -
+ {value} {:else}
-
@@ -26,12 +19,8 @@ display: flex; align-items: center; overflow: hidden; + gap: var(--spacing-m); } - - .spacing { - margin-right: var(--spacing-m); - } - .text { opacity: 0.8; } diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte deleted file mode 100644 index e00123614a..0000000000 --- a/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte +++ /dev/null @@ -1,129 +0,0 @@ - - -
-
-
-
- -
-
-
- {group.name} -
-
-
-
- -
- {parseInt(group?.users?.length) || 0} user{parseInt( - group?.users?.length - ) === 1 - ? "" - : "s"} -
-
-
- - -
- {parseInt(group?.apps?.length) || 0} app{parseInt(group?.apps?.length) === 1 - ? "" - : "s"} -
-
-
-
-
- -
-
- - - - - deleteGroup(group)} icon="Delete" - >Delete - editGroup(group)} icon="Edit">Edit - -
-
-
- - - - - - diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/UsersTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/UsersTableRenderer.svelte new file mode 100644 index 0000000000..2adc0c82ae --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/UsersTableRenderer.svelte @@ -0,0 +1,22 @@ + + +
+
+ +
+ {parseInt(value?.length) || 0} +
+ + diff --git a/packages/builder/src/pages/builder/portal/manage/groups/index.svelte b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte index bb8f18ff13..558e9af8b7 100644 --- a/packages/builder/src/pages/builder/portal/manage/groups/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte @@ -4,16 +4,23 @@ Heading, Body, Button, + ButtonGroup, Modal, Tag, Tags, + Table, + Divider, + Search, notifications, } from "@budibase/bbui" - import { groups, auth } from "stores/portal" + import { groups, auth, licensing, admin } from "stores/portal" import { onMount } from "svelte" import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte" - import UserGroupsRow from "./_components/UserGroupsRow.svelte" import { cloneDeep } from "lodash/fp" + import GroupAppsTableRenderer from "./_components/GroupAppsTableRenderer.svelte" + import UsersTableRenderer from "./_components/UsersTableRenderer.svelte" + import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte" + import { goto } from "@roxi/routify" const DefaultGroup = { name: "", @@ -23,20 +30,38 @@ apps: [], roles: {}, } - let modal - let group = cloneDeep(DefaultGroup) - async function deleteGroup(group) { - try { - groups.actions.delete(group) - } catch (error) { - notifications.error(`Failed to delete group`) + let modal + let searchString + let group = cloneDeep(DefaultGroup) + let customRenderers = [ + { column: "name", component: GroupNameTableRenderer }, + { column: "users", component: UsersTableRenderer }, + { column: "roles", component: GroupAppsTableRenderer }, + ] + + $: schema = { + name: {}, + users: { sortable: false }, + roles: { sortable: false, displayName: "Apps" }, + } + $: filteredGroups = filterGroups($groups, searchString) + + const filterGroups = (groups, searchString) => { + if (!searchString) { + return groups } + searchString = searchString.toLocaleLowerCase() + return groups?.filter(group => { + return group.name?.toLowerCase().includes(searchString) + }) } async function saveGroup(group) { try { - await groups.actions.save(group) + group = await groups.actions.save(group) + $goto(`./${group._id}`) + notifications.success(`User group created successfully`) } catch (error) { if (error.status === 400) { notifications.error(error.message) @@ -53,62 +78,81 @@ onMount(async () => { try { - if ($auth.groupsEnabled) { + // always load latest + await licensing.init() + if ($licensing.groupsEnabled) { await groups.actions.init() } } catch (error) { - notifications.error("Error getting User groups") + notifications.error("Error getting user groups") } }) - + -
- User groups - {#if !$auth.groupsEnabled} - -
-
- Pro plan -
+ User groups + {#if !$licensing.groupsEnabled} + +
+
+ Pro plan
- - {/if} -
- Easily assign and manage your users access with User Groups - -
- - {#if !$auth.groupsEnabled} - - {/if} -
- - {#if $auth.groupsEnabled && $groups.length} -
- {#each $groups as group} -
-
- {/each} + + {/if} + + Easily assign and manage your users' access with user groups. + {#if !$auth.accountPortalAccess && !$licensing.groupsEnabled && $admin.cloud} + Contact your account holder to upgrade your plan. + {/if} + + + +
+ + {#if $licensing.groupsEnabled} + + + {:else} + + + + {/if} + +
+
- {/if} +
+ $goto(`./${detail._id}`)} + {schema} + data={filteredGroups} + allowEditColumns={false} + allowEditRows={false} + {customRenderers} + /> @@ -116,37 +160,24 @@ diff --git a/packages/builder/src/pages/builder/portal/manage/plugins/index.svelte b/packages/builder/src/pages/builder/portal/manage/plugins/index.svelte index c03232d091..b1f2480c28 100644 --- a/packages/builder/src/pages/builder/portal/manage/plugins/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/plugins/index.svelte @@ -45,7 +45,7 @@ Plugins - Add your own custom datasources and components + Add your own custom datasources and components. diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index 8f7b24f1b6..f818595539 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -19,17 +19,17 @@ Modal, notifications, Divider, + Banner, StatusLight, } from "@budibase/bbui" import { onMount } from "svelte" - import { fetchData } from "helpers" - import { users, auth, groups, apps } from "stores/portal" + import { users, auth, groups, apps, licensing } from "stores/portal" import { roles } from "stores/backend" - import { Constants } from "@budibase/frontend-core" import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte" - import { RoleUtils } from "@budibase/frontend-core" import UserGroupPicker from "components/settings/UserGroupPicker.svelte" import DeleteUserModal from "./_components/DeleteUserModal.svelte" + import GroupIcon from "../groups/_components/GroupIcon.svelte" + import { Constants, RoleUtils } from "@budibase/frontend-core" export let userId @@ -38,59 +38,57 @@ let popoverAnchor let searchTerm = "" let popover - let selectedGroups = [] - let allAppList = [] let user let loaded = false - $: fetchUser(userId) - $: fullName = $userFetch?.data?.firstName - ? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName - : "" - $: nameLabel = getNameLabel($userFetch) + $: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : "" + $: privileged = user?.admin?.global || user?.builder?.global + $: nameLabel = getNameLabel(user) $: initials = getInitials(nameLabel) - $: allAppList = $apps - .filter(x => { - if ($userFetch.data?.roles) { - return Object.keys($userFetch.data.roles).find(y => { - return x.appId === apps.extractAppId(y) - }) - } - }) - .map(app => { - let roles = Object.fromEntries( - Object.entries($userFetch.data.roles).filter(([key]) => { - return apps.extractAppId(key) === app.appId - }) - ) - return { - name: app.name, - devId: app.devId, - icon: app.icon, - roles, - } - }) - // Used for searching through groups in the add group popover - $: filteredGroups = $groups.filter( - group => - selectedGroups && - group?.name?.toLowerCase().includes(searchTerm.toLowerCase()) - ) + $: filteredGroups = getFilteredGroups($groups, searchTerm) + $: availableApps = getAvailableApps($apps, privileged, user?.roles) $: userGroups = $groups.filter(x => { return x.users?.find(y => { return y._id === userId }) }) - $: globalRole = $userFetch?.data?.admin?.global + $: globalRole = user?.admin?.global ? "admin" - : $userFetch?.data?.builder?.global + : user?.builder?.global ? "developer" : "appUser" - const userFetch = fetchData(`/api/global/users/${userId}`) + const getAvailableApps = (appList, privileged, roles) => { + let availableApps = appList.slice() + if (!privileged) { + availableApps = availableApps.filter(x => { + return Object.keys(roles || {}).find(y => { + return x.appId === apps.extractAppId(y) + }) + }) + } + return availableApps.map(app => { + const prodAppId = apps.getProdAppID(app.appId) + console.log(prodAppId) + return { + name: app.name, + devId: app.devId, + icon: app.icon, + role: privileged ? Constants.Roles.ADMIN : roles[prodAppId], + } + }) + } - const getNameLabel = userFetch => { - const { firstName, lastName, email } = userFetch?.data || {} + const getFilteredGroups = (groups, search) => { + if (!search) { + return groups + } + search = search.toLowerCase() + return groups.filter(group => group.name?.toLowerCase().includes(search)) + } + + const getNameLabel = user => { + const { firstName, lastName, email } = user || {} if (!firstName && !lastName) { return email || "" } @@ -122,38 +120,19 @@ return role?.name || "Custom role" } - function getHighestRole(roles) { - let highestRole - let highestRoleNumber = 0 - Object.keys(roles).forEach(role => { - let roleNumber = RoleUtils.getRolePriority(roles[role]) - if (roleNumber > highestRoleNumber) { - highestRoleNumber = roleNumber - highestRole = roles[role] - } - }) - return highestRole - } async function updateUserFirstName(evt) { try { - await users.save({ ...$userFetch?.data, firstName: evt.target.value }) - await userFetch.refresh() + await users.save({ ...user, firstName: evt.target.value }) + await fetchUser() } catch (error) { notifications.error("Error updating user") } } - async function removeGroup(id) { - let updatedGroup = $groups.find(x => x._id === id) - let newUsers = updatedGroup.users.filter(user => user._id !== userId) - updatedGroup.users = newUsers - groups.actions.save(updatedGroup) - } - async function updateUserLastName(evt) { try { - await users.save({ ...$userFetch?.data, lastName: evt.target.value }) - await userFetch.refresh() + await users.save({ ...user, lastName: evt.target.value }) + await fetchUser() } catch (error) { notifications.error("Error updating user") } @@ -169,40 +148,40 @@ } } - async function addGroup(groupId) { - let selectedGroup = selectedGroups.includes(groupId) - let group = $groups.find(group => group._id === groupId) - - if (selectedGroup) { - selectedGroups = selectedGroups.filter(id => id === selectedGroup) - let newUsers = group.users.filter(groupUser => user._id !== groupUser._id) - group.users = newUsers - } else { - selectedGroups = [...selectedGroups, groupId] - group.users.push(user) + async function fetchUser() { + user = await users.get(userId) + if (!user?._id) { + $goto("./") } - - await groups.actions.save(group) - } - - async function fetchUser(userId) { - let userPromise = users.get(userId) - user = await userPromise } async function toggleFlags(detail) { try { - await users.save({ ...$userFetch?.data, ...detail }) - await userFetch.refresh() + await users.save({ ...user, ...detail }) + await fetchUser() } catch (error) { notifications.error("Error updating user") } } - function addAll() {} + const addGroup = async groupId => { + await groups.actions.addUser(groupId, userId) + await fetchUser() + } + + const removeGroup = async groupId => { + await groups.actions.removeUser(groupId, userId) + await fetchUser() + } + onMount(async () => { try { - await Promise.all([groups.actions.init(), apps.load(), roles.fetch()]) + await Promise.all([ + fetchUser(), + groups.actions.init(), + apps.load(), + roles.fetch(), + ]) loaded = true } catch (error) { notifications.error("Error getting user groups") @@ -225,13 +204,13 @@
{nameLabel} - {#if nameLabel !== $userFetch?.data?.email} - {$userFetch?.data?.email} + {#if nameLabel !== user?.email} + {user?.email} {/if}
- {#if userId !== $auth.user._id} + {#if userId !== $auth.user?._id}
@@ -247,27 +226,21 @@
{/if} - + Details
- +
- +
- +
{#if userId !== $auth.user._id} @@ -284,7 +257,7 @@ - {#if $auth.groupsEnabled} + {#if $licensing.groupsEnabled}
@@ -301,13 +274,14 @@
addGroup(e.detail)} + on:deselect={e => removeGroup(e.detail)} + iconComponent={GroupIcon} + extractIconProps={item => ({ group: item, size: "S" })} />
@@ -322,7 +296,10 @@ on:click={() => $goto(`../groups/${group._id}`)} > { + removeGroup(group._id) + e.stopPropagation() + }} hoverable size="S" name="Close" @@ -330,7 +307,7 @@ {/each} {:else} - + {/if}
@@ -339,27 +316,28 @@ Apps - {#if allAppList.length} - {#each allAppList as app} + {#if privileged} + + This user's role grants admin access to all apps + + {:else if availableApps.length} + {#each availableApps as app} $goto(`../../overview/${app.devId}`)} >
- - {getRoleLabel(getHighestRole(app.roles))} + + {getRoleLabel(app.role)}
{/each} {:else} - + {/if}
@@ -367,13 +345,10 @@ {/if} - + - + diff --git a/packages/builder/src/pages/builder/portal/settings/usage.svelte b/packages/builder/src/pages/builder/portal/settings/usage.svelte index f2809452fd..75ceccc0a3 100644 --- a/packages/builder/src/pages/builder/portal/settings/usage.svelte +++ b/packages/builder/src/pages/builder/portal/settings/usage.svelte @@ -147,7 +147,8 @@ const init = async () => { try { - await licensing.getQuotaUsage() + // always load latest + await licensing.init() } catch (e) { console.error(e) notifications.error(e) @@ -175,18 +176,18 @@ {#if loaded} - - + + Usage - Get information about your current usage within Budibase. + + Get information about your current usage within Budibase. {#if accountPortalAccess} To upgrade your plan and usage limits visit your Account {:else} - To upgrade your plan and usage limits contact your account holder + To upgrade your plan and usage limits contact your account holder. {/if} diff --git a/packages/builder/src/stores/portal/apps.js b/packages/builder/src/stores/portal/apps.js index 6323046eef..41fdc232b7 100644 --- a/packages/builder/src/stores/portal/apps.js +++ b/packages/builder/src/stores/portal/apps.js @@ -8,14 +8,21 @@ const extractAppId = id => { } const getProdAppID = appId => { - if (!appId || !appId.startsWith("app_dev")) { + if (!appId) { return appId } - // split to take off the app_dev element, then join it together incase any other app_ exist - const split = appId.split("app_dev") - split.shift() - const rest = split.join("app_dev") - return `${"app"}${rest}` + let rest, + separator = "" + if (appId.startsWith("app_dev")) { + // split to take off the app_dev element, then join it together incase any other app_ exist + const split = appId.split("app_dev") + split.shift() + rest = split.join("app_dev") + } else if (!appId.startsWith("app")) { + rest = appId + separator = "_" + } + return `app${separator}${rest}` } export function createAppStore() { diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index 8ac19ab785..31b4533738 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -2,23 +2,20 @@ 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({ user: null, + accountPortalAccess: false, tenantId: "default", 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) { @@ -33,12 +30,10 @@ 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, + accountPortalAccess: $store.accountPortalAccess, tenantId: $store.tenantId, tenantSet: $store.tenantSet, loaded: $store.loaded, @@ -46,7 +41,6 @@ export function createAuthStore() { initials, isAdmin, isBuilder, - groupsEnabled, } }) @@ -54,6 +48,7 @@ export function createAuthStore() { auth.update(store => { store.loaded = true store.user = user + store.accountPortalAccess = user?.accountPortalAccess if (user) { store.tenantId = user.tenantId || "default" store.tenantSet = true diff --git a/packages/builder/src/stores/portal/groups.js b/packages/builder/src/stores/portal/groups.js index ca814ac057..eda3961e2b 100644 --- a/packages/builder/src/stores/portal/groups.js +++ b/packages/builder/src/stores/portal/groups.js @@ -1,36 +1,45 @@ import { writable, get } from "svelte/store" import { API } from "api" -import { auth } from "stores/portal" -import { Constants } from "@budibase/frontend-core" +import { licensing } from "stores/portal" export function createGroupsStore() { const store = writable([]) + const updateStore = group => { + store.update(state => { + const currentIdx = state.findIndex(gr => gr._id === group._id) + if (currentIdx >= 0) { + state.splice(currentIdx, 1, group) + } else { + state.push(group) + } + return state + }) + } + + const getGroup = async groupId => { + const group = await API.getGroup(groupId) + updateStore(group) + } + const actions = { init: async () => { - // only init if these is a groups license, just to be sure but the feature will be blocked + // only init if there is a groups license, just to be sure but the feature will be blocked // on the backend anyway - if ( - get(auth).user.license.features.includes(Constants.Features.USER_GROUPS) - ) { - const users = await API.getGroups() - store.set(users) + if (get(licensing).groupsEnabled) { + const groups = await API.getGroups() + store.set(groups) } }, + get: getGroup, + save: async group => { const response = await API.saveGroup(group) group._id = response._id group._rev = response._rev - store.update(state => { - const currentIdx = state.findIndex(gr => gr._id === response._id) - if (currentIdx >= 0) { - state.splice(currentIdx, 1, group) - } else { - state.push(group) - } - return state - }) + updateStore(group) + return group }, delete: async group => { @@ -43,6 +52,34 @@ export function createGroupsStore() { return state }) }, + + addUser: async (groupId, userId) => { + await API.addUsersToGroup(groupId, userId) + // refresh the group enrichment + await getGroup(groupId) + }, + + removeUser: async (groupId, userId) => { + await API.removeUsersFromGroup(groupId, userId) + // refresh the group enrichment + await getGroup(groupId) + }, + + addApp: async (groupId, appId, roleId) => { + await API.addAppsToGroup(groupId, [{ appId, roleId }]) + // refresh the group roles + await getGroup(groupId) + }, + + removeApp: async (groupId, appId) => { + await API.removeAppsFromGroup(groupId, [{ appId }]) + // refresh the group roles + await getGroup(groupId) + }, + + getGroupAppIds: group => { + return Object.keys(group?.roles || {}) + }, } return { diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index e2b4570302..d927555ceb 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -1,14 +1,31 @@ import { writable, get } from "svelte/store" import { API } from "api" -import { auth } from "stores/portal" +import { auth, admin } from "stores/portal" import { Constants } from "@budibase/frontend-core" import { StripeStatus } from "components/portal/licensing/constants" -import { FEATURE_FLAGS, isEnabled } from "../../helpers/featureFlags" +import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags" export const createLicensingStore = () => { const DEFAULT = { - plans: {}, - usageMetrics: {}, + // navigation + goToUpgradePage: () => {}, + // the top level license + license: undefined, + isFreePlan: true, + // features + groupsEnabled: false, + // the currently used quotas from the db + quotaUsage: undefined, + // derived quota metrics for percentages used + usageMetrics: undefined, + // quota reset + quotaResetDaysRemaining: undefined, + quotaResetDate: undefined, + // failed payments + accountPastDue: undefined, + pastDueEndDate: undefined, + pastDueDaysRemaining: undefined, + accountDowngraded: undefined, } const oneDayInMilliseconds = 86400000 @@ -16,10 +33,39 @@ export const createLicensingStore = () => { const actions = { init: async () => { - await actions.getQuotaUsage() - await actions.getUsageMetrics() + actions.setNavigation() + actions.setLicense() + await actions.setQuotaUsage() + actions.setUsageMetrics() }, - getQuotaUsage: async () => { + setNavigation: () => { + const upgradeUrl = `${get(admin).accountPortalUrl}/portal/upgrade` + const goToUpgradePage = () => { + window.location.href = upgradeUrl + } + store.update(state => { + return { + ...state, + goToUpgradePage, + } + }) + }, + setLicense: () => { + const license = get(auth).user.license + const isFreePlan = license?.plan.type === Constants.PlanType.FREE + const groupsEnabled = license.features.includes( + Constants.Features.USER_GROUPS + ) + store.update(state => { + return { + ...state, + license, + isFreePlan, + groupsEnabled, + } + }) + }, + setQuotaUsage: async () => { const quotaUsage = await API.getQuotaUsage() store.update(state => { return { @@ -28,8 +74,8 @@ export const createLicensingStore = () => { } }) }, - getUsageMetrics: async () => { - if (isEnabled(FEATURE_FLAGS.LICENSING)) { + setUsageMetrics: () => { + if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) { const quota = get(store).quotaUsage const license = get(auth).user.license const now = new Date() @@ -97,9 +143,6 @@ export const createLicensingStore = () => { accountPastDue: pastDueAtMilliseconds != null, pastDueEndDate, pastDueDaysRemaining, - isFreePlan: () => { - return license?.plan.type === Constants.PlanType.FREE - }, } }) } diff --git a/packages/builder/src/stores/portal/plugins.js b/packages/builder/src/stores/portal/plugins.js index 8997e8f49d..e259f9aa6d 100644 --- a/packages/builder/src/stores/portal/plugins.js +++ b/packages/builder/src/stores/portal/plugins.js @@ -34,8 +34,7 @@ export function createPluginsStore() { } let res = await API.createPlugin(pluginData) - - let newPlugin = res.plugins[0] + let newPlugin = res.plugin update(state => { const currentIdx = state.findIndex(plugin => plugin._id === newPlugin._id) if (currentIdx >= 0) { diff --git a/packages/cli/package.json b/packages/cli/package.json index 499acb5bfa..e1e052d655 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,7 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "1.4.3-alpha.2", + "@budibase/backend-core": "1.4.8-alpha.10", + "@budibase/string-templates": "1.4.8-alpha.10", + "@budibase/types": "1.4.8-alpha.10", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index 3ef11a24aa..87da774769 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "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.4.3-alpha.2", - "@budibase/frontend-core": "1.4.3-alpha.2", - "@budibase/string-templates": "1.4.3-alpha.2", + "@budibase/bbui": "1.4.8-alpha.10", + "@budibase/frontend-core": "1.4.8-alpha.10", + "@budibase/string-templates": "1.4.8-alpha.10", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index b72fa16216..2d586df24d 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -142,6 +142,10 @@ // Determine and apply settings to the component $: applySettings(staticSettings, enrichedSettings, conditionalSettings) + // Determine custom css. + // Broken out as a separate variable to minimize reactivity updates. + $: customCSS = cachedSettings?._css + // Scroll the selected element into view $: selected && scrollIntoView() @@ -151,6 +155,7 @@ children: children.length, styles: { ...instance._styles, + custom: customCSS, id, empty: emptyState, interactive, @@ -249,14 +254,18 @@ // Get raw settings let settings = {} Object.entries(instance) - .filter(([name]) => name === "_conditions" || !name.startsWith("_")) + .filter(([name]) => !name.startsWith("_")) .forEach(([key, value]) => { settings[key] = value }) - - // Derive static, dynamic and nested settings if the instance changed let newStaticSettings = { ...settings } let newDynamicSettings = { ...settings } + + // Attach some internal properties + newDynamicSettings["_conditions"] = instance._conditions + newDynamicSettings["_css"] = instance._styles?.custom + + // Derive static, dynamic and nested settings if the instance changed settingsDefinition?.forEach(setting => { if (setting.nested) { delete newDynamicSettings[setting.key] @@ -370,6 +379,11 @@ // setting it on initialSettings directly, we avoid a double render. cachedSettings[key] = allSettings[key] + // Don't update components for internal properties + if (key.startsWith("_")) { + return + } + if (ref?.$$set) { // Programmatically set the prop to avoid svelte reactive statements // firing inside components. This circumvents the problems caused by diff --git a/packages/client/src/components/app/embedded-map/EmbeddedMap.svelte b/packages/client/src/components/app/embedded-map/EmbeddedMap.svelte index 7738a0d345..2cfe3f497f 100644 --- a/packages/client/src/components/app/embedded-map/EmbeddedMap.svelte +++ b/packages/client/src/components/app/embedded-map/EmbeddedMap.svelte @@ -374,6 +374,11 @@ min-height: 180px; min-width: 200px; } + .embedded-map :global(a.map-svg-button) { + display: flex; + justify-content: center; + align-items: center; + } .embedded-map :global(.leaflet-top), .embedded-map :global(.leaflet-bottom) { z-index: 998; diff --git a/packages/client/src/components/app/embedded-map/EmbeddedMapControls.js b/packages/client/src/components/app/embedded-map/EmbeddedMapControls.js index ca1b1ed22a..de14190b64 100644 --- a/packages/client/src/components/app/embedded-map/EmbeddedMapControls.js +++ b/packages/client/src/components/app/embedded-map/EmbeddedMapControls.js @@ -37,7 +37,7 @@ const FullScreenControl = L.Control.extend({ this._fullScreenButton = this._createButton( options.fullScreenContent, options.fullScreenTitle, - "map-fullscreen", + "map-fullscreen map-svg-button", container, this._fullScreen ) @@ -87,7 +87,7 @@ const LocationControl = L.Control.extend({ this._locationButton = this._createButton( options.locationContent, options.locationTitle, - "map-location", + "map-location map-svg-button", container, this._location ) diff --git a/packages/client/src/licensing/features.js b/packages/client/src/licensing/features.js index 98e03b77d2..1f0d4a4870 100644 --- a/packages/client/src/licensing/features.js +++ b/packages/client/src/licensing/features.js @@ -1,6 +1,5 @@ -// import { isFreePlan } from "./utils.js" +import { isFreePlan } from "./utils.js" export const logoEnabled = () => { - return false - // return isFreePlan() + return isFreePlan() } diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index bac0f054ac..388a670376 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "1.4.3-alpha.2", + "@budibase/bbui": "1.4.8-alpha.10", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/api/groups.js b/packages/frontend-core/src/api/groups.js index ce0c8e7729..c27f11e0ea 100644 --- a/packages/frontend-core/src/api/groups.js +++ b/packages/frontend-core/src/api/groups.js @@ -1,40 +1,91 @@ -export const buildGroupsEndpoints = API => ({ - /** - * Creates a user group. - * @param user the new group to create - */ - saveGroup: async group => { +export const buildGroupsEndpoints = API => { + // underlying functionality of adding/removing users/apps to groups + async function updateGroupResource(groupId, resource, operation, ids) { + if (!Array.isArray(ids)) { + ids = [ids] + } return await API.post({ - url: "/api/global/groups", - body: group, + url: `/api/global/groups/${groupId}/${resource}`, + body: { + [operation]: ids, + }, }) - }, - /** - * Gets all of the user groups - */ - getGroups: async () => { - return await API.get({ - url: "/api/global/groups", - }) - }, + } - /** - * Gets a group by ID - */ - getGroup: async id => { - return await API.get({ - url: `/api/global/groups/${id}`, - }) - }, + return { + /** + * Creates a user group. + * @param group the new group to create + */ + saveGroup: async group => { + return await API.post({ + url: "/api/global/groups", + body: group, + }) + }, + /** + * Gets all the user groups + */ + getGroups: async () => { + return await API.get({ + url: "/api/global/groups", + }) + }, - /** - * Deletes a user group - * @param id the id of the config to delete - * @param rev the revision of the config to delete - */ - deleteGroup: async ({ id, rev }) => { - return await API.delete({ - url: `/api/global/groups/${id}/${rev}`, - }) - }, -}) + /** + * Gets a group by ID + */ + getGroup: async id => { + return await API.get({ + url: `/api/global/groups/${id}`, + }) + }, + + /** + * Deletes a user group + * @param id the id of the config to delete + * @param rev the revision of the config to delete + */ + deleteGroup: async ({ id, rev }) => { + return await API.delete({ + url: `/api/global/groups/${id}/${rev}`, + }) + }, + + /** + * Adds users to a group + * @param groupId The group to update + * @param userIds The user IDs to be added + */ + addUsersToGroup: async (groupId, userIds) => { + return updateGroupResource(groupId, "users", "add", userIds) + }, + + /** + * Removes users from a group + * @param groupId The group to update + * @param userIds The user IDs to be removed + */ + removeUsersFromGroup: async (groupId, userIds) => { + return updateGroupResource(groupId, "users", "remove", userIds) + }, + + /** + * Adds apps to a group + * @param groupId The group to update + * @param appArray Array of objects, containing the appId and roleId to be added + */ + addAppsToGroup: async (groupId, appArray) => { + return updateGroupResource(groupId, "apps", "add", appArray) + }, + + /** + * Removes apps from a group + * @param groupId The group to update + * @param appArray Array of objects, containing the appId to be removed + */ + removeAppsFromGroup: async (groupId, appArray) => { + return updateGroupResource(groupId, "apps", "remove", appArray) + }, + } +} diff --git a/packages/frontend-core/src/api/licensing.js b/packages/frontend-core/src/api/licensing.js index 16d65a20d7..c27d79d740 100644 --- a/packages/frontend-core/src/api/licensing.js +++ b/packages/frontend-core/src/api/licensing.js @@ -9,6 +9,15 @@ export const buildLicensingEndpoints = API => ({ }) }, + /** + * Delete a self hosted license key + */ + deleteLicenseKey: async () => { + return API.delete({ + url: `/api/global/license/info`, + }) + }, + /** * Get the license info - metadata about the license including the * obfuscated license key. diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index 653376aa55..39d9359e91 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -86,15 +86,19 @@ export const buildUserEndpoints = API => ({ /** * Creates multiple users. * @param users the array of user objects to create + * @param groups the array of group ids to add all users to */ createUsers: async ({ users, groups }) => { - return await API.post({ - url: "/api/global/users/bulkCreate", + const res = await API.post({ + url: "/api/global/users/bulk", body: { - users, - groups, + create: { + users, + groups, + }, }, }) + return res.created }, /** @@ -109,15 +113,18 @@ export const buildUserEndpoints = API => ({ /** * Deletes multiple users - * @param userId the ID of the user to delete + * @param userIds the ID of the user to delete */ deleteUsers: async userIds => { - return await API.post({ - url: `/api/global/users/bulkDelete`, + const res = await API.post({ + url: `/api/global/users/bulk`, body: { - userIds, + delete: { + userIds, + }, }, }) + return res.deleted }, /** @@ -151,6 +158,7 @@ export const buildUserEndpoints = API => ({ userInfo: { admin: user.admin ? { global: true } : undefined, builder: user.admin || user.builder ? { global: true } : undefined, + groups: user.groups, }, })), }) diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index 338e6e0405..e875219e88 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -158,6 +158,8 @@ export default class DataFetch { schema, query, loading: true, + cursors: [], + cursor: null, })) // Actually fetch data diff --git a/packages/frontend-core/src/fetch/UserFetch.js b/packages/frontend-core/src/fetch/UserFetch.js new file mode 100644 index 0000000000..9aeadbc0f5 --- /dev/null +++ b/packages/frontend-core/src/fetch/UserFetch.js @@ -0,0 +1,52 @@ +import { get } from "svelte/store" +import DataFetch from "./DataFetch.js" +import { TableNames } from "../constants" + +export default class UserFetch extends DataFetch { + constructor(opts) { + super({ + ...opts, + datasource: { + tableId: TableNames.USERS, + }, + }) + } + + determineFeatureFlags() { + return { + supportsSearch: true, + supportsSort: false, + supportsPagination: true, + } + } + + async getDefinition() { + return { + schema: {}, + } + } + + async getData() { + const { cursor, query } = get(this.store) + try { + // "query" normally contains a lucene query, but users uses a non-standard + // search endpoint so we use query uniquely here + const res = await this.API.searchUsers({ + page: cursor, + email: query.email, + appId: query.appId, + }) + return { + rows: res?.data || [], + hasNextPage: res?.hasNextPage || false, + cursor: res?.nextPage || null, + } + } catch (error) { + return { + rows: [], + hasNextPage: false, + error, + } + } + } +} diff --git a/packages/frontend-core/src/fetch/fetchData.js b/packages/frontend-core/src/fetch/fetchData.js index e914ff863f..4974816496 100644 --- a/packages/frontend-core/src/fetch/fetchData.js +++ b/packages/frontend-core/src/fetch/fetchData.js @@ -5,12 +5,14 @@ import RelationshipFetch from "./RelationshipFetch.js" import NestedProviderFetch from "./NestedProviderFetch.js" import FieldFetch from "./FieldFetch.js" import JSONArrayFetch from "./JSONArrayFetch.js" +import UserFetch from "./UserFetch.js" const DataFetchMap = { table: TableFetch, view: ViewFetch, query: QueryFetch, link: RelationshipFetch, + user: UserFetch, // Client specific datasource types provider: NestedProviderFetch, diff --git a/packages/frontend-core/src/themes/nord.css b/packages/frontend-core/src/themes/nord.css index c5a9b13640..11c7a3aea1 100644 --- a/packages/frontend-core/src/themes/nord.css +++ b/packages/frontend-core/src/themes/nord.css @@ -28,6 +28,7 @@ --spectrum-global-color-static-blue-600: #5680b4; --spectrum-global-color-static-blue-700: #4e79af; --spectrum-global-color-static-blue-800: #4a73a6; + --spectrum-global-color-static-blue: var(--spectrum-global-color-blue-600); --spectrum-global-color-gray-50: #2e3440; --spectrum-global-color-gray-75: #353b4a; diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index 71688981a9..587d057351 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -19,3 +19,24 @@ export const sequential = fn => { } } } + +/** + * Utility to debounce an async function and ensure a minimum delay between + * invocations is enforced. + * @param callback an async function to run + * @param minDelay the minimum delay between invocations + * @returns {Promise} a debounced version of the callback + */ +export const debounce = (callback, minDelay = 1000) => { + let timeout + return async (...params) => { + return new Promise(resolve => { + if (timeout) { + clearTimeout(timeout) + } + timeout = setTimeout(async () => { + resolve(await callback(...params)) + }, minDelay) + }) + } +} diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index bd01b6f9ff..b55bde7906 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -12,6 +12,8 @@ ENV COUCH_DB_URL=https://couchdb.budi.live:5984 ENV BUDIBASE_ENVIRONMENT=PRODUCTION ENV SERVICE=app-service ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU +ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS +ENV ACCOUNT_PORTAL_URL=https://account.budibase.app # copy files and install dependencies COPY . ./ diff --git a/packages/server/package.json b/packages/server/package.json index ee6c109f64..92dbd7a67b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "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.4.3-alpha.2", - "@budibase/client": "1.4.3-alpha.2", - "@budibase/pro": "1.4.3-alpha.2", - "@budibase/string-templates": "1.4.3-alpha.2", - "@budibase/types": "1.4.3-alpha.2", + "@budibase/backend-core": "1.4.8-alpha.10", + "@budibase/client": "1.4.8-alpha.10", + "@budibase/pro": "1.4.8-alpha.10", + "@budibase/string-templates": "1.4.8-alpha.10", + "@budibase/types": "1.4.8-alpha.10", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/scripts/dev/manage.js b/packages/server/scripts/dev/manage.js index ac04780ada..e06c6308f8 100644 --- a/packages/server/scripts/dev/manage.js +++ b/packages/server/scripts/dev/manage.js @@ -59,6 +59,7 @@ async function init() { BB_ADMIN_USER_EMAIL: "", BB_ADMIN_USER_PASSWORD: "", PLUGINS_DIR: "", + TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS", } let envFile = "" Object.keys(envFileJson).forEach(key => { diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index a9cf1a834d..8d95407268 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -47,14 +47,9 @@ import { checkAppMetadata } from "../../automations/logging" import { getUniqueRows } from "../../utilities/usageQuota/rows" import { quotas } from "@budibase/pro" import { errors, events, migrations } from "@budibase/backend-core" -import { - App, - Layout, - Screen, - MigrationType, - AppNavigation, -} from "@budibase/types" +import { App, Layout, Screen, MigrationType } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" +import { groups } from "@budibase/pro" const URL_REGEX_SLASH = /\/|\\/g @@ -501,6 +496,7 @@ const preDestroyApp = async (ctx: any) => { const postDestroyApp = async (ctx: any) => { const rowCount = ctx.rowCount + await groups.cleanupApp(ctx.params.appId) if (rowCount) { await quotas.removeRows(rowCount) } diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.ts similarity index 59% rename from packages/server/src/api/controllers/auth.js rename to packages/server/src/api/controllers/auth.ts index 51ce737c41..ef2cb29385 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.ts @@ -1,19 +1,21 @@ -const { outputProcessing } = require("../../utilities/rowProcessor") -const { InternalTables } = require("../../db/utils") -const { getFullUser } = require("../../utilities/users") -const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles") -const { getAppDB, getAppId } = require("@budibase/backend-core/context") +import { outputProcessing } from "../../utilities/rowProcessor" +import { InternalTables } from "../../db/utils" +import { getFullUser } from "../../utilities/users" +import { roles, context } from "@budibase/backend-core" +import { groups } from "@budibase/pro" + +const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC /** * Add the attributes that are session based to the current user. */ -const addSessionAttributesToUser = ctx => { +const addSessionAttributesToUser = (ctx: any) => { if (ctx.user) { ctx.body.license = ctx.user.license } } -exports.fetchSelf = async ctx => { +export async function fetchSelf(ctx: any) { let userId = ctx.user.userId || ctx.user._id /* istanbul ignore next */ if (!userId || !ctx.isAuthenticated) { @@ -21,30 +23,30 @@ exports.fetchSelf = async ctx => { return } + const appId = context.getAppId() const user = await getFullUser(ctx, userId) // this shouldn't be returned by the app self delete user.roles // forward the csrf token from the session user.csrfToken = ctx.user.csrfToken - if (getAppId()) { - const db = getAppDB() + if (appId) { + const db = context.getAppDB() + // check for group permissions + if (!user.roleId || user.roleId === PUBLIC_ROLE) { + const groupRoleId = await groups.getGroupRoleId(user, appId) + user.roleId = groupRoleId || user.roleId + } // remove the full roles structure delete user.roles try { const userTable = await db.get(InternalTables.USER_METADATA) - const metadata = await db.get(userId) - // make sure there is never a stale csrf token - delete metadata.csrfToken // specifically needs to make sure is enriched - ctx.body = await outputProcessing(userTable, { - ...user, - ...metadata, - }) - } catch (err) { + ctx.body = await outputProcessing(userTable, user) + } catch (err: any) { let response // user didn't exist in app, don't pretend they do - if (user.roleId === BUILTIN_ROLE_IDS.PUBLIC) { + if (user.roleId === PUBLIC_ROLE) { response = {} } // user has a role of some sort, return them diff --git a/packages/server/src/api/controllers/integration.js b/packages/server/src/api/controllers/integration.js index ae9be7e6fe..3d1643601b 100644 --- a/packages/server/src/api/controllers/integration.js +++ b/packages/server/src/api/controllers/integration.js @@ -8,7 +8,7 @@ exports.fetch = async function (ctx) { const defs = await getDefinitions() // for google sheets integration google verification - if (featureFlags.isEnabled(featureFlags.FeatureFlag.GOOGLE_SHEETS)) { + if (featureFlags.isEnabled(featureFlags.TenantFeatureFlag.GOOGLE_SHEETS)) { defs[SourceName.GOOGLE_SHEETS] = googlesheets.schema } diff --git a/packages/server/src/api/routes/analytics.js b/packages/server/src/api/routes/analytics.js index d13ace12d1..610d6d0c7f 100644 --- a/packages/server/src/api/routes/analytics.js +++ b/packages/server/src/api/routes/analytics.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/analytics") -const router = Router() +const router = new Router() router.get("/api/bbtel", controller.isEnabled) router.post("/api/bbtel/ping", controller.ping) diff --git a/packages/server/src/api/routes/apikeys.js b/packages/server/src/api/routes/apikeys.js index 315cffb41a..ddbd35c23c 100644 --- a/packages/server/src/api/routes/apikeys.js +++ b/packages/server/src/api/routes/apikeys.js @@ -3,7 +3,7 @@ const controller = require("../controllers/apikeys") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get("/api/keys", authorized(BUILDER), controller.fetch) diff --git a/packages/server/src/api/routes/auth.js b/packages/server/src/api/routes/auth.js deleted file mode 100644 index 153c86a62d..0000000000 --- a/packages/server/src/api/routes/auth.js +++ /dev/null @@ -1,8 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../controllers/auth") - -const router = Router() - -router.get("/api/self", controller.fetchSelf) - -module.exports = router diff --git a/packages/server/src/api/routes/auth.ts b/packages/server/src/api/routes/auth.ts new file mode 100644 index 0000000000..8a9d11fb27 --- /dev/null +++ b/packages/server/src/api/routes/auth.ts @@ -0,0 +1,8 @@ +import Router from "@koa/router" +import * as controller from "../controllers/auth" + +const router = new Router() + +router.get("/api/self", controller.fetchSelf) + +export default router diff --git a/packages/server/src/api/routes/automation.js b/packages/server/src/api/routes/automation.js index e0f4744e1e..e30a0c1113 100644 --- a/packages/server/src/api/routes/automation.js +++ b/packages/server/src/api/routes/automation.js @@ -13,7 +13,7 @@ const { } = require("../../middleware/appInfo") const { automationValidator } = require("./utils/validators") -const router = Router() +const router = new Router() router .get( diff --git a/packages/server/src/api/routes/backup.js b/packages/server/src/api/routes/backup.js index 83387ea75a..9f3b27e95a 100644 --- a/packages/server/src/api/routes/backup.js +++ b/packages/server/src/api/routes/backup.js @@ -3,7 +3,7 @@ const controller = require("../controllers/backup") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router.get("/api/backups/export", authorized(BUILDER), controller.exportAppDump) diff --git a/packages/server/src/api/routes/cloud.js b/packages/server/src/api/routes/cloud.js index 3cee889abf..c183ffb5ba 100644 --- a/packages/server/src/api/routes/cloud.js +++ b/packages/server/src/api/routes/cloud.js @@ -3,7 +3,7 @@ const controller = require("../controllers/cloud") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get("/api/cloud/export", authorized(BUILDER), controller.exportApps) diff --git a/packages/server/src/api/routes/component.js b/packages/server/src/api/routes/component.js index 0f122169aa..275f58bd6c 100644 --- a/packages/server/src/api/routes/component.js +++ b/packages/server/src/api/routes/component.js @@ -3,7 +3,7 @@ const controller = require("../controllers/component") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router.get( "/api/:appId/components/definitions", diff --git a/packages/server/src/api/routes/datasource.js b/packages/server/src/api/routes/datasource.js index 21df11b55c..23a3ea9fb0 100644 --- a/packages/server/src/api/routes/datasource.js +++ b/packages/server/src/api/routes/datasource.js @@ -11,7 +11,7 @@ const { datasourceQueryValidator, } = require("./utils/validators") -const router = Router() +const router = new Router() router .get("/api/datasources", authorized(BUILDER), datasourceController.fetch) diff --git a/packages/server/src/api/routes/deploy.js b/packages/server/src/api/routes/deploy.js index 762646435a..1f6b07c6f3 100644 --- a/packages/server/src/api/routes/deploy.js +++ b/packages/server/src/api/routes/deploy.js @@ -3,7 +3,7 @@ const controller = require("../controllers/deploy") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get("/api/deployments", authorized(BUILDER), controller.fetchDeployments) diff --git a/packages/server/src/api/routes/dev.js b/packages/server/src/api/routes/dev.js index 165149ca8b..0103219246 100644 --- a/packages/server/src/api/routes/dev.js +++ b/packages/server/src/api/routes/dev.js @@ -4,7 +4,7 @@ const env = require("../../environment") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() function redirectPath(path) { router diff --git a/packages/server/src/api/routes/integration.js b/packages/server/src/api/routes/integration.js index ebe79d978e..5469aaa27d 100644 --- a/packages/server/src/api/routes/integration.js +++ b/packages/server/src/api/routes/integration.js @@ -3,7 +3,7 @@ const controller = require("../controllers/integration") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get("/api/integrations", authorized(BUILDER), controller.fetch) diff --git a/packages/server/src/api/routes/layout.js b/packages/server/src/api/routes/layout.js index fa04b63402..76103f9cfc 100644 --- a/packages/server/src/api/routes/layout.js +++ b/packages/server/src/api/routes/layout.js @@ -3,7 +3,7 @@ const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") const controller = require("../controllers/layout") -const router = Router() +const router = new Router() router .post("/api/layouts", authorized(BUILDER), controller.save) diff --git a/packages/server/src/api/routes/metadata.js b/packages/server/src/api/routes/metadata.js index 129e72b5e7..0c2867c45a 100644 --- a/packages/server/src/api/routes/metadata.js +++ b/packages/server/src/api/routes/metadata.js @@ -7,7 +7,7 @@ const { const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .post( diff --git a/packages/server/src/api/routes/migrations.js b/packages/server/src/api/routes/migrations.js index 01e573edb3..a40111cf25 100644 --- a/packages/server/src/api/routes/migrations.js +++ b/packages/server/src/api/routes/migrations.js @@ -1,6 +1,6 @@ const Router = require("@koa/router") const migrationsController = require("../controllers/migrations") -const router = Router() +const router = new Router() const { internalApi } = require("@budibase/backend-core/auth") router diff --git a/packages/server/src/api/routes/permission.js b/packages/server/src/api/routes/permission.js index 831b6dd004..4736769f61 100644 --- a/packages/server/src/api/routes/permission.js +++ b/packages/server/src/api/routes/permission.js @@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") const { permissionValidator } = require("./utils/validators") -const router = Router() +const router = new Router() router .get("/api/permission/builtin", authorized(BUILDER), controller.fetchBuiltin) diff --git a/packages/server/src/api/routes/query.js b/packages/server/src/api/routes/query.js index 37a26f6808..14434a45c7 100644 --- a/packages/server/src/api/routes/query.js +++ b/packages/server/src/api/routes/query.js @@ -16,7 +16,7 @@ const { generateQueryValidation, } = require("../controllers/query/validation") -const router = Router() +const router = new Router() router .get("/api/queries", authorized(BUILDER), queryController.fetch) diff --git a/packages/server/src/api/routes/role.js b/packages/server/src/api/routes/role.js index 107d9ec583..a6e04e81fa 100644 --- a/packages/server/src/api/routes/role.js +++ b/packages/server/src/api/routes/role.js @@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") const { roleValidator } = require("./utils/validators") -const router = Router() +const router = new Router() router .post("/api/roles", authorized(BUILDER), roleValidator(), controller.save) diff --git a/packages/server/src/api/routes/routing.js b/packages/server/src/api/routes/routing.js index 45ccb7bb64..d7e971d507 100644 --- a/packages/server/src/api/routes/routing.js +++ b/packages/server/src/api/routes/routing.js @@ -3,7 +3,7 @@ const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") const controller = require("../controllers/routing") -const router = Router() +const router = new Router() router // gets correct structure for user role diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index e33a026126..426b89fd0f 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") const { screenValidator } = require("./utils/validators") -const router = Router() +const router = new Router() router .get("/api/screens", authorized(BUILDER), controller.fetch) diff --git a/packages/server/src/api/routes/script.js b/packages/server/src/api/routes/script.js index d0d3214db3..a4b4e4a7f5 100644 --- a/packages/server/src/api/routes/script.js +++ b/packages/server/src/api/routes/script.js @@ -3,7 +3,7 @@ const controller = require("../controllers/script") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router.post("/api/script", authorized(BUILDER), controller.save) diff --git a/packages/server/src/api/routes/table.js b/packages/server/src/api/routes/table.js index 9de36cac72..711312149a 100644 --- a/packages/server/src/api/routes/table.js +++ b/packages/server/src/api/routes/table.js @@ -9,7 +9,7 @@ const { } = require("@budibase/backend-core/permissions") const { tableValidator } = require("./utils/validators") -const router = Router() +const router = new Router() router /** diff --git a/packages/server/src/api/routes/templates.js b/packages/server/src/api/routes/templates.js index 710475db84..61a185b5c8 100644 --- a/packages/server/src/api/routes/templates.js +++ b/packages/server/src/api/routes/templates.js @@ -3,7 +3,7 @@ const controller = require("../controllers/templates") const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get("/api/templates", authorized(BUILDER), controller.fetch) diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 5cd282bb34..e85ffddee7 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -5,10 +5,12 @@ const { doInAppContext } = require("@budibase/backend-core/context") const { doInTenant } = require("@budibase/backend-core/tenancy") const { quotas, +} = require("@budibase/pro") +const { QuotaUsageType, StaticQuotaName, MonthlyQuotaName, -} = require("@budibase/pro") +} = require("@budibase/types") describe("/rows", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 5ac446d54e..a0eaf26ec6 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -6,7 +6,7 @@ const { PermissionTypes, } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get( diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index 9d57d722e1..a7045f0814 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -9,7 +9,7 @@ const { PermissionLevels, } = require("@budibase/backend-core/permissions") -const router = Router() +const router = new Router() router .get("/api/views/export", authorized(BUILDER), viewController.exportView) diff --git a/packages/server/src/api/routes/webhook.js b/packages/server/src/api/routes/webhook.js index 9635638700..9d60438a63 100644 --- a/packages/server/src/api/routes/webhook.js +++ b/packages/server/src/api/routes/webhook.js @@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized") const { BUILDER } = require("@budibase/backend-core/permissions") const { webhookValidator } = require("./utils/validators") -const router = Router() +const router = new Router() router .get("/api/webhooks", authorized(BUILDER), controller.fetch) diff --git a/packages/server/src/automations/steps/sendSmtpEmail.js b/packages/server/src/automations/steps/sendSmtpEmail.js index 71e544a00d..423363701b 100644 --- a/packages/server/src/automations/steps/sendSmtpEmail.js +++ b/packages/server/src/automations/steps/sendSmtpEmail.js @@ -21,6 +21,14 @@ exports.definition = { type: "string", title: "Send From", }, + cc: { + type: "string", + title: "CC", + }, + bcc: { + type: "string", + title: "BCC", + }, subject: { type: "string", title: "Email Subject", @@ -49,13 +57,21 @@ exports.definition = { } exports.run = async function ({ inputs }) { - let { to, from, subject, contents } = inputs + let { to, from, subject, contents, cc, bcc } = inputs if (!contents) { contents = "

No content

" } to = to || undefined try { - let response = await sendSmtpEmail(to, from, subject, contents, true) + let response = await sendSmtpEmail( + to, + from, + subject, + contents, + cc, + bcc, + true + ) return { success: true, response, diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index 75ad19b87f..381c295d18 100644 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -7,7 +7,7 @@ exports.init = () => { find: true, } - if (env.isTest()) { + if (env.isTest() && !env.COUCH_DB_URL) { dbConfig.inMemory = true dbConfig.allDbs = true } diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 298a50988a..1fa983a72a 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -52,9 +52,9 @@ const checkAuthorizedResource = async ( ) => { // get the user's roles const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC - const userRoles = await getUserRoleHierarchy(roleId, { + const userRoles = (await getUserRoleHierarchy(roleId, { idOnly: false, - }) + })) as { _id: string }[] const permError = "User does not have permission" // check if the user has the required role if (resourceRoles.length > 0) { diff --git a/packages/server/src/migrations/functions/usageQuotas/syncApps.ts b/packages/server/src/migrations/functions/usageQuotas/syncApps.ts index 24e4c21969..4770844a99 100644 --- a/packages/server/src/migrations/functions/usageQuotas/syncApps.ts +++ b/packages/server/src/migrations/functions/usageQuotas/syncApps.ts @@ -1,6 +1,7 @@ import { getTenantId } from "@budibase/backend-core/tenancy" import { getAllApps } from "@budibase/backend-core/db" -import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" +import { quotas } from "@budibase/pro" +import { QuotaUsageType, StaticQuotaName } from "@budibase/types" export const run = async () => { // get app count diff --git a/packages/server/src/migrations/functions/usageQuotas/syncRows.ts b/packages/server/src/migrations/functions/usageQuotas/syncRows.ts index b92d880f7a..540ea6e819 100644 --- a/packages/server/src/migrations/functions/usageQuotas/syncRows.ts +++ b/packages/server/src/migrations/functions/usageQuotas/syncRows.ts @@ -1,7 +1,8 @@ import { getTenantId } from "@budibase/backend-core/tenancy" import { getAllApps } from "@budibase/backend-core/db" import { getUniqueRows } from "../../../utilities/usageQuota/rows" -import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" +import { quotas } from "@budibase/pro" +import { QuotaUsageType, StaticQuotaName } from "@budibase/types" export const run = async () => { // get all rows in all apps diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts index dbc978b9bd..d0d50395b2 100644 --- a/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts +++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts @@ -1,6 +1,7 @@ import TestConfig from "../../../../tests/utilities/TestConfiguration" import * as syncApps from "../syncApps" -import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" +import { quotas } from "@budibase/pro" +import { QuotaUsageType, StaticQuotaName } from "@budibase/types" describe("syncApps", () => { let config = new TestConfig(false) diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts index 851deb5417..b403179958 100644 --- a/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts +++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts @@ -1,6 +1,7 @@ import TestConfig from "../../../../tests/utilities/TestConfiguration" import * as syncRows from "../syncRows" -import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" +import { quotas } from "@budibase/pro" +import { QuotaUsageType, StaticQuotaName } from "@budibase/types" describe("syncRows", () => { let config = new TestConfig(false) diff --git a/packages/server/src/utilities/global.js b/packages/server/src/utilities/global.js index 462ef6ba5d..6d82f79ce2 100644 --- a/packages/server/src/utilities/global.js +++ b/packages/server/src/utilities/global.js @@ -43,9 +43,10 @@ exports.updateAppRole = (user, { appId } = {}) => { } async function checkGroupRoles(user, { appId } = {}) { - let roleId = await groups.getGroupRoleId(user, appId) - user.roleId = roleId - + if (user.roleId && user.roleId !== BUILTIN_ROLE_IDS.PUBLIC) { + return user + } + user.roleId = await groups.getGroupRoleId(user, appId) return user } @@ -74,8 +75,9 @@ exports.getRawGlobalUser = async userId => { } exports.getGlobalUser = async userId => { + const appId = getAppId() let user = await exports.getRawGlobalUser(userId) - return processUser(user) + return processUser(user, { appId }) } exports.getGlobalUsers = async (users = null) => { diff --git a/packages/server/src/utilities/users.js b/packages/server/src/utilities/users.js index e769441322..3fa222e677 100644 --- a/packages/server/src/utilities/users.js +++ b/packages/server/src/utilities/users.js @@ -2,10 +2,11 @@ const { InternalTables } = require("../db/utils") const { getGlobalUser } = require("../utilities/global") const { getAppDB } = require("@budibase/backend-core/context") const { getProdAppID } = require("@budibase/backend-core/db") +const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles") exports.getFullUser = async (ctx, userId) => { const global = await getGlobalUser(userId) - let metadata + let metadata = {} try { // this will throw an error if the db doesn't exist, or there is no appId const db = getAppDB() @@ -15,9 +16,11 @@ exports.getFullUser = async (ctx, userId) => { delete global._id delete global._rev } + delete metadata.csrfToken return { - ...global, ...metadata, + ...global, + roleId: global.roleId || BUILTIN_ROLE_IDS.PUBLIC, tableId: InternalTables.USER_METADATA, // make sure the ID is always a local ID, not a global one _id: userId, diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index e08ad147d1..53f13b6e02 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -54,7 +54,15 @@ async function checkResponse(response, errorMsg, { ctx } = {}) { exports.request = request // have to pass in the tenant ID as this could be coming from an automation -exports.sendSmtpEmail = async (to, from, subject, contents, automation) => { +exports.sendSmtpEmail = async ( + to, + from, + subject, + contents, + cc, + bcc, + automation +) => { // tenant ID will be set in header const response = await fetch( checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`), @@ -65,6 +73,8 @@ exports.sendSmtpEmail = async (to, from, subject, contents, automation) => { from, contents, subject, + cc, + bcc, purpose: "custom", automation, }, diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 593ac07e79..346d54df76 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1094,12 +1094,12 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.4.3-alpha.2": - version "1.4.3-alpha.2" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.3-alpha.2.tgz#7e0ccffd393004e45c24cc633b296e2bdcd978a5" - integrity sha512-K40Hz2n0ESlfn4YWs5NL21kvY+NNX4aTatMyWBfz4ifio554ry0qZ6gZP4lxtj/shg5eedmldmDKJ2T+FiR2pA== +"@budibase/backend-core@1.4.8-alpha.10": + version "1.4.8-alpha.10" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.8-alpha.10.tgz#7cddbd81fb67d5a28d0f5d103191015b4a65e168" + integrity sha512-CyB6DOj/CuA0ZezvgU0LsojUGwQs+f8ZquvfKC+nlb7Kc/9v7lMNuZhu5Qe+9EHDUYlOjzddJQO6pGWKDpdt2w== dependencies: - "@budibase/types" "1.4.3-alpha.2" + "@budibase/types" "1.4.8-alpha.10" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" @@ -1180,13 +1180,13 @@ svelte-flatpickr "^3.2.3" svelte-portal "^1.0.0" -"@budibase/pro@1.4.3-alpha.2": - version "1.4.3-alpha.2" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.3-alpha.2.tgz#044aa5cf0e55793a914b9e0c6e931b8a9f5e8f39" - integrity sha512-TakJ8A8RY6VwxRYZ4528zD3rQOwJW422sNrUbOcOK9H08B/EFltXMZBP+Okk9sX8e9/8+0pxGQLQ6RdKHYYXww== +"@budibase/pro@1.4.8-alpha.10": + version "1.4.8-alpha.10" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.8-alpha.10.tgz#2a773b8cd70ed6e834107f54838bb088356f1de3" + integrity sha512-1U8NkzxwuyKWKTn7pN0FPf2Wgl8BtaaXwOAcmTIbv94ySsL58/sMpToCRU1ycdnwNzCSHUa8cfJil/ACURW2xw== dependencies: - "@budibase/backend-core" "1.4.3-alpha.2" - "@budibase/types" "1.4.3-alpha.2" + "@budibase/backend-core" "1.4.8-alpha.10" + "@budibase/types" "1.4.8-alpha.10" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" @@ -1209,10 +1209,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@1.4.3-alpha.2": - version "1.4.3-alpha.2" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.3-alpha.2.tgz#55d52cb8f3fc1a56b9d5b008ddc9b3e46f3e078e" - integrity sha512-2kOrlHjtjfgYqaE8JAGV5X5FHSIzeb2LqSsUDzWHIlVWk9r0/bZ2NTvAvFMT5LSImfF4A4qbNXS0ZVtqjX2qzw== +"@budibase/types@1.4.8-alpha.10": + version "1.4.8-alpha.10" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.8-alpha.10.tgz#33c03714d8ed75f39bce55a3c685917c2ff278e2" + integrity sha512-hytgdJ4UoWEeZ9xsquDC6moV2bouN5wWvD/wgv3GlGQEBOfypo5P7tz4mRLhmVJyJmcNPUHG8QeDZsyixQGf/Q== "@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 56ca048ee2..d26e979f61 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "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 3d3ad95259..663b693708 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index b2c17575c2..c66d3203e8 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -6,28 +6,31 @@ export interface CreateUserResponse { email: string } -export interface BulkCreateUsersRequest { - users: User[] - groups: any[] -} - export interface UserDetails { _id: string email: string } -export interface BulkCreateUsersResponse { - successful: UserDetails[] - unsuccessful: { email: string; reason: string }[] +export interface BulkUserRequest { + delete?: { + userIds: string[] + } + create?: { + users: User[] + groups: any[] + } } -export interface BulkDeleteUsersRequest { - userIds: string[] -} - -export interface BulkDeleteUsersResponse { - successful: UserDetails[] - unsuccessful: { _id: string; email: string; reason: string }[] +export interface BulkUserResponse { + created?: { + successful: UserDetails[] + unsuccessful: { email: string; reason: string }[] + } + deleted?: { + successful: UserDetails[] + unsuccessful: { _id: string; email: string; reason: string }[] + } + message?: string } export interface InviteUserRequest { diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index 33c96033a0..e7dcf2d89f 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -1,4 +1,4 @@ -import { Hosting } from "../../sdk" +import { Feature, Hosting, PlanType, Quotas } from "../../sdk" export interface CreateAccount { email: string @@ -22,6 +22,11 @@ export const isCreatePasswordAccount = ( account: CreateAccount ): account is CreatePassswordAccount => account.authType === AuthType.PASSWORD +export interface LicenseOverrides { + features?: Feature[] + quotas?: Quotas +} + export interface Account extends CreateAccount { // generated accountId: string @@ -31,9 +36,12 @@ export interface Account extends CreateAccount { verificationSent: boolean // licensing tier: string // deprecated + planType?: PlanType + planTier?: number stripeCustomerId?: string licenseKey?: string licenseKeyActivatedAt?: number + licenseOverrides?: LicenseOverrides } export interface PasswordAccount extends Account { diff --git a/packages/types/src/documents/global/index.ts b/packages/types/src/documents/global/index.ts index 1f8bb4a84f..84684df369 100644 --- a/packages/types/src/documents/global/index.ts +++ b/packages/types/src/documents/global/index.ts @@ -2,3 +2,4 @@ export * from "./config" export * from "./user" export * from "./userGroup" export * from "./plugin" +export * from "./quotas" diff --git a/packages/types/src/documents/global/quotas.ts b/packages/types/src/documents/global/quotas.ts new file mode 100644 index 0000000000..b90c7e0ddb --- /dev/null +++ b/packages/types/src/documents/global/quotas.ts @@ -0,0 +1,15 @@ +import { MonthlyQuotaName, StaticQuotaName } from "../../sdk" + +export interface QuotaUsage { + _id: string + _rev?: string + quotaReset: string + usageQuota: { + [key in StaticQuotaName]: number + } + monthly: { + [key: string]: { + [key in MonthlyQuotaName]: number + } + } +} diff --git a/packages/types/src/documents/global/userGroup.ts b/packages/types/src/documents/global/userGroup.ts index 86010d118b..cda74b0536 100644 --- a/packages/types/src/documents/global/userGroup.ts +++ b/packages/types/src/documents/global/userGroup.ts @@ -4,9 +4,8 @@ export interface UserGroup extends Document { name: string icon: string color: string - users: GroupUser[] - apps: string[] - roles: UserGroupRoles + users?: GroupUser[] + roles?: UserGroupRoles createdAt?: number } diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index de56740e44..73e5315713 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -133,9 +133,14 @@ export enum Event { AUTOMATION_TRIGGER_UPDATED = "automation:trigger:updated", // LICENSE - LICENSE_UPGRADED = "license:upgraded", - LICENSE_DOWNGRADED = "license:downgraded", + LICENSE_PLAN_CHANGED = "license:plan:changed", + LICENSE_TIER_CHANGED = "license:tier:changed", LICENSE_ACTIVATED = "license:activated", + LICENSE_PAYMENT_FAILED = "license:payment:failed", + LICENSE_PAYMENT_RECOVERED = "license:payment:recovered", + LICENSE_CHECKOUT_OPENED = "license:checkout:opened", + LICENSE_CHECKOUT_SUCCESS = "license:checkout:success", + LICENSE_PORTAL_OPENED = "license:portal:opened", // ACCOUNT ACCOUNT_CREATED = "account:created", diff --git a/packages/types/src/sdk/events/license.ts b/packages/types/src/sdk/events/license.ts index 771327c960..a12fc6bbb5 100644 --- a/packages/types/src/sdk/events/license.ts +++ b/packages/types/src/sdk/events/license.ts @@ -1,7 +1,37 @@ -export interface LicenseUpgradedEvent {} +import { PlanType } from "../licensing" -export interface LicenseDowngradedEvent {} +export interface LicenseTierChangedEvent { + accountId: string + from: number + to: number +} -export interface LicenseUpdatedEvent {} +export interface LicensePlanChangedEvent { + accountId: string + from: PlanType + to: PlanType +} -export interface LicenseActivatedEvent {} +export interface LicenseActivatedEvent { + accountId: string +} + +export interface LicenseCheckoutOpenedEvent { + accountId: string +} + +export interface LicenseCheckoutSuccessEvent { + accountId: string +} + +export interface LicensePortalOpenedEvent { + accountId: string +} + +export interface LicensePaymentFailedEvent { + accountId: string +} + +export interface LicensePaymentRecoveredEvent { + accountId: string +} diff --git a/packages/types/src/sdk/licensing/billing.ts b/packages/types/src/sdk/licensing/billing.ts index da2aca1615..d4365525db 100644 --- a/packages/types/src/sdk/licensing/billing.ts +++ b/packages/types/src/sdk/licensing/billing.ts @@ -12,6 +12,9 @@ export interface Subscription { cancelAt: number | null | undefined currentPeriodStart: number currentPeriodEnd: number + status: string + pastDueAt?: number | null + downgradeAt?: number } export interface Billing { diff --git a/packages/types/src/sdk/licensing/plan.ts b/packages/types/src/sdk/licensing/plan.ts index 6b226887b4..b370397534 100644 --- a/packages/types/src/sdk/licensing/plan.ts +++ b/packages/types/src/sdk/licensing/plan.ts @@ -6,6 +6,7 @@ export interface AccountPlan { export enum PlanType { FREE = "free", PRO = "pro", + TEAM = "team", BUSINESS = "business", ENTERPRISE = "enterprise", } diff --git a/packages/types/src/sdk/licensing/quota.ts b/packages/types/src/sdk/licensing/quota.ts index 578a5d98d0..2f9a8f918c 100644 --- a/packages/types/src/sdk/licensing/quota.ts +++ b/packages/types/src/sdk/licensing/quota.ts @@ -13,6 +13,8 @@ export enum QuotaType { export enum StaticQuotaName { ROWS = "rows", APPS = "apps", + USER_GROUPS = "userGroups", + PLUGINS = "plugins", } export enum MonthlyQuotaName { @@ -22,7 +24,6 @@ export enum MonthlyQuotaName { } export enum ConstantQuotaName { - QUERY_TIMEOUT_SECONDS = "queryTimeoutSeconds", AUTOMATION_LOG_RETENTION_DAYS = "automationLogRetentionDays", } @@ -54,6 +55,7 @@ export const isConstantQuota = ( export type PlanQuotas = { [PlanType.FREE]: Quotas [PlanType.PRO]: Quotas + [PlanType.TEAM]: Quotas [PlanType.BUSINESS]: Quotas [PlanType.ENTERPRISE]: Quotas } @@ -68,10 +70,11 @@ export type Quotas = { [QuotaUsageType.STATIC]: { [StaticQuotaName.ROWS]: Quota [StaticQuotaName.APPS]: Quota + [StaticQuotaName.USER_GROUPS]: Quota + [StaticQuotaName.PLUGINS]: Quota } } [QuotaType.CONSTANT]: { - [ConstantQuotaName.QUERY_TIMEOUT_SECONDS]: Quota [ConstantQuotaName.AUTOMATION_LOG_RETENTION_DAYS]: Quota } } diff --git a/packages/worker/Dockerfile b/packages/worker/Dockerfile index 883a6c299b..046b844815 100644 --- a/packages/worker/Dockerfile +++ b/packages/worker/Dockerfile @@ -23,5 +23,7 @@ ENV NODE_ENV=production ENV CLUSTER_MODE=${CLUSTER_MODE} ENV SERVICE=worker-service ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU +ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS +ENV ACCOUNT_PORTAL_URL=https://account.budibase.app CMD ["./docker_run.sh"] diff --git a/packages/worker/package.json b/packages/worker/package.json index 9a49ca9d5a..367f33ad9a 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.4.3-alpha.2", + "version": "1.4.8-alpha.10", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "1.4.3-alpha.2", - "@budibase/pro": "1.4.3-alpha.2", - "@budibase/string-templates": "1.4.3-alpha.2", - "@budibase/types": "1.4.3-alpha.2", + "@budibase/backend-core": "1.4.8-alpha.10", + "@budibase/pro": "1.4.8-alpha.10", + "@budibase/string-templates": "1.4.8-alpha.10", + "@budibase/types": "1.4.8-alpha.10", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", diff --git a/packages/worker/scripts/dev/manage.js b/packages/worker/scripts/dev/manage.js index 96f5c29af4..a4eaf37162 100644 --- a/packages/worker/scripts/dev/manage.js +++ b/packages/worker/scripts/dev/manage.js @@ -28,6 +28,7 @@ async function init() { APPS_URL: "http://localhost:4001", SERVICE: "worker-service", DEPLOYMENT_ENVIRONMENT: "development", + TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS", } let envFile = "" Object.keys(envFileJson).forEach(key => { diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 834531cd78..c27fe17ee7 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -8,7 +8,7 @@ const { checkResetPasswordCode } = require("../../../utilities/redis") const { getGlobalDB } = require("@budibase/backend-core/tenancy") const env = require("../../../environment") import { events, users as usersCore, context } from "@budibase/backend-core" -import { users } from "../../../sdk" +import sdk from "../../../sdk" import { User } from "@budibase/types" export const googleCallbackUrl = async (config: any) => { @@ -167,7 +167,11 @@ export const googlePreAuth = async (ctx: any, next: any) => { workspace: ctx.query.workspace, }) let callbackUrl = await exports.googleCallbackUrl(config) - const strategy = await google.strategyFactory(config, callbackUrl, users.save) + const strategy = await google.strategyFactory( + config, + callbackUrl, + sdk.users.save + ) return passport.authenticate(strategy, { scope: ["profile", "email"], @@ -184,7 +188,11 @@ export const googleAuth = async (ctx: any, next: any) => { workspace: ctx.query.workspace, }) const callbackUrl = await exports.googleCallbackUrl(config) - const strategy = await google.strategyFactory(config, callbackUrl, users.save) + const strategy = await google.strategyFactory( + config, + callbackUrl, + sdk.users.save + ) return passport.authenticate( strategy, @@ -214,7 +222,7 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => { chosenConfig, callbackUrl ) - return oidc.strategyFactory(enrichedConfig, users.save) + return oidc.strategyFactory(enrichedConfig, sdk.users.save) } /** diff --git a/packages/worker/src/api/controllers/global/email.js b/packages/worker/src/api/controllers/global/email.js index 125376cdc2..85e39be0da 100644 --- a/packages/worker/src/api/controllers/global/email.js +++ b/packages/worker/src/api/controllers/global/email.js @@ -10,6 +10,8 @@ exports.sendEmail = async ctx => { contents, from, subject, + cc, + bcc, automation, } = ctx.request.body let user @@ -23,6 +25,8 @@ exports.sendEmail = async ctx => { contents, from, subject, + cc, + bcc, automation, }) ctx.body = { diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index 1e5ca9beac..2bd173010f 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -24,6 +24,11 @@ export const getInfo = async (ctx: any) => { ctx.status = 200 } +export const deleteInfo = async (ctx: any) => { + await licensing.deleteLicenseInfo() + ctx.status = 200 +} + export const getQuotaUsage = async (ctx: any) => { ctx.body = await quotas.getQuotaUsage() } diff --git a/packages/worker/src/api/controllers/global/self.js b/packages/worker/src/api/controllers/global/self.ts similarity index 65% rename from packages/worker/src/api/controllers/global/self.js rename to packages/worker/src/api/controllers/global/self.ts index 4d71e636c9..685e2c8243 100644 --- a/packages/worker/src/api/controllers/global/self.js +++ b/packages/worker/src/api/controllers/global/self.ts @@ -1,39 +1,37 @@ -const { - getGlobalDB, - getTenantId, - isUserInAppTenant, -} = require("@budibase/backend-core/tenancy") -const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db") -const { user: userCache } = require("@budibase/backend-core/cache") -const { - hash, - platformLogout, - getCookie, - clearCookie, -} = require("@budibase/backend-core/utils") -const { encrypt } = require("@budibase/backend-core/encryption") -const { newid } = require("@budibase/backend-core/utils") -const { users } = require("../../../sdk") -const { Cookies } = require("@budibase/backend-core/constants") -const { events, featureFlags } = require("@budibase/backend-core") -const env = require("../../../environment") +import sdk from "../../../sdk" +import { + events, + featureFlags, + tenancy, + constants, + db as dbCore, + utils, + cache, + encryption, +} from "@budibase/backend-core" +import env from "../../../environment" +import { groups } from "@budibase/pro" +const { hash, platformLogout, getCookie, clearCookie, newid } = utils +const { user: userCache } = cache function newTestApiKey() { return env.ENCRYPTED_TEST_PUBLIC_API_KEY } function newApiKey() { - return encrypt(`${getTenantId()}${SEPARATOR}${newid()}`) + return encryption.encrypt( + `${tenancy.getTenantId()}${dbCore.SEPARATOR}${newid()}` + ) } -function cleanupDevInfo(info) { +function cleanupDevInfo(info: any) { // user doesn't need to aware of dev doc info delete info._id delete info._rev return info } -exports.generateAPIKey = async ctx => { +export async function generateAPIKey(ctx: any) { let userId let apiKey if (env.isTest() && ctx.request.body.userId) { @@ -44,8 +42,8 @@ exports.generateAPIKey = async ctx => { apiKey = newApiKey() } - const db = getGlobalDB() - const id = generateDevInfoID(userId) + const db = tenancy.getGlobalDB() + const id = dbCore.generateDevInfoID(userId) let devInfo try { devInfo = await db.get(id) @@ -57,9 +55,9 @@ exports.generateAPIKey = async ctx => { ctx.body = cleanupDevInfo(devInfo) } -exports.fetchAPIKey = async ctx => { - const db = getGlobalDB() - const id = generateDevInfoID(ctx.user._id) +export async function fetchAPIKey(ctx: any) { + const db = tenancy.getGlobalDB() + const id = dbCore.generateDevInfoID(ctx.user._id) let devInfo try { devInfo = await db.get(id) @@ -74,20 +72,20 @@ exports.fetchAPIKey = async ctx => { ctx.body = cleanupDevInfo(devInfo) } -const checkCurrentApp = ctx => { - const appCookie = getCookie(ctx, Cookies.CurrentApp) - if (appCookie && !isUserInAppTenant(appCookie.appId)) { +const checkCurrentApp = (ctx: any) => { + const appCookie = getCookie(ctx, constants.Cookies.CurrentApp) + if (appCookie && !tenancy.isUserInAppTenant(appCookie.appId)) { // there is a currentapp cookie from another tenant // remove the cookie as this is incompatible with the builder // due to builder and admin permissions being removed - clearCookie(ctx, Cookies.CurrentApp) + clearCookie(ctx, constants.Cookies.CurrentApp) } } /** * Add the attributes that are session based to the current user. */ -const addSessionAttributesToUser = ctx => { +const addSessionAttributesToUser = (ctx: any) => { ctx.body.account = ctx.user.account ctx.body.license = ctx.user.license ctx.body.budibaseAccess = !!ctx.user.budibaseAccess @@ -95,9 +93,9 @@ const addSessionAttributesToUser = ctx => { ctx.body.csrfToken = ctx.user.csrfToken } -const sanitiseUserUpdate = ctx => { +const sanitiseUserUpdate = (ctx: any) => { const allowed = ["firstName", "lastName", "password", "forceResetPassword"] - const resp = {} + const resp: { [key: string]: any } = {} for (let [key, value] of Object.entries(ctx.request.body)) { if (allowed.includes(key)) { resp[key] = value @@ -106,7 +104,7 @@ const sanitiseUserUpdate = ctx => { return resp } -exports.getSelf = async ctx => { +export async function getSelf(ctx: any) { if (!ctx.user) { ctx.throw(403, "User not logged in") } @@ -118,17 +116,18 @@ exports.getSelf = async ctx => { checkCurrentApp(ctx) // get the main body of the user - ctx.body = await users.getUser(userId) + const user = await sdk.users.getUser(userId) + ctx.body = await groups.enrichUserRolesFromGroups(user) // add the feature flags for this tenant - const tenantId = getTenantId() + const tenantId = tenancy.getTenantId() ctx.body.featureFlags = featureFlags.getTenantFeatureFlags(tenantId) addSessionAttributesToUser(ctx) } -exports.updateSelf = async ctx => { - const db = getGlobalDB() +export async function updateSelf(ctx: any) { + const db = tenancy.getGlobalDB() const user = await db.get(ctx.user._id) let passwordChange = false diff --git a/packages/worker/src/api/controllers/global/templates.js b/packages/worker/src/api/controllers/global/templates.ts similarity index 59% rename from packages/worker/src/api/controllers/global/templates.js rename to packages/worker/src/api/controllers/global/templates.ts index b16e9423ec..0abce704c7 100644 --- a/packages/worker/src/api/controllers/global/templates.js +++ b/packages/worker/src/api/controllers/global/templates.ts @@ -1,20 +1,19 @@ -const { generateTemplateID } = require("@budibase/backend-core/db") -const { +import { TemplateMetadata, TemplateBindings, GLOBAL_OWNER, -} = require("../../../constants") -const { getTemplates } = require("../../../constants/templates") -const { getGlobalDB } = require("@budibase/backend-core/tenancy") +} from "../../../constants" +import { getTemplates } from "../../../constants/templates" +import { tenancy, db as dbCore } from "@budibase/backend-core" -exports.save = async ctx => { - const db = getGlobalDB() +export async function save(ctx: any) { + const db = tenancy.getGlobalDB() let template = ctx.request.body if (!template.ownerId) { template.ownerId = GLOBAL_OWNER } if (!template._id) { - template._id = generateTemplateID(template.ownerId) + template._id = dbCore.generateTemplateID(template.ownerId) } const response = await db.put(template) @@ -24,9 +23,9 @@ exports.save = async ctx => { } } -exports.definitions = async ctx => { - const bindings = {} - const info = {} +export async function definitions(ctx: any) { + const bindings: any = {} + const info: any = {} for (let template of TemplateMetadata.email) { bindings[template.purpose] = template.bindings info[template.purpose] = { @@ -45,30 +44,33 @@ exports.definitions = async ctx => { } } -exports.fetch = async ctx => { +export async function fetch(ctx: any) { ctx.body = await getTemplates() } -exports.fetchByType = async ctx => { +export async function fetchByType(ctx: any) { + // @ts-ignore ctx.body = await getTemplates({ type: ctx.params.type, }) } -exports.fetchByOwner = async ctx => { +export async function fetchByOwner(ctx: any) { + // @ts-ignore ctx.body = await getTemplates({ ownerId: ctx.params.ownerId, }) } -exports.find = async ctx => { +export async function find(ctx: any) { + // @ts-ignore ctx.body = await getTemplates({ id: ctx.params.id, }) } -exports.destroy = async ctx => { - const db = getGlobalDB() +export async function destroy(ctx: any) { + const db = tenancy.getGlobalDB() await db.remove(ctx.params.id, ctx.params.rev) ctx.message = `Template ${ctx.params.id} deleted.` ctx.status = 200 diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index ea9375f238..8894330f67 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,8 +1,9 @@ import { checkInviteCode } from "../../../utilities/redis" -import { users } from "../../../sdk" +import sdk from "../../../sdk" import env from "../../../environment" import { - BulkDeleteUsersRequest, + BulkUserRequest, + BulkUserResponse, CloudAccount, InviteUserRequest, InviteUsersRequest, @@ -16,46 +17,48 @@ import { tenancy, } 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) => { try { - ctx.body = await users.save(ctx.request.body) + ctx.body = await sdk.users.save(ctx.request.body) } catch (err: any) { ctx.throw(err.status || 400, err) } } -export const bulkCreate = async (ctx: any) => { - let { users: newUsersRequested, groups } = ctx.request.body +const bulkDelete = async (userIds: string[], currentUserId: string) => { + if (userIds?.indexOf(currentUserId) !== -1) { + throw new Error("Unable to delete self.") + } + return await sdk.users.bulkDelete(userIds) +} - if (!env.SELF_HOSTED && newUsersRequested.length > MAX_USERS_UPLOAD_LIMIT) { - ctx.throw( - 400, +const bulkCreate = async (users: User[], groupIds: string[]) => { + if (!env.SELF_HOSTED && users.length > MAX_USERS_UPLOAD_LIMIT) { + throw new Error( "Max limit for upload is 1000 users. Please reduce file size and try again." ) } + return await sdk.users.bulkCreate(users, groupIds) +} - const db = tenancy.getGlobalDB() - let groupsToSave: any[] = [] - - if (groups.length) { - for (const groupId of groups) { - let oldGroup = await db.get(groupId) - groupsToSave.push(oldGroup) - } - } - +export const bulkUpdate = async (ctx: any) => { + const currentUserId = ctx.user._id + const input = ctx.request.body as BulkUserRequest + let created, deleted try { - const response = await users.bulkCreate(newUsersRequested, groups) - await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful) - - ctx.body = response + if (input.create) { + created = await bulkCreate(input.create.users, input.create.groups) + } + if (input.delete) { + deleted = await bulkDelete(input.delete.userIds, currentUserId) + } } catch (err: any) { - ctx.throw(err.status || 400, err) + ctx.throw(err.status || 400, err?.message || err) } + ctx.body = { created, deleted } as BulkUserResponse } const parseBooleanParam = (param: any) => { @@ -99,7 +102,7 @@ export const adminUser = async (ctx: any) => { // always bust checklist beforehand, if an error occurs but can proceed, don't get // stuck in a cycle await cache.bustCache(cache.CacheKeys.CHECKLIST) - const finalUser = await users.save(user, { + const finalUser = await sdk.users.save(user, { hashPassword, requirePassword, }) @@ -121,7 +124,7 @@ export const adminUser = async (ctx: any) => { export const countByApp = async (ctx: any) => { const appId = ctx.params.appId try { - ctx.body = await users.countUsersByApp(appId) + ctx.body = await sdk.users.countUsersByApp(appId) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -133,28 +136,15 @@ export const destroy = async (ctx: any) => { ctx.throw(400, "Unable to delete self.") } - await users.destroy(id, ctx.user) + await sdk.users.destroy(id, ctx.user) ctx.body = { message: `User ${id} deleted.`, } } -export const bulkDelete = async (ctx: any) => { - const { userIds } = ctx.request.body as BulkDeleteUsersRequest - if (userIds?.indexOf(ctx.user._id) !== -1) { - ctx.throw(400, "Unable to delete self.") - } - - try { - ctx.body = await users.bulkDelete(userIds) - } catch (err) { - ctx.throw(err) - } -} - export const search = async (ctx: any) => { - const paginated = await users.paginatedUsers(ctx.request.body) + const paginated = await sdk.users.paginatedUsers(ctx.request.body) // user hashed password shouldn't ever be returned for (let user of paginated.data) { if (user) { @@ -166,7 +156,7 @@ export const search = async (ctx: any) => { // called internally by app server user fetch export const fetch = async (ctx: any) => { - const all = await users.allUsers() + const all = await sdk.users.allUsers() // user hashed password shouldn't ever be returned for (let user of all) { if (user) { @@ -178,7 +168,7 @@ export const fetch = async (ctx: any) => { // called internally by app server user find export const find = async (ctx: any) => { - ctx.body = await users.getUser(ctx.params.id) + ctx.body = await sdk.users.getUser(ctx.params.id) } export const tenantUserLookup = async (ctx: any) => { @@ -193,7 +183,7 @@ export const tenantUserLookup = async (ctx: any) => { export const invite = async (ctx: any) => { const request = ctx.request.body as InviteUserRequest - const response = await users.invite([request]) + const response = await sdk.users.invite([request]) // explicitly throw for single user invite if (response.unsuccessful.length) { @@ -212,7 +202,7 @@ export const invite = async (ctx: any) => { export const inviteMultiple = async (ctx: any) => { const request = ctx.request.body as InviteUsersRequest - ctx.body = await users.invite(request) + ctx.body = await sdk.users.invite(request) } export const inviteAccept = async (ctx: any) => { @@ -221,7 +211,7 @@ export const inviteAccept = async (ctx: any) => { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) ctx.body = await tenancy.doInTenant(info.tenantId, async () => { - const saved = await users.save({ + const saved = await sdk.users.save({ firstName, lastName, password, diff --git a/packages/worker/src/api/controllers/system/accounts.ts b/packages/worker/src/api/controllers/system/accounts.ts index 5e72f35bab..0aa5f25785 100644 --- a/packages/worker/src/api/controllers/system/accounts.ts +++ b/packages/worker/src/api/controllers/system/accounts.ts @@ -1,21 +1,21 @@ import { Account, AccountMetadata } from "@budibase/types" -import { accounts } from "../../../sdk" +import sdk from "../../../sdk" export const save = async (ctx: any) => { const account = ctx.request.body as Account let metadata: AccountMetadata = { - _id: accounts.formatAccountMetadataId(account.accountId), + _id: sdk.accounts.formatAccountMetadataId(account.accountId), email: account.email, } - metadata = await accounts.saveMetadata(metadata) + metadata = await sdk.accounts.saveMetadata(metadata) ctx.body = metadata ctx.status = 200 } export const destroy = async (ctx: any) => { - const accountId = accounts.formatAccountMetadataId(ctx.params.accountId) - await accounts.destroyMetadata(accountId) + const accountId = sdk.accounts.formatAccountMetadataId(ctx.params.accountId) + await sdk.accounts.destroyMetadata(accountId) ctx.status = 204 } diff --git a/packages/worker/src/api/routes/global/auth.js b/packages/worker/src/api/routes/global/auth.js index 07d95f808d..1c292cdc7f 100644 --- a/packages/worker/src/api/routes/global/auth.js +++ b/packages/worker/src/api/routes/global/auth.js @@ -4,7 +4,7 @@ const { joiValidator } = require("@budibase/backend-core/auth") const Joi = require("joi") const { updateTenantId } = require("@budibase/backend-core/tenancy") -const router = Router() +const router = new Router() function buildAuthValidation() { // prettier-ignore diff --git a/packages/worker/src/api/routes/global/configs.js b/packages/worker/src/api/routes/global/configs.js index e08611b73a..a7cd1a38e8 100644 --- a/packages/worker/src/api/routes/global/configs.js +++ b/packages/worker/src/api/routes/global/configs.js @@ -5,7 +5,7 @@ const { adminOnly } = require("@budibase/backend-core/auth") const Joi = require("joi") const { Configs } = require("../../../constants") -const router = Router() +const router = new Router() function smtpValidation() { // prettier-ignore diff --git a/packages/worker/src/api/routes/global/email.js b/packages/worker/src/api/routes/global/email.js index 940bb4d134..962aea8d14 100644 --- a/packages/worker/src/api/routes/global/email.js +++ b/packages/worker/src/api/routes/global/email.js @@ -5,14 +5,20 @@ const { joiValidator } = require("@budibase/backend-core/auth") const { adminOnly } = require("@budibase/backend-core/auth") const Joi = require("joi") -const router = Router() +const router = new Router() function buildEmailSendValidation() { // prettier-ignore return joiValidator.body(Joi.object({ email: Joi.string().email({ multiple: true, - }), + }), + cc: Joi.string().email({ + multiple: true, + }).allow("", null), + bcc: Joi.string().email({ + multiple: true, + }).allow("", null), purpose: Joi.string().valid(...Object.values(EmailTemplatePurpose)), workspaceId: Joi.string().allow("", null), from: Joi.string().allow("", null), diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index b9f5aa3218..03908e052b 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -7,6 +7,7 @@ router .post("/api/global/license/activate", controller.activate) .post("/api/global/license/refresh", controller.refresh) .get("/api/global/license/info", controller.getInfo) + .delete("/api/global/license/info", controller.deleteInfo) .get("/api/global/license/usage", controller.getQuotaUsage) export = router diff --git a/packages/worker/src/api/routes/global/roles.js b/packages/worker/src/api/routes/global/roles.js index d99e0e5b56..da7d5405ad 100644 --- a/packages/worker/src/api/routes/global/roles.js +++ b/packages/worker/src/api/routes/global/roles.js @@ -2,7 +2,7 @@ const Router = require("@koa/router") const controller = require("../../controllers/global/roles") const { builderOrAdmin } = require("@budibase/backend-core/auth") -const router = Router() +const router = new Router() router .get("/api/global/roles", builderOrAdmin, controller.fetch) diff --git a/packages/worker/src/api/routes/global/self.js b/packages/worker/src/api/routes/global/self.js deleted file mode 100644 index 1683a94f37..0000000000 --- a/packages/worker/src/api/routes/global/self.js +++ /dev/null @@ -1,18 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/global/self") -const { builderOnly } = require("@budibase/backend-core/auth") -const { users } = require("../validation") - -const router = Router() - -router - .post("/api/global/self/api_key", builderOnly, controller.generateAPIKey) - .get("/api/global/self/api_key", builderOnly, controller.fetchAPIKey) - .get("/api/global/self", controller.getSelf) - .post( - "/api/global/self", - users.buildUserSaveValidation(true), - controller.updateSelf - ) - -module.exports = router diff --git a/packages/worker/src/api/routes/global/self.ts b/packages/worker/src/api/routes/global/self.ts new file mode 100644 index 0000000000..4b52225783 --- /dev/null +++ b/packages/worker/src/api/routes/global/self.ts @@ -0,0 +1,18 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/global/self" +import { auth } from "@budibase/backend-core" +import { users } from "../validation" + +const router = new Router() + +router + .post("/api/global/self/api_key", auth.builderOnly, controller.generateAPIKey) + .get("/api/global/self/api_key", auth.builderOnly, controller.fetchAPIKey) + .get("/api/global/self", controller.getSelf) + .post( + "/api/global/self", + users.buildUserSaveValidation(true), + controller.updateSelf + ) + +export default router as any diff --git a/packages/worker/src/api/routes/global/templates.js b/packages/worker/src/api/routes/global/templates.ts similarity index 72% rename from packages/worker/src/api/routes/global/templates.js rename to packages/worker/src/api/routes/global/templates.ts index 321e0543ad..2db9b5009e 100644 --- a/packages/worker/src/api/routes/global/templates.js +++ b/packages/worker/src/api/routes/global/templates.ts @@ -1,11 +1,11 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/global/templates") -const { joiValidator } = require("@budibase/backend-core/auth") -const Joi = require("joi") -const { TemplatePurpose, TemplateTypes } = require("../../../constants") -const { adminOnly } = require("@budibase/backend-core/auth") +import Router from "@koa/router" +import * as controller from "../../controllers/global/templates" +import { TemplatePurpose, TemplateTypes } from "../../../constants" +import { auth as authCore } from "@budibase/backend-core" +import Joi from "joi" +const { adminOnly, joiValidator } = authCore -const router = Router() +const router = new Router() function buildTemplateSaveValidation() { // prettier-ignore @@ -34,4 +34,4 @@ router .get("/api/global/template/:id", controller.find) .delete("/api/global/template/:id/:rev", adminOnly, controller.destroy) -module.exports = router +export default router diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index fd9ef7ff9f..218bc60800 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -97,16 +97,16 @@ describe("/api/global/users", () => { }) }) - describe("bulkCreate", () => { + describe("bulk (create)", () => { it("should ignore users existing in the same tenant", async () => { const user = await config.createUser() jest.clearAllMocks() const response = await api.users.bulkCreateUsers([user]) - expect(response.successful.length).toBe(0) - expect(response.unsuccessful.length).toBe(1) - expect(response.unsuccessful[0].email).toBe(user.email) + expect(response.created?.successful.length).toBe(0) + expect(response.created?.unsuccessful.length).toBe(1) + expect(response.created?.unsuccessful[0].email).toBe(user.email) expect(events.user.created).toBeCalledTimes(0) }) @@ -117,9 +117,9 @@ describe("/api/global/users", () => { await tenancy.doInTenant(TENANT_1, async () => { const response = await api.users.bulkCreateUsers([user]) - expect(response.successful.length).toBe(0) - expect(response.unsuccessful.length).toBe(1) - expect(response.unsuccessful[0].email).toBe(user.email) + expect(response.created?.successful.length).toBe(0) + expect(response.created?.unsuccessful.length).toBe(1) + expect(response.created?.unsuccessful[0].email).toBe(user.email) expect(events.user.created).toBeCalledTimes(0) }) }) @@ -132,24 +132,24 @@ describe("/api/global/users", () => { const response = await api.users.bulkCreateUsers([user]) - expect(response.successful.length).toBe(0) - expect(response.unsuccessful.length).toBe(1) - expect(response.unsuccessful[0].email).toBe(user.email) + expect(response.created?.successful.length).toBe(0) + expect(response.created?.unsuccessful.length).toBe(1) + expect(response.created?.unsuccessful[0].email).toBe(user.email) expect(events.user.created).toBeCalledTimes(0) }) - it("should be able to bulkCreate users", async () => { + it("should be able to bulk create users", async () => { const builder = structures.users.builderUser() const admin = structures.users.adminUser() const user = structures.users.user() const response = await api.users.bulkCreateUsers([builder, admin, user]) - expect(response.successful.length).toBe(3) - expect(response.successful[0].email).toBe(builder.email) - expect(response.successful[1].email).toBe(admin.email) - expect(response.successful[2].email).toBe(user.email) - expect(response.unsuccessful.length).toBe(0) + expect(response.created?.successful.length).toBe(3) + expect(response.created?.successful[0].email).toBe(builder.email) + expect(response.created?.successful[1].email).toBe(admin.email) + expect(response.created?.successful[2].email).toBe(user.email) + expect(response.created?.unsuccessful.length).toBe(0) expect(events.user.created).toBeCalledTimes(3) expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2) @@ -420,33 +420,30 @@ describe("/api/global/users", () => { }) }) - describe("bulkDelete", () => { - it("should not be able to bulkDelete current user", async () => { + describe("bulk (delete)", () => { + it("should not be able to bulk delete current user", async () => { const user = await config.defaultUser! - const request = { userIds: [user._id!] } - const response = await api.users.bulkDeleteUsers(request, 400) + const response = await api.users.bulkDeleteUsers([user._id!], 400) - expect(response.body.message).toBe("Unable to delete self.") + expect(response.message).toBe("Unable to delete self.") expect(events.user.deleted).not.toBeCalled() }) - it("should not be able to bulkDelete account owner", async () => { + it("should not be able to bulk delete account owner", async () => { const user = await config.createUser() const account = structures.accounts.cloudAccount() account.budibaseUserId = user._id! mocks.accounts.getAccountByTenantId.mockReturnValue(account) - const request = { userIds: [user._id!] } + const response = await api.users.bulkDeleteUsers([user._id!]) - const response = await api.users.bulkDeleteUsers(request) - - expect(response.body.successful.length).toBe(0) - expect(response.body.unsuccessful.length).toBe(1) - expect(response.body.unsuccessful[0].reason).toBe( + expect(response.deleted?.successful.length).toBe(0) + expect(response.deleted?.unsuccessful.length).toBe(1) + expect(response.deleted?.unsuccessful[0].reason).toBe( "Account holder cannot be deleted" ) - expect(response.body.unsuccessful[0]._id).toBe(user._id) + expect(response.deleted?.unsuccessful[0]._id).toBe(user._id) expect(events.user.deleted).not.toBeCalled() }) @@ -462,12 +459,14 @@ describe("/api/global/users", () => { admin, user, ]) - const request = { userIds: createdUsers.successful.map(u => u._id!) } - const response = await api.users.bulkDeleteUsers(request) + const toDelete = createdUsers.created?.successful.map( + u => u._id! + ) as string[] + const response = await api.users.bulkDeleteUsers(toDelete) - expect(response.body.successful.length).toBe(3) - expect(response.body.unsuccessful.length).toBe(0) + expect(response.deleted?.successful.length).toBe(3) + expect(response.deleted?.unsuccessful.length).toBe(0) expect(events.user.deleted).toBeCalledTimes(3) expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2) diff --git a/packages/worker/src/api/routes/global/users.js b/packages/worker/src/api/routes/global/users.js index e0a221a795..2d9b1d9ac9 100644 --- a/packages/worker/src/api/routes/global/users.js +++ b/packages/worker/src/api/routes/global/users.js @@ -8,7 +8,7 @@ const { users } = require("../validation") const selfController = require("../../controllers/global/self") const { builderOrAdmin } = require("@budibase/backend-core/auth") -const router = Router() +const router = new Router() function buildAdminInitValidation() { return joiValidator.body( @@ -56,16 +56,15 @@ router controller.save ) .post( - "/api/global/users/bulkCreate", + "/api/global/users/bulk", adminOnly, - users.buildUserBulkSaveValidation(), - controller.bulkCreate + users.buildUserBulkUserValidation(), + controller.bulkUpdate ) .get("/api/global/users", builderOrAdmin, controller.fetch) .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", builderOrAdmin, controller.countByApp) .get("/api/global/roles/:appId") .post( @@ -74,12 +73,6 @@ router buildInviteValidation(), controller.invite ) - .post( - "/api/global/users/invite", - adminOnly, - buildInviteValidation(), - controller.invite - ) .post( "/api/global/users/multi/invite", adminOnly, diff --git a/packages/worker/src/api/routes/global/workspaces.js b/packages/worker/src/api/routes/global/workspaces.js index 0198991bfa..c0e172cd8d 100644 --- a/packages/worker/src/api/routes/global/workspaces.js +++ b/packages/worker/src/api/routes/global/workspaces.js @@ -4,7 +4,7 @@ const { joiValidator } = require("@budibase/backend-core/auth") const { adminOnly } = require("@budibase/backend-core/auth") const Joi = require("joi") -const router = Router() +const router = new Router() function buildWorkspaceSaveValidation() { // prettier-ignore diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js deleted file mode 100644 index 7f5c783caa..0000000000 --- a/packages/worker/src/api/routes/index.js +++ /dev/null @@ -1,34 +0,0 @@ -const { api } = require("@budibase/pro") -const userRoutes = require("./global/users") -const configRoutes = require("./global/configs") -const workspaceRoutes = require("./global/workspaces") -const templateRoutes = require("./global/templates") -const emailRoutes = require("./global/email") -const authRoutes = require("./global/auth") -const roleRoutes = require("./global/roles") -const environmentRoutes = require("./system/environment") -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") -const accountRoutes = require("./system/accounts") - -let userGroupRoutes = api.groups -exports.routes = [ - configRoutes, - userRoutes, - workspaceRoutes, - authRoutes, - templateRoutes, - tenantsRoutes, - emailRoutes, - roleRoutes, - environmentRoutes, - statusRoutes, - selfRoutes, - licenseRoutes, - userGroupRoutes, - migrationRoutes, - accountRoutes, -] diff --git a/packages/worker/src/api/routes/index.ts b/packages/worker/src/api/routes/index.ts new file mode 100644 index 0000000000..67edf5d51b --- /dev/null +++ b/packages/worker/src/api/routes/index.ts @@ -0,0 +1,34 @@ +import { api } from "@budibase/pro" +import userRoutes from "./global/users" +import configRoutes from "./global/configs" +import workspaceRoutes from "./global/workspaces" +import templateRoutes from "./global/templates" +import emailRoutes from "./global/email" +import authRoutes from "./global/auth" +import roleRoutes from "./global/roles" +import environmentRoutes from "./system/environment" +import tenantsRoutes from "./system/tenants" +import statusRoutes from "./system/status" +import selfRoutes from "./global/self" +import licenseRoutes from "./global/license" +import migrationRoutes from "./system/migrations" +import accountRoutes from "./system/accounts" + +let userGroupRoutes = api.groups +export const routes = [ + configRoutes, + userRoutes, + workspaceRoutes, + authRoutes, + templateRoutes, + tenantsRoutes, + emailRoutes, + roleRoutes, + environmentRoutes, + statusRoutes, + selfRoutes, + licenseRoutes, + userGroupRoutes, + migrationRoutes, + accountRoutes, +] diff --git a/packages/worker/src/api/routes/system/environment.js b/packages/worker/src/api/routes/system/environment.js index 9b1b85638f..3d34046317 100644 --- a/packages/worker/src/api/routes/system/environment.js +++ b/packages/worker/src/api/routes/system/environment.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../../controllers/system/environment") -const router = Router() +const router = new Router() router.get("/api/system/environment", controller.fetch) diff --git a/packages/worker/src/api/routes/system/status.js b/packages/worker/src/api/routes/system/status.js index a39801375b..17d2f8a5a6 100644 --- a/packages/worker/src/api/routes/system/status.js +++ b/packages/worker/src/api/routes/system/status.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../../controllers/system/status") -const router = Router() +const router = new Router() router.get("/api/system/status", controller.fetch) diff --git a/packages/worker/src/api/routes/system/tenants.js b/packages/worker/src/api/routes/system/tenants.js index 451f09f773..6247e76058 100644 --- a/packages/worker/src/api/routes/system/tenants.js +++ b/packages/worker/src/api/routes/system/tenants.js @@ -2,7 +2,7 @@ const Router = require("@koa/router") const controller = require("../../controllers/system/tenants") const { adminOnly } = require("@budibase/backend-core/auth") -const router = Router() +const router = new Router() router .get("/api/system/tenants/:tenantId/exists", controller.exists) diff --git a/packages/worker/src/api/routes/system/tests/accounts.spec.ts b/packages/worker/src/api/routes/system/tests/accounts.spec.ts index e3a6141cb7..f977d22cd9 100644 --- a/packages/worker/src/api/routes/system/tests/accounts.spec.ts +++ b/packages/worker/src/api/routes/system/tests/accounts.spec.ts @@ -1,4 +1,4 @@ -import { accounts } from "../../../../sdk" +import sdk from "../../../../sdk" import { TestConfiguration, structures, API } from "../../../../tests" import { v4 as uuid } from "uuid" @@ -25,8 +25,8 @@ describe("accounts", () => { const response = await api.accounts.saveMetadata(account) - const id = accounts.formatAccountMetadataId(account.accountId) - const metadata = await accounts.getMetadata(id) + const id = sdk.accounts.formatAccountMetadataId(account.accountId) + const metadata = await sdk.accounts.getMetadata(id) expect(response).toStrictEqual(metadata) }) }) @@ -38,7 +38,7 @@ describe("accounts", () => { await api.accounts.destroyMetadata(account.accountId) - const deleted = await accounts.getMetadata(account.accountId) + const deleted = await sdk.accounts.getMetadata(account.accountId) expect(deleted).toBe(undefined) }) diff --git a/packages/worker/src/api/routes/validation/users.ts b/packages/worker/src/api/routes/validation/users.ts index d84ae94ee6..0cb14c047e 100644 --- a/packages/worker/src/api/routes/validation/users.ts +++ b/packages/worker/src/api/routes/validation/users.ts @@ -28,7 +28,7 @@ export const buildUserSaveValidation = (isSelf = false) => { return joiValidator.body(Joi.object(schema).required().unknown(true)) } -export const buildUserBulkSaveValidation = (isSelf = false) => { +export const buildUserBulkUserValidation = (isSelf = false) => { if (!isSelf) { schema = { ...schema, @@ -36,10 +36,15 @@ export const buildUserBulkSaveValidation = (isSelf = false) => { _rev: Joi.string(), } } - let bulkSaveSchema = { - groups: Joi.array().optional(), - users: Joi.array().items(Joi.object(schema).required().unknown(true)), + let bulkSchema = { + create: Joi.object({ + groups: Joi.array().optional(), + users: Joi.array().items(Joi.object(schema).required().unknown(true)), + }), + delete: Joi.object({ + userIds: Joi.array().items(Joi.string()), + }), } - return joiValidator.body(Joi.object(bulkSaveSchema).required().unknown(true)) + return joiValidator.body(Joi.object(bulkSchema).required().unknown(true)) } diff --git a/packages/worker/src/db/index.js b/packages/worker/src/db/index.js index 25dc02962b..58fd8484ff 100644 --- a/packages/worker/src/db/index.js +++ b/packages/worker/src/db/index.js @@ -3,7 +3,7 @@ const env = require("../environment") exports.init = () => { const dbConfig = {} - if (env.isTest()) { + if (env.isTest() && !env.COUCH_DB_URL) { dbConfig.inMemory = true dbConfig.allDbs = true } diff --git a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts index cae6c6af51..941791fe93 100644 --- a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts +++ b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts @@ -1,5 +1,5 @@ import { User } from "@budibase/types" -import * as sdk from "../../sdk" +import sdk from "../../sdk" /** * Date: diff --git a/packages/worker/src/sdk/index.ts b/packages/worker/src/sdk/index.ts index fdc1098361..5febb7ba3c 100644 --- a/packages/worker/src/sdk/index.ts +++ b/packages/worker/src/sdk/index.ts @@ -1,2 +1,7 @@ -export * as users from "./users" -export * as accounts from "./accounts" +import * as users from "./users" +import * as accounts from "./accounts" + +export default { + users, + accounts, +} diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 4e030f5e61..775514ea5e 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -19,9 +19,7 @@ import { import { AccountMetadata, AllDocsResponse, - BulkCreateUsersResponse, - BulkDeleteUsersResponse, - BulkDocsResponse, + BulkUserResponse, CloudAccount, CreateUserResponse, InviteUsersRequest, @@ -31,9 +29,9 @@ import { RowResponse, User, } from "@budibase/types" -import { groups as groupUtils } from "@budibase/pro" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" +import { groups as groupsSdk } from "@budibase/pro" const PAGE_LIMIT = 8 @@ -349,8 +347,7 @@ const searchExistingEmails = async (emails: string[]) => { export const bulkCreate = async ( newUsersRequested: User[], groups: string[] -): Promise => { - const db = tenancy.getGlobalDB() +): Promise => { const tenantId = tenancy.getTenantId() let usersToSave: any[] = [] @@ -392,9 +389,9 @@ export const bulkCreate = async ( }) const usersToBulkSave = await Promise.all(usersToSave) - await db.bulkDocs(usersToBulkSave) + await usersCore.bulkUpdateGlobalUsers(usersToBulkSave) - // Post processing of bulk added users, i.e events and cache operations + // Post-processing of bulk added users, e.g. 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 @@ -410,6 +407,16 @@ export const bulkCreate = async ( } }) + // now update the groups + if (Array.isArray(saved) && groups) { + const groupPromises = [] + const createdUserIds = saved.map(user => user._id) + for (let groupId of groups) { + groupPromises.push(groupsSdk.addUsers(groupId, createdUserIds)) + } + await Promise.all(groupPromises) + } + return { successful: saved, unsuccessful, @@ -438,10 +445,10 @@ const getAccountHolderFromUserIds = async ( export const bulkDelete = async ( userIds: string[] -): Promise => { +): Promise => { const db = tenancy.getGlobalDB() - const response: BulkDeleteUsersResponse = { + const response: BulkUserResponse["deleted"] = { successful: [], unsuccessful: [], } @@ -458,7 +465,6 @@ export const bulkDelete = async ( }) } - let groupsToModify: any = {} // Get users and delete const allDocsResponse: AllDocsResponse = await db.allDocs({ include_docs: true, @@ -466,33 +472,16 @@ export const bulkDelete = async ( }) const usersToDelete: User[] = allDocsResponse.rows.map( (user: RowResponse) => { - // if we find a user that has an associated group, add it to - // an array so we can easily use allDocs on them later. - // This prevents us having to re-loop over all the users - if (user.doc.userGroups) { - for (let groupId of user.doc.userGroups) { - if (!Object.keys(groupsToModify).includes(groupId)) { - groupsToModify[groupId] = [user.id] - } else { - groupsToModify[groupId] = [...groupsToModify[groupId], user.id] - } - } - } - return user.doc } ) // Delete from DB - const dbResponse: BulkDocsResponse = await db.bulkDocs( - usersToDelete.map(user => ({ - ...user, - _deleted: true, - })) - ) - - // Deletion post processing - await groupUtils.bulkDeleteGroupUsers(groupsToModify) + const toDelete = usersToDelete.map(user => ({ + ...user, + _deleted: true, + })) + const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) for (let user of usersToDelete) { await bulkDeleteProcessing(user) } @@ -526,7 +515,6 @@ export const destroy = async (id: string, currentUser: any) => { const db = tenancy.getGlobalDB() const dbUser = await db.get(id) const userId = dbUser._id as string - let groups = dbUser.userGroups if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { // root account holder can't be deleted from inside budibase @@ -545,10 +533,6 @@ export const destroy = async (id: string, currentUser: any) => { await db.remove(userId, dbUser._rev) - if (groups) { - await groupUtils.deleteGroupUsers(groups, dbUser) - } - await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "deletion" }) diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index 986a26ad5f..3677bfffc6 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -1,8 +1,6 @@ import { - BulkCreateUsersRequest, - BulkCreateUsersResponse, - BulkDeleteUsersRequest, - BulkDeleteUsersResponse, + BulkUserResponse, + BulkUserRequest, InviteUsersRequest, User, } from "@budibase/types" @@ -69,24 +67,26 @@ export class UserAPI { // BULK bulkCreateUsers = async (users: User[], groups: any[] = []) => { - const body: BulkCreateUsersRequest = { users, groups } + const body: BulkUserRequest = { create: { users, groups } } const res = await this.request - .post(`/api/global/users/bulkCreate`) + .post(`/api/global/users/bulk`) .send(body) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) - return res.body as BulkCreateUsersResponse + return res.body as BulkUserResponse } - bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => { - return this.request - .post(`/api/global/users/bulkDelete`) + bulkDeleteUsers = async (userIds: string[], status?: number) => { + const body: BulkUserRequest = { delete: { userIds } } + const res = await this.request + .post(`/api/global/users/bulk`) .send(body) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) .expect(status ? status : 200) + return res.body as BulkUserResponse } // USER diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index 06b1ea851c..66f78bb543 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -174,7 +174,7 @@ exports.isEmailConfigured = async (workspaceId = null) => { exports.sendEmail = async ( email, purpose, - { workspaceId, user, from, contents, subject, info, automation } = {} + { workspaceId, user, from, contents, subject, info, cc, bcc, automation } = {} ) => { const db = getGlobalDB() let config = (await getSmtpConfiguration(db, workspaceId, automation)) || {} @@ -197,6 +197,8 @@ exports.sendEmail = async ( message = { ...message, to: email, + cc: cc, + bcc: bcc, } if (subject || config.subject) { diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 6df1ae5f67..58e825af00 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -291,12 +291,12 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.4.3-alpha.2": - version "1.4.3-alpha.2" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.3-alpha.2.tgz#7e0ccffd393004e45c24cc633b296e2bdcd978a5" - integrity sha512-K40Hz2n0ESlfn4YWs5NL21kvY+NNX4aTatMyWBfz4ifio554ry0qZ6gZP4lxtj/shg5eedmldmDKJ2T+FiR2pA== +"@budibase/backend-core@1.4.8-alpha.10": + version "1.4.8-alpha.10" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.8-alpha.10.tgz#7cddbd81fb67d5a28d0f5d103191015b4a65e168" + integrity sha512-CyB6DOj/CuA0ZezvgU0LsojUGwQs+f8ZquvfKC+nlb7Kc/9v7lMNuZhu5Qe+9EHDUYlOjzddJQO6pGWKDpdt2w== dependencies: - "@budibase/types" "1.4.3-alpha.2" + "@budibase/types" "1.4.8-alpha.10" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" @@ -327,21 +327,21 @@ uuid "8.3.2" zlib "1.0.5" -"@budibase/pro@1.4.3-alpha.2": - version "1.4.3-alpha.2" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.3-alpha.2.tgz#044aa5cf0e55793a914b9e0c6e931b8a9f5e8f39" - integrity sha512-TakJ8A8RY6VwxRYZ4528zD3rQOwJW422sNrUbOcOK9H08B/EFltXMZBP+Okk9sX8e9/8+0pxGQLQ6RdKHYYXww== +"@budibase/pro@1.4.8-alpha.10": + version "1.4.8-alpha.10" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.8-alpha.10.tgz#2a773b8cd70ed6e834107f54838bb088356f1de3" + integrity sha512-1U8NkzxwuyKWKTn7pN0FPf2Wgl8BtaaXwOAcmTIbv94ySsL58/sMpToCRU1ycdnwNzCSHUa8cfJil/ACURW2xw== dependencies: - "@budibase/backend-core" "1.4.3-alpha.2" - "@budibase/types" "1.4.3-alpha.2" + "@budibase/backend-core" "1.4.8-alpha.10" + "@budibase/types" "1.4.8-alpha.10" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" -"@budibase/types@1.4.3-alpha.2": - version "1.4.3-alpha.2" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.3-alpha.2.tgz#55d52cb8f3fc1a56b9d5b008ddc9b3e46f3e078e" - integrity sha512-2kOrlHjtjfgYqaE8JAGV5X5FHSIzeb2LqSsUDzWHIlVWk9r0/bZ2NTvAvFMT5LSImfF4A4qbNXS0ZVtqjX2qzw== +"@budibase/types@1.4.8-alpha.10": + version "1.4.8-alpha.10" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.8-alpha.10.tgz#33c03714d8ed75f39bce55a3c685917c2ff278e2" + integrity sha512-hytgdJ4UoWEeZ9xsquDC6moV2bouN5wWvD/wgv3GlGQEBOfypo5P7tz4mRLhmVJyJmcNPUHG8QeDZsyixQGf/Q== "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" diff --git a/qa-core/.env b/qa-core/.env index 740b1b2b2a..36dd0a3656 100644 --- a/qa-core/.env +++ b/qa-core/.env @@ -1,3 +1,6 @@ BB_ADMIN_USER_EMAIL=qa@budibase.com BB_ADMIN_USER_PASSWORD=budibase ENCRYPTED_TEST_PUBLIC_API_KEY=a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f +COUCH_DB_URL=http://budibase:budibase@localhost:4567 +COUCH_DB_USER=budibase +COUCH_DB_PASSWORD=budibase \ No newline at end of file diff --git a/qa-core/docker-compose.yaml b/qa-core/docker-compose.yaml new file mode 100644 index 0000000000..abd8e4818e --- /dev/null +++ b/qa-core/docker-compose.yaml @@ -0,0 +1,12 @@ +version: "3.8" +services: + qa-core-couchdb: + # platform: linux/amd64 + container_name: budi-couchdb-qa + restart: on-failure + image: ibmcom/couchdb3 + environment: + - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} + - COUCHDB_USER=${COUCH_DB_USER} + ports: + - "4567:5984" diff --git a/qa-core/package.json b/qa-core/package.json index b2c3f464d7..529827bc9f 100644 --- a/qa-core/package.json +++ b/qa-core/package.json @@ -12,7 +12,9 @@ "test": "jest --runInBand", "test:watch": "jest --watch", "test:debug": "DEBUG=1 jest", - "api:server:setup": "env-cmd ts-node ../packages/builder/cypress/ts/setup.ts", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down", + "api:server:setup": "npm run docker:up && env-cmd ts-node ../packages/builder/cypress/ts/setup.ts", "api:server:setup:ci": "env-cmd node ../packages/builder/cypress/setup.js", "api:test:ci": "start-server-and-test api:server:setup:ci http://localhost:4100/builder test", "api:test": "start-server-and-test api:server:setup http://localhost:4100/builder test" diff --git a/qa-core/src/tests/public-api/tables/rows.spec.ts b/qa-core/src/tests/public-api/tables/rows.spec.ts index 91df85e65c..41519fbf3f 100644 --- a/qa-core/src/tests/public-api/tables/rows.spec.ts +++ b/qa-core/src/tests/public-api/tables/rows.spec.ts @@ -32,7 +32,7 @@ describe("Public API - /rows endpoints", () => { expect(row._id).toBeDefined() }) - it("POST - Search rows", async () => { + /*it("POST - Search rows", async () => { const [response, rows] = await config.rows.search({ query: { string: { @@ -41,10 +41,11 @@ describe("Public API - /rows endpoints", () => { }, }) expect(response).toHaveStatusCode(200) + expect(rows.length).toEqual(1) expect(rows[0]._id).toEqual(config.context._id) expect(rows[0].tableId).toEqual(config.context.tableId) expect(rows[0].testColumn).toEqual(config.context.testColumn) - }) + })*/ it("GET - Retrieve a row", async () => { const [response, row] = await config.rows.read(config.context._id)