diff --git a/.eslintignore b/.eslintignore index 54824be5c7..579bd55947 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,4 +7,5 @@ packages/server/client packages/builder/.routify packages/builder/cypress/support/queryLevelTransformerFunction.js packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js -packages/builder/cypress/reports \ No newline at end of file +packages/builder/cypress/reports +packages/sdk/sdk \ No newline at end of file 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/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 57e65c734e..21c74851e1 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -46,7 +46,8 @@ jobs: - run: yarn - run: yarn bootstrap - run: yarn lint - - run: yarn build + - run: yarn build + - run: yarn build:sdk - run: yarn test - name: Configure AWS Credentials diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 961082e1ef..de288dd7db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,6 +56,7 @@ jobs: - run: yarn bootstrap - run: yarn lint - run: yarn build + - run: yarn build:sdk - run: yarn test - name: Configure AWS Credentials diff --git a/.prettierignore b/.prettierignore index ad36a86b99..3a381d255e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,4 +8,5 @@ packages/server/client packages/server/src/definitions/openapi.ts packages/builder/.routify packages/builder/cypress/support/queryLevelTransformerFunction.js -packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js \ No newline at end of file +packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js +packages/sdk/sdk \ No newline at end of file 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 a1b0833420..78897dabab 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.4.3-alpha.1", + "version": "1.4.8-alpha.12", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index d9b78368ba..579e86802e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh", "build": "lerna run build", "build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput", + "build:sdk": "lerna run build:sdk", "deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular", "release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro", "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 0e19d84417..eacb2926c7 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.1", + "version": "1.4.8-alpha.12", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "1.4.3-alpha.1", + "@budibase/types": "1.4.8-alpha.12", "@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/plugin/utils.js b/packages/backend-core/src/plugin/utils.js index 020fb4484d..ade84bf44a 100644 --- a/packages/backend-core/src/plugin/utils.js +++ b/packages/backend-core/src/plugin/utils.js @@ -75,6 +75,15 @@ function validateDatasource(schema) { }) .unknown(true) .required(), + extra: joi.object().pattern( + joi.string(), + joi.object({ + type: joi.string().required(), + displayName: joi.string().required(), + required: joi.boolean(), + data: joi.object(), + }) + ), }), }) runJoi(validator, schema) 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 c8d0bcf02d..38e6f4ec45 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.1", + "version": "1.4.8-alpha.12", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "1.4.3-alpha.1", + "@budibase/string-templates": "1.4.8-alpha.12", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte index 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..23cdbff877 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(apps.getProdAppID(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) { @@ -126,11 +70,29 @@ } const getRoleLabel = appId => { - const roleId = group?.roles?.[`app_${appId}`] + const roleId = group?.roles?.[apps.getProdAppID(appId)] const role = $roles.find(x => x._id === roleId) 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..179dac9689 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() @@ -41,7 +87,7 @@ export const createLicensingStore = () => { return keys.reduce((acc, key) => { const quotaLimit = license[key].value const quotaUsed = (quota[key] / quotaLimit) * 100 - acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1 + acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1 return acc }, {}) } @@ -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 4191dd58b6..3088b5f37f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.4.3-alpha.1", + "version": "1.4.8-alpha.12", "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.1", + "@budibase/backend-core": "1.4.8-alpha.12", + "@budibase/string-templates": "1.4.8-alpha.12", + "@budibase/types": "1.4.8-alpha.12", "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 2dd9e9e192..e7b675e9fa 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.4.3-alpha.1", + "version": "1.4.8-alpha.12", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "1.4.3-alpha.1", - "@budibase/frontend-core": "1.4.3-alpha.1", - "@budibase/string-templates": "1.4.3-alpha.1", + "@budibase/bbui": "1.4.8-alpha.12", + "@budibase/frontend-core": "1.4.8-alpha.12", + "@budibase/string-templates": "1.4.8-alpha.12", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", @@ -35,7 +35,6 @@ "downloadjs": "1.4.7", "leaflet": "^1.7.1", "regexparam": "^1.3.0", - "rollup-plugin-polyfill-node": "^0.8.0", "sanitize-html": "^2.7.0", "screenfull": "^6.0.1", "shortid": "^2.2.15", @@ -52,6 +51,7 @@ "postcss": "^8.2.10", "rollup": "^2.44.0", "rollup-plugin-json": "^4.0.0", + "rollup-plugin-polyfill-node": "^0.8.0", "rollup-plugin-postcss": "^4.0.0", "rollup-plugin-svelte": "^7.1.0", "rollup-plugin-svg": "^2.0.0", diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index e55cb0b7c4..1450fda399 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() @@ -155,6 +159,7 @@ ...instance._styles?.normal, ...(selected ? $builderStore.gridStyles : null), }, + custom: customCSS, id, empty: emptyState, interactive, @@ -253,14 +258,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] @@ -374,6 +383,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/client/src/sdk.js b/packages/client/src/sdk.js index aa778388f6..1afeea0055 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -17,6 +17,7 @@ import { getAction } from "utils/getAction" import Provider from "components/context/Provider.svelte" import { ActionTypes } from "./constants" import { fetchDatasourceSchema } from "./utils/schema.js" +import { getAPIKey } from "./utils/api.js" export default { API, @@ -36,4 +37,5 @@ export default { fetchDatasourceSchema, Provider, ActionTypes, + getAPIKey, } diff --git a/packages/client/src/utils/api.js b/packages/client/src/utils/api.js new file mode 100644 index 0000000000..4e6025873d --- /dev/null +++ b/packages/client/src/utils/api.js @@ -0,0 +1,6 @@ +import { API } from "api" + +export const getAPIKey = async () => { + const { apiKey } = await API.fetchDeveloperInfo() + return apiKey +} diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index fbbce15654..2a1cda41f8 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.1", + "version": "1.4.8-alpha.12", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "1.4.3-alpha.1", + "@budibase/bbui": "1.4.8-alpha.12", "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/sdk/.gitignore b/packages/sdk/.gitignore new file mode 100644 index 0000000000..43e879ac90 --- /dev/null +++ b/packages/sdk/.gitignore @@ -0,0 +1,4 @@ +sdk +docs +node_modules +dist \ No newline at end of file diff --git a/packages/sdk/README.md b/packages/sdk/README.md new file mode 100644 index 0000000000..64b4c0538d --- /dev/null +++ b/packages/sdk/README.md @@ -0,0 +1,33 @@ +# Budibase Public API SDK +JS SDK for the Budibase Public API. + +This SDK is generated by [swagger-codegen](https://github.com/swagger-api/swagger-codegen). + +Docker is used to run the generator, so Java is not required. Docker is the only requirement to generate the SDK. + +The generated code will only run in a browser. It is not currently useable in a NodeJS environment. + +## Example usage +```js +import { configure, ApplicationsApi } from "@budibase/sdk" + +// Configure the API client +configure({ + apiKey: "my-api-key", + host: "https://my.budibase.app" +}) + +// Search for an app. +// We can use the promisified version... +const res = await ApplicationsApi.applicationsSearchPost({ name: "foo" }) +console.log("Applications:", res.data) + +// ...or the callback version +ApplicationsApi.applicationsSearchPost({ name: "foo" }, ((error, data) => { + if (error) { + console.error("Failed to search:", error) + } else { + console.log("Applications:", data.data) + } +})) +``` \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000000..1db54b580c --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,23 @@ +{ + "name": "@budibase/sdk", + "version": "1.4.8-alpha.12", + "description": "Budibase Public API SDK", + "author": "Budibase", + "license": "MPL-2.0", + "module": "dist/sdk.mjs", + "type": "module", + "scripts": { + "generate": "cd scripts && bash generate-sdk.sh", + "build:sdk": "yarn run generate && rollup -c" + }, + "dependencies": { + "superagent": "^5.3.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^18.0.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "rollup": "^2.44.0", + "rollup-plugin-polyfill-node": "^0.8.0", + "rollup-plugin-terser": "^7.0.2" + } +} diff --git a/packages/sdk/rollup.config.js b/packages/sdk/rollup.config.js new file mode 100644 index 0000000000..b9c1543c42 --- /dev/null +++ b/packages/sdk/rollup.config.js @@ -0,0 +1,22 @@ +import commonjs from "@rollup/plugin-commonjs" +import resolve from "@rollup/plugin-node-resolve" +import nodePolyfills from "rollup-plugin-polyfill-node" + +export default { + input: "src/index.js", + output: [ + { + sourcemap: false, + format: "esm", + file: "./dist/sdk.mjs", + }, + ], + plugins: [ + commonjs(), + nodePolyfills(), + resolve({ + preferBuiltins: true, + browser: true, + }), + ], +} diff --git a/packages/sdk/scripts/config.json b/packages/sdk/scripts/config.json new file mode 100644 index 0000000000..a6a3e1acb8 --- /dev/null +++ b/packages/sdk/scripts/config.json @@ -0,0 +1,3 @@ +{ + "usePromises": true +} \ No newline at end of file diff --git a/packages/sdk/scripts/generate-sdk.sh b/packages/sdk/scripts/generate-sdk.sh new file mode 100755 index 0000000000..82cb3f1d36 --- /dev/null +++ b/packages/sdk/scripts/generate-sdk.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Cleanup +if [[ -f "openapi.yaml" ]]; then + rm openapi.yaml +fi +if [[ -d "generated" ]]; then + rm -r generated +fi +if [[ -d "../sdk" ]]; then + rm -r ../sdk +fi + +# Generate new SDK +mkdir generated +cp ../../server/specs/openapi.yaml ./ +docker run --rm \ + -v ${PWD}/openapi.yaml:/openapi.yml \ + -v ${PWD}/generated:/generated \ + -v ${PWD}/config.json:/config.json \ + -u $(id -u):$(id -g) \ + swaggerapi/swagger-codegen-cli-v3 generate \ + -i /openapi.yml \ + -l javascript \ + -o /generated \ + -c /config.json + +# Use a subset of the generated files +mv generated/src ../sdk + +# Cleanup +if [[ -f "openapi.yaml" ]]; then + rm openapi.yaml +fi +if [[ -d "generated" ]]; then + rm -r generated +fi \ No newline at end of file diff --git a/packages/sdk/src/index.js b/packages/sdk/src/index.js new file mode 100644 index 0000000000..4569907702 --- /dev/null +++ b/packages/sdk/src/index.js @@ -0,0 +1,23 @@ +import * as BudibaseApi from "../sdk" + +export default class SDK { + applications = new BudibaseApi.ApplicationsApi() + queries = new BudibaseApi.QueriesApi() + rows = new BudibaseApi.RowsApi() + tables = new BudibaseApi.TablesApi() + users = new BudibaseApi.UsersApi() + + constructor({ apiKey, host }) { + let ApiClient = new BudibaseApi.ApiClient() + + // Default to current host + ApiClient.basePath = `${host || ""}/api/public/v1` + ApiClient.authentications["ApiKeyAuth"].apiKey = apiKey + + this.applications = new BudibaseApi.ApplicationsApi(ApiClient) + this.queries = new BudibaseApi.QueriesApi(ApiClient) + this.rows = new BudibaseApi.RowsApi(ApiClient) + this.tables = new BudibaseApi.TablesApi(ApiClient) + this.users = new BudibaseApi.UsersApi(ApiClient) + } +} diff --git a/packages/sdk/yarn.lock b/packages/sdk/yarn.lock new file mode 100644 index 0000000000..4e8d6d718f --- /dev/null +++ b/packages/sdk/yarn.lock @@ -0,0 +1,635 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.10.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@rollup/plugin-commonjs@^18.0.0": + version "18.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-18.1.0.tgz#5a760d757af168a50727c0ae080251fbfcc5eb02" + integrity sha512-h3e6T9rUxVMAQswpDIobfUHn/doMzM9sgkMrsMWCFLmB84PSoC8mV8tOloAJjSRwdqhXBqstlX2BwBpHJvbhxg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-inject@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-4.0.4.tgz#fbeee66e9a700782c4f65c8b0edbafe58678fbc2" + integrity sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + estree-walker "^2.0.1" + magic-string "^0.25.7" + +"@rollup/plugin-node-resolve@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@types/estree@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/node@*": + version "18.7.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" + integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + +acorn@^8.5.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +builtin-modules@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cookiejar@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + +debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fast-safe-stringify@^2.0.7: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@^1.2.2: + version "1.2.6" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" + integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.4.6: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picomatch@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +qs@^6.9.4: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +resolve@^1.17.0, resolve@^1.19.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup-plugin-polyfill-node@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.8.0.tgz#859c070822f5e38d221e5b4238cb34aa894c2b19" + integrity sha512-C4UeKedOmOBkB3FgR+z/v9kzRwV1Q/H8xWs1u1+CNe4XOV6hINfOrcO+TredKxYvopCmr+WKUSNsFUnD1RLHgQ== + dependencies: + "@rollup/plugin-inject" "^4.0.0" + +rollup-plugin-terser@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup@^2.44.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.0.tgz#9177992c9f09eb58c5e56cbfa641607a12b57ce2" + integrity sha512-x4KsrCgwQ7ZJPcFA/SUu6QVcYlO7uRLfLAy0DSA4NS2eG8japdbpM50ToH7z4iObodRYOJ0soneF0iaQRJ6zhA== + optionalDependencies: + fsevents "~2.3.2" + +safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^7.3.2: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +superagent@^5.3.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1" + integrity sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +terser@^5.0.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425" + integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 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 c943ffea37..66332176de 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.1", + "version": "1.4.8-alpha.12", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -77,11 +77,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "1.4.3-alpha.1", - "@budibase/client": "1.4.3-alpha.1", - "@budibase/pro": "1.4.3-alpha.1", - "@budibase/string-templates": "1.4.3-alpha.1", - "@budibase/types": "1.4.3-alpha.1", + "@budibase/backend-core": "1.4.8-alpha.12", + "@budibase/client": "1.4.8-alpha.12", + "@budibase/pro": "1.4.8-alpha.12", + "@budibase/string-templates": "1.4.8-alpha.12", + "@budibase/types": "1.4.8-alpha.12", "@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/specs/openapi.json b/packages/server/specs/openapi.json index f8539b9f7f..ce410823ec 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -1792,6 +1792,7 @@ "paths": { "/applications": { "post": { + "operationId": "create", "summary": "Create an application", "tags": [ "applications" @@ -1832,6 +1833,7 @@ }, "/applications/{appId}": { "put": { + "operationId": "update", "summary": "Update an application", "tags": [ "applications" @@ -1870,6 +1872,7 @@ } }, "delete": { + "operationId": "destroy", "summary": "Delete an application", "tags": [ "applications" @@ -1898,6 +1901,7 @@ } }, "get": { + "operationId": "getById", "summary": "Retrieve an application", "tags": [ "applications" @@ -1928,6 +1932,7 @@ }, "/applications/search": { "post": { + "operationId": "search", "summary": "Search for applications", "description": "Based on application properties (currently only name) search for applications.", "tags": [ @@ -1964,6 +1969,7 @@ }, "/queries/{queryId}": { "post": { + "operationId": "execute", "summary": "Execute a query", "description": "Queries which have been created within a Budibase app can be executed using this,", "tags": [ @@ -2011,6 +2017,7 @@ }, "/queries/search": { "post": { + "operationId": "search", "summary": "Search for queries", "description": "Based on query properties (currently only name) search for queries.", "tags": [ @@ -2052,6 +2059,7 @@ }, "/tables/{tableId}/rows": { "post": { + "operationId": "create", "summary": "Create a row", "description": "Creates a row within the specified table.", "tags": [ @@ -2101,6 +2109,7 @@ }, "/tables/{tableId}/rows/{rowId}": { "put": { + "operationId": "update", "summary": "Update a row", "description": "Updates a row within the specified table.", "tags": [ @@ -2151,6 +2160,7 @@ } }, "delete": { + "operationId": "destroy", "summary": "Delete a row", "description": "Deletes a row within the specified table.", "tags": [ @@ -2186,6 +2196,7 @@ } }, "get": { + "operationId": "getById", "summary": "Retrieve a row", "description": "This gets a single row, it will be enriched with the full related rows, rather than the squashed \"primaryDisplay\" format returned by the search endpoint.", "tags": [ @@ -2223,6 +2234,7 @@ }, "/tables/{tableId}/rows/search": { "post": { + "operationId": "search", "summary": "Search for rows", "tags": [ "rows" @@ -2266,6 +2278,7 @@ }, "/tables": { "post": { + "operationId": "create", "summary": "Create a table", "description": "Create a table, this could be internal or external.", "tags": [ @@ -2311,6 +2324,7 @@ }, "/tables/{tableId}": { "put": { + "operationId": "update", "summary": "Update a table", "description": "Update a table, this could be internal or external.", "tags": [ @@ -2357,6 +2371,7 @@ } }, "delete": { + "operationId": "destroy", "summary": "Delete a table", "description": "Delete a table, this could be internal or external.", "tags": [ @@ -2389,6 +2404,7 @@ } }, "get": { + "operationId": "getById", "summary": "Retrieve a table", "description": "Lookup a table, this could be internal or external.", "tags": [ @@ -2423,6 +2439,7 @@ }, "/tables/search": { "post": { + "operationId": "search", "summary": "Search for tables", "description": "Based on table properties (currently only name) search for tables. This could be an internal or an external table.", "tags": [ @@ -2464,6 +2481,7 @@ }, "/users": { "post": { + "operationId": "create", "summary": "Create a user", "tags": [ "users" @@ -2499,6 +2517,7 @@ }, "/users/{userId}": { "put": { + "operationId": "update", "summary": "Update a user", "tags": [ "users" @@ -2537,6 +2556,7 @@ } }, "delete": { + "operationId": "destroy", "summary": "Delete a user", "tags": [ "users" @@ -2565,6 +2585,7 @@ } }, "get": { + "operationId": "getById", "summary": "Retrieve a user", "tags": [ "users" @@ -2595,6 +2616,7 @@ }, "/users/search": { "post": { + "operationId": "search", "summary": "Search for users", "description": "Based on user properties (currently only name) search for users.", "tags": [ diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index 3cd29791af..ed13ac01f4 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -1370,6 +1370,7 @@ security: paths: /applications: post: + operationId: create summary: Create an application tags: - applications @@ -1393,6 +1394,7 @@ paths: $ref: "#/components/examples/application" "/applications/{appId}": put: + operationId: update summary: Update an application tags: - applications @@ -1415,6 +1417,7 @@ paths: application: $ref: "#/components/examples/application" delete: + operationId: destroy summary: Delete an application tags: - applications @@ -1431,6 +1434,7 @@ paths: application: $ref: "#/components/examples/application" get: + operationId: getById summary: Retrieve an application tags: - applications @@ -1448,6 +1452,7 @@ paths: $ref: "#/components/examples/application" /applications/search: post: + operationId: search summary: Search for applications description: Based on application properties (currently only name) search for applications. @@ -1472,6 +1477,7 @@ paths: $ref: "#/components/examples/applications" "/queries/{queryId}": post: + operationId: execute summary: Execute a query description: Queries which have been created within a Budibase app can be executed using this, @@ -1500,6 +1506,7 @@ paths: $ref: "#/components/examples/sqlResponse" /queries/search: post: + operationId: search summary: Search for queries description: Based on query properties (currently only name) search for queries. tags: @@ -1524,6 +1531,7 @@ paths: $ref: "#/components/examples/queries" "/tables/{tableId}/rows": post: + operationId: create summary: Create a row description: Creates a row within the specified table. tags: @@ -1554,6 +1562,7 @@ paths: $ref: "#/components/examples/row" "/tables/{tableId}/rows/{rowId}": put: + operationId: update summary: Update a row description: Updates a row within the specified table. tags: @@ -1583,6 +1592,7 @@ paths: row: $ref: "#/components/examples/row" delete: + operationId: destroy summary: Delete a row description: Deletes a row within the specified table. tags: @@ -1603,6 +1613,7 @@ paths: row: $ref: "#/components/examples/row" get: + operationId: getById summary: Retrieve a row description: This gets a single row, it will be enriched with the full related rows, rather than the squashed "primaryDisplay" format returned by the @@ -1625,6 +1636,7 @@ paths: $ref: "#/components/examples/enrichedRow" "/tables/{tableId}/rows/search": post: + operationId: search summary: Search for rows tags: - rows @@ -1650,6 +1662,7 @@ paths: $ref: "#/components/examples/rows" /tables: post: + operationId: create summary: Create a table description: Create a table, this could be internal or external. tags: @@ -1677,6 +1690,7 @@ paths: $ref: "#/components/examples/table" "/tables/{tableId}": put: + operationId: update summary: Update a table description: Update a table, this could be internal or external. tags: @@ -1703,6 +1717,7 @@ paths: table: $ref: "#/components/examples/table" delete: + operationId: destroy summary: Delete a table description: Delete a table, this could be internal or external. tags: @@ -1721,6 +1736,7 @@ paths: table: $ref: "#/components/examples/table" get: + operationId: getById summary: Retrieve a table description: Lookup a table, this could be internal or external. tags: @@ -1740,6 +1756,7 @@ paths: $ref: "#/components/examples/table" /tables/search: post: + operationId: search summary: Search for tables description: Based on table properties (currently only name) search for tables. This could be an internal or an external table. @@ -1765,6 +1782,7 @@ paths: $ref: "#/components/examples/tables" /users: post: + operationId: create summary: Create a user tags: - users @@ -1786,6 +1804,7 @@ paths: $ref: "#/components/examples/user" "/users/{userId}": put: + operationId: update summary: Update a user tags: - users @@ -1808,6 +1827,7 @@ paths: user: $ref: "#/components/examples/user" delete: + operationId: destroy summary: Delete a user tags: - users @@ -1824,6 +1844,7 @@ paths: user: $ref: "#/components/examples/user" get: + operationId: getById summary: Retrieve a user tags: - users @@ -1841,6 +1862,7 @@ paths: $ref: "#/components/examples/user" /users/search: post: + operationId: search summary: Search for users description: Based on user properties (currently only name) search for users. tags: 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/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts index 868d2689fd..7d1b1291ab 100644 --- a/packages/server/src/api/controllers/plugin/index.ts +++ b/packages/server/src/api/controllers/plugin/index.ts @@ -129,6 +129,6 @@ export async function processUploadedPlugin( } const doc = await plugins.storePlugin(metadata, directory, source) - ClientAppSocket.emit("plugins-update", { name: doc.name, hash: doc.hash }) + ClientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash }) return doc } 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/public/applications.ts b/packages/server/src/api/routes/public/applications.ts index 05f56da7b0..93d86d2aed 100644 --- a/packages/server/src/api/routes/public/applications.ts +++ b/packages/server/src/api/routes/public/applications.ts @@ -9,6 +9,7 @@ const read = [], * @openapi * /applications: * post: + * operationId: create * summary: Create an application * tags: * - applications @@ -41,6 +42,7 @@ write.push( * @openapi * /applications/{appId}: * put: + * operationId: update * summary: Update an application * tags: * - applications @@ -73,6 +75,7 @@ write.push( * @openapi * /applications/{appId}: * delete: + * operationId: destroy * summary: Delete an application * tags: * - applications @@ -95,6 +98,7 @@ write.push(new Endpoint("delete", "/applications/:appId", controller.destroy)) * @openapi * /applications/{appId}: * get: + * operationId: getById * summary: Retrieve an application * tags: * - applications @@ -117,6 +121,7 @@ read.push(new Endpoint("get", "/applications/:appId", controller.read)) * @openapi * /applications/search: * post: + * operationId: search * summary: Search for applications * description: Based on application properties (currently only name) search for applications. * tags: diff --git a/packages/server/src/api/routes/public/queries.ts b/packages/server/src/api/routes/public/queries.ts index 9e5d73714e..dc18fb91ac 100644 --- a/packages/server/src/api/routes/public/queries.ts +++ b/packages/server/src/api/routes/public/queries.ts @@ -9,6 +9,7 @@ const read = [], * @openapi * /queries/{queryId}: * post: + * operationId: execute * summary: Execute a query * description: Queries which have been created within a Budibase app can be executed using this, * tags: @@ -42,6 +43,7 @@ write.push(new Endpoint("post", "/queries/:queryId", controller.execute)) * @openapi * /queries/search: * post: + * operationId: search * summary: Search for queries * description: Based on query properties (currently only name) search for queries. * tags: diff --git a/packages/server/src/api/routes/public/rows.ts b/packages/server/src/api/routes/public/rows.ts index 80da073e3e..9109ae76b8 100644 --- a/packages/server/src/api/routes/public/rows.ts +++ b/packages/server/src/api/routes/public/rows.ts @@ -9,6 +9,7 @@ const read = [], * @openapi * /tables/{tableId}/rows: * post: + * operationId: create * summary: Create a row * description: Creates a row within the specified table. * tags: @@ -43,6 +44,7 @@ write.push(new Endpoint("post", "/tables/:tableId/rows", controller.create)) * @openapi * /tables/{tableId}/rows/{rowId}: * put: + * operationId: update * summary: Update a row * description: Updates a row within the specified table. * tags: @@ -79,6 +81,7 @@ write.push( * @openapi * /tables/{tableId}/rows/{rowId}: * delete: + * operationId: destroy * summary: Delete a row * description: Deletes a row within the specified table. * tags: @@ -106,6 +109,7 @@ write.push( * @openapi * /tables/{tableId}/rows/{rowId}: * get: + * operationId: getById * summary: Retrieve a row * description: This gets a single row, it will be enriched with the full related rows, rather than * the squashed "primaryDisplay" format returned by the search endpoint. @@ -132,6 +136,7 @@ read.push(new Endpoint("get", "/tables/:tableId/rows/:rowId", controller.read)) * @openapi * /tables/{tableId}/rows/search: * post: + * operationId: search * summary: Search for rows * tags: * - rows diff --git a/packages/server/src/api/routes/public/tables.ts b/packages/server/src/api/routes/public/tables.ts index 7e8ce29ae3..74cd8ca3cf 100644 --- a/packages/server/src/api/routes/public/tables.ts +++ b/packages/server/src/api/routes/public/tables.ts @@ -9,6 +9,7 @@ const read = [], * @openapi * /tables: * post: + * operationId: create * summary: Create a table * description: Create a table, this could be internal or external. * tags: @@ -45,6 +46,7 @@ write.push( * @openapi * /tables/{tableId}: * put: + * operationId: update * summary: Update a table * description: Update a table, this could be internal or external. * tags: @@ -81,6 +83,7 @@ write.push( * @openapi * /tables/{tableId}: * delete: + * operationId: destroy * summary: Delete a table * description: Delete a table, this could be internal or external. * tags: @@ -105,6 +108,7 @@ write.push(new Endpoint("delete", "/tables/:tableId", controller.destroy)) * @openapi * /tables/{tableId}: * get: + * operationId: getById * summary: Retrieve a table * description: Lookup a table, this could be internal or external. * tags: @@ -129,6 +133,7 @@ read.push(new Endpoint("get", "/tables/:tableId", controller.read)) * @openapi * /tables/search: * post: + * operationId: search * summary: Search for tables * description: Based on table properties (currently only name) search for tables. This could be * an internal or an external table. diff --git a/packages/server/src/api/routes/public/users.ts b/packages/server/src/api/routes/public/users.ts index 06e17fba42..67b73f6cbe 100644 --- a/packages/server/src/api/routes/public/users.ts +++ b/packages/server/src/api/routes/public/users.ts @@ -9,6 +9,7 @@ const read = [], * @openapi * /users: * post: + * operationId: create * summary: Create a user * tags: * - users @@ -35,6 +36,7 @@ write.push(new Endpoint("post", "/users", controller.create)) * @openapi * /users/{userId}: * put: + * operationId: update * summary: Update a user * tags: * - users @@ -63,6 +65,7 @@ write.push(new Endpoint("put", "/users/:userId", controller.update)) * @openapi * /users/{userId}: * delete: + * operationId: destroy * summary: Delete a user * tags: * - users @@ -85,6 +88,7 @@ write.push(new Endpoint("delete", "/users/:userId", controller.destroy)) * @openapi * /users/{userId}: * get: + * operationId: getById * summary: Retrieve a user * tags: * - users @@ -107,6 +111,7 @@ read.push(new Endpoint("get", "/users/:userId", controller.read)) * @openapi * /users/search: * post: + * operationId: search * summary: Search for users * description: Based on user properties (currently only name) search for users. * tags: 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/definitions/openapi.ts b/packages/server/src/definitions/openapi.ts index a4bd0c1e3e..bb0ffb6771 100644 --- a/packages/server/src/definitions/openapi.ts +++ b/packages/server/src/definitions/openapi.ts @@ -5,491 +5,67 @@ export interface paths { "/applications": { - post: { - parameters: { - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the created application. */ - 200: { - content: { - "application/json": components["schemas"]["applicationOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["application"]; - }; - }; - }; + post: operations["create"]; }; "/applications/{appId}": { - get: { - parameters: { - path: { - /** The ID of the app which this request is targeting. */ - appId: components["parameters"]["appIdUrl"]; - }; - }; - responses: { - /** Returns the retrieved application. */ - 200: { - content: { - "application/json": components["schemas"]["applicationOutput"]; - }; - }; - }; - }; - put: { - parameters: { - path: { - /** The ID of the app which this request is targeting. */ - appId: components["parameters"]["appIdUrl"]; - }; - }; - responses: { - /** Returns the updated application. */ - 200: { - content: { - "application/json": components["schemas"]["applicationOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["application"]; - }; - }; - }; - delete: { - parameters: { - path: { - /** The ID of the app which this request is targeting. */ - appId: components["parameters"]["appIdUrl"]; - }; - }; - responses: { - /** Returns the deleted application. */ - 200: { - content: { - "application/json": components["schemas"]["applicationOutput"]; - }; - }; - }; - }; + get: operations["getById"]; + put: operations["update"]; + delete: operations["destroy"]; }; "/applications/search": { /** Based on application properties (currently only name) search for applications. */ - post: { - responses: { - /** Returns the applications that were found based on the search parameters. */ - 200: { - content: { - "application/json": components["schemas"]["applicationSearch"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["nameSearch"]; - }; - }; - }; + post: operations["search"]; }; "/queries/{queryId}": { /** Queries which have been created within a Budibase app can be executed using this, */ - post: { - parameters: { - path: { - /** The ID of the query which this request is targeting. */ - queryId: components["parameters"]["queryId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the result of the query execution. */ - 200: { - content: { - "application/json": components["schemas"]["executeQueryOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["executeQuery"]; - }; - }; - }; + post: operations["execute"]; }; "/queries/search": { /** Based on query properties (currently only name) search for queries. */ - post: { - parameters: { - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the queries found based on the search parameters. */ - 200: { - content: { - "application/json": components["schemas"]["querySearch"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["nameSearch"]; - }; - }; - }; + post: operations["search"]; }; "/tables/{tableId}/rows": { /** Creates a row within the specified table. */ - post: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the created row, including the ID which has been generated for it. This can be found in the Budibase portal, viewed under the developer information. */ - 200: { - content: { - "application/json": components["schemas"]["rowOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["row"]; - }; - }; - }; + post: operations["create"]; }; "/tables/{tableId}/rows/{rowId}": { /** This gets a single row, it will be enriched with the full related rows, rather than the squashed "primaryDisplay" format returned by the search endpoint. */ - get: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - /** The ID of the row which this request is targeting. */ - rowId: components["parameters"]["rowId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the retrieved row. */ - 200: { - content: { - "application/json": components["schemas"]["rowOutput"]; - }; - }; - }; - }; + get: operations["getById"]; /** Updates a row within the specified table. */ - put: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - /** The ID of the row which this request is targeting. */ - rowId: components["parameters"]["rowId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the created row, including the ID which has been generated for it. */ - 200: { - content: { - "application/json": components["schemas"]["rowOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["row"]; - }; - }; - }; + put: operations["update"]; /** Deletes a row within the specified table. */ - delete: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - /** The ID of the row which this request is targeting. */ - rowId: components["parameters"]["rowId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the deleted row, including the ID which has been generated for it. */ - 200: { - content: { - "application/json": components["schemas"]["rowOutput"]; - }; - }; - }; - }; + delete: operations["destroy"]; }; "/tables/{tableId}/rows/search": { - post: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** The response will contain an array of rows that match the search parameters. */ - 200: { - content: { - "application/json": components["schemas"]["searchOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["rowSearch"]; - }; - }; - }; + post: operations["search"]; }; "/tables": { /** Create a table, this could be internal or external. */ - post: { - parameters: { - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */ - 200: { - content: { - "application/json": components["schemas"]["tableOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["table"]; - }; - }; - }; + post: operations["create"]; }; "/tables/{tableId}": { /** Lookup a table, this could be internal or external. */ - get: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the retrieved table. */ - 200: { - content: { - "application/json": components["schemas"]["tableOutput"]; - }; - }; - }; - }; + get: operations["getById"]; /** Update a table, this could be internal or external. */ - put: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the updated table. */ - 200: { - content: { - "application/json": components["schemas"]["tableOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["table"]; - }; - }; - }; + put: operations["update"]; /** Delete a table, this could be internal or external. */ - delete: { - parameters: { - path: { - /** The ID of the table which this request is targeting. */ - tableId: components["parameters"]["tableId"]; - }; - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the deleted table. */ - 200: { - content: { - "application/json": components["schemas"]["tableOutput"]; - }; - }; - }; - }; + delete: operations["destroy"]; }; "/tables/search": { /** Based on table properties (currently only name) search for tables. This could be an internal or an external table. */ - post: { - parameters: { - header: { - /** The ID of the app which this request is targeting. */ - "x-budibase-app-id": components["parameters"]["appId"]; - }; - }; - responses: { - /** Returns the found tables, based on the search parameters. */ - 200: { - content: { - "application/json": components["schemas"]["tableSearch"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["nameSearch"]; - }; - }; - }; + post: operations["search"]; }; "/users": { - post: { - responses: { - /** Returns the created user. */ - 200: { - content: { - "application/json": components["schemas"]["userOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["user"]; - }; - }; - }; + post: operations["create"]; }; "/users/{userId}": { - get: { - parameters: { - path: { - /** The ID of the user which this request is targeting. */ - userId: components["parameters"]["userId"]; - }; - }; - responses: { - /** Returns the retrieved user. */ - 200: { - content: { - "application/json": components["schemas"]["userOutput"]; - }; - }; - }; - }; - put: { - parameters: { - path: { - /** The ID of the user which this request is targeting. */ - userId: components["parameters"]["userId"]; - }; - }; - responses: { - /** Returns the updated user. */ - 200: { - content: { - "application/json": components["schemas"]["userOutput"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["user"]; - }; - }; - }; - delete: { - parameters: { - path: { - /** The ID of the user which this request is targeting. */ - userId: components["parameters"]["userId"]; - }; - }; - responses: { - /** Returns the deleted user. */ - 200: { - content: { - "application/json": components["schemas"]["userOutput"]; - }; - }; - }; - }; + get: operations["getById"]; + put: operations["update"]; + delete: operations["destroy"]; }; "/users/search": { /** Based on user properties (currently only name) search for users. */ - post: { - responses: { - /** Returns the found users based on search parameters. */ - 200: { - content: { - "application/json": components["schemas"]["userSearch"]; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["nameSearch"]; - }; - }; - }; + post: operations["search"]; }; } @@ -1127,6 +703,117 @@ export interface components { }; } -export interface operations {} +export interface operations { + create: { + responses: { + /** Returns the created user. */ + 200: { + content: { + "application/json": components["schemas"]["userOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["user"]; + }; + }; + }; + getById: { + parameters: { + path: { + /** The ID of the user which this request is targeting. */ + userId: components["parameters"]["userId"]; + }; + }; + responses: { + /** Returns the retrieved user. */ + 200: { + content: { + "application/json": components["schemas"]["userOutput"]; + }; + }; + }; + }; + update: { + parameters: { + path: { + /** The ID of the user which this request is targeting. */ + userId: components["parameters"]["userId"]; + }; + }; + responses: { + /** Returns the updated user. */ + 200: { + content: { + "application/json": components["schemas"]["userOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["user"]; + }; + }; + }; + destroy: { + parameters: { + path: { + /** The ID of the user which this request is targeting. */ + userId: components["parameters"]["userId"]; + }; + }; + responses: { + /** Returns the deleted user. */ + 200: { + content: { + "application/json": components["schemas"]["userOutput"]; + }; + }; + }; + }; + /** Based on user properties (currently only name) search for users. */ + search: { + responses: { + /** Returns the found users based on search parameters. */ + 200: { + content: { + "application/json": components["schemas"]["userSearch"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["nameSearch"]; + }; + }; + }; + /** Queries which have been created within a Budibase app can be executed using this, */ + execute: { + parameters: { + path: { + /** The ID of the query which this request is targeting. */ + queryId: components["parameters"]["queryId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the result of the query execution. */ + 200: { + content: { + "application/json": components["schemas"]["executeQueryOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["executeQuery"]; + }; + }; + }; +} export interface external {} 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 18d81c18c7..241603fe23 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.1": - version "1.4.3-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.3-alpha.1.tgz#df3ebe4bd7b4f52b02a59f06e7adb36cdcf3a77b" - integrity sha512-dtRZZ2JV0rwNvqmzSiSb1xrmob/HaX9krFGORHglqu+dOtXrMZpQLO0qz7J/7+p7+zUk+kOCvFbXqbWKOguy8w== +"@budibase/backend-core@1.4.8-alpha.12": + version "1.4.8-alpha.12" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.8-alpha.12.tgz#e8b031adbc5d9cfb1bee7fbcabd29cf27a6a9723" + integrity sha512-LTH2KMoWuxCxaqrnU+Z8melCu/g+BguBGa0MejxAnz5c6RmDDnzGa7DhNlGFEywF/WWOV0sOybu1W4JqM+r9dA== dependencies: - "@budibase/types" "1.4.3-alpha.1" + "@budibase/types" "1.4.8-alpha.12" "@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.1": - version "1.4.3-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.3-alpha.1.tgz#bb5d2919679c88911e3abe2c33da705c5031bc06" - integrity sha512-vR6B8u2P5PHBet6xnrTksrEtwWIIVZ1KqkbXhc+QyBODxlOdfN0F/Zdb1yV//+hIbesskXogZTA7asp2YiVJIg== +"@budibase/pro@1.4.8-alpha.12": + version "1.4.8-alpha.12" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.8-alpha.12.tgz#8b9107e4b2531b0a6be0973f6a042a3815191bca" + integrity sha512-bJ0yMe9GZy98D9/asnxbMgnAr5TVQdz7aX4iWzyDeHwffTbOcdQE2LODz9rSCVxJ1Cfk+6SYovIwX3kQIW6OXA== dependencies: - "@budibase/backend-core" "1.4.3-alpha.1" - "@budibase/types" "1.4.3-alpha.1" + "@budibase/backend-core" "1.4.8-alpha.12" + "@budibase/types" "1.4.8-alpha.12" "@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.1": - version "1.4.3-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.3-alpha.1.tgz#2b5c3a2e3072ca6043369f36eedbdd53aaa9e50c" - integrity sha512-04g6eJsj4wYcP7pyIEzinQmIBylJlBxaP7E0sIBAK92BUzS9xZljV10pM2T0BuwRMMYzRF8HVLAaBH87mdcgkw== +"@budibase/types@1.4.8-alpha.12": + version "1.4.8-alpha.12" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.8-alpha.12.tgz#7c9be3b385a92e5782c359ab2b7a4aef006be3a2" + integrity sha512-m/kveGPyFomyW1ISnX+GafVb9mbmanRph/1mUtVP11FaEz4N/HAjbSxeldJoeC4TB9pd8xyxvmWQmRWn+PC0FA== "@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 5c148f8208..59349b2181 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.1", + "version": "1.4.8-alpha.12", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index dc594d3dd0..94c9c8257a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "1.4.3-alpha.1", + "version": "1.4.8-alpha.12", "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 caf49e45a4..04aef0d54b 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.1", + "version": "1.4.8-alpha.12", "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.1", - "@budibase/pro": "1.4.3-alpha.1", - "@budibase/string-templates": "1.4.3-alpha.1", - "@budibase/types": "1.4.3-alpha.1", + "@budibase/backend-core": "1.4.8-alpha.12", + "@budibase/pro": "1.4.8-alpha.12", + "@budibase/string-templates": "1.4.8-alpha.12", + "@budibase/types": "1.4.8-alpha.12", "@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 5f6e79c93d..9fac782fa4 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.1": - version "1.4.3-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.3-alpha.1.tgz#df3ebe4bd7b4f52b02a59f06e7adb36cdcf3a77b" - integrity sha512-dtRZZ2JV0rwNvqmzSiSb1xrmob/HaX9krFGORHglqu+dOtXrMZpQLO0qz7J/7+p7+zUk+kOCvFbXqbWKOguy8w== +"@budibase/backend-core@1.4.8-alpha.12": + version "1.4.8-alpha.12" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.8-alpha.12.tgz#e8b031adbc5d9cfb1bee7fbcabd29cf27a6a9723" + integrity sha512-LTH2KMoWuxCxaqrnU+Z8melCu/g+BguBGa0MejxAnz5c6RmDDnzGa7DhNlGFEywF/WWOV0sOybu1W4JqM+r9dA== dependencies: - "@budibase/types" "1.4.3-alpha.1" + "@budibase/types" "1.4.8-alpha.12" "@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.1": - version "1.4.3-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.3-alpha.1.tgz#bb5d2919679c88911e3abe2c33da705c5031bc06" - integrity sha512-vR6B8u2P5PHBet6xnrTksrEtwWIIVZ1KqkbXhc+QyBODxlOdfN0F/Zdb1yV//+hIbesskXogZTA7asp2YiVJIg== +"@budibase/pro@1.4.8-alpha.12": + version "1.4.8-alpha.12" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.8-alpha.12.tgz#8b9107e4b2531b0a6be0973f6a042a3815191bca" + integrity sha512-bJ0yMe9GZy98D9/asnxbMgnAr5TVQdz7aX4iWzyDeHwffTbOcdQE2LODz9rSCVxJ1Cfk+6SYovIwX3kQIW6OXA== dependencies: - "@budibase/backend-core" "1.4.3-alpha.1" - "@budibase/types" "1.4.3-alpha.1" + "@budibase/backend-core" "1.4.8-alpha.12" + "@budibase/types" "1.4.8-alpha.12" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" -"@budibase/types@1.4.3-alpha.1": - version "1.4.3-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.3-alpha.1.tgz#2b5c3a2e3072ca6043369f36eedbdd53aaa9e50c" - integrity sha512-04g6eJsj4wYcP7pyIEzinQmIBylJlBxaP7E0sIBAK92BUzS9xZljV10pM2T0BuwRMMYzRF8HVLAaBH87mdcgkw== +"@budibase/types@1.4.8-alpha.12": + version "1.4.8-alpha.12" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.8-alpha.12.tgz#7c9be3b385a92e5782c359ab2b7a4aef006be3a2" + integrity sha512-m/kveGPyFomyW1ISnX+GafVb9mbmanRph/1mUtVP11FaEz4N/HAjbSxeldJoeC4TB9pd8xyxvmWQmRWn+PC0FA== "@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)