diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 1303d5921a..6e886f3011 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -71,7 +71,7 @@ jobs: - name: Upload to S3 if: github.ref == 'refs/heads/new-design-ui' run: | - tar -czvf new_ui.tar.gz packages/server/assets packages/server/index.html + tar -czvf new_ui.tar.gz packages/server/builder/assets packages/server/builder/index.html aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/ aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*" diff --git a/.vscode/settings.json b/.vscode/settings.json index d471924fe0..4838a4fd89 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,17 @@ "editor.codeActionsOnSave": { "source.fixAll": true }, - "editor.defaultFormatter": "svelte.svelte-vscode" + "editor.defaultFormatter": "svelte.svelte-vscode", + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "debug.javascript.terminalOptions": { + "skipFiles": [ + "${workspaceFolder}/packages/backend-core/node_modules/**", + "/**" + ] + }, } diff --git a/charts/budibase/Chart.yaml b/charts/budibase/Chart.yaml index 227a515432..570aa04d8e 100644 --- a/charts/budibase/Chart.yaml +++ b/charts/budibase/Chart.yaml @@ -11,8 +11,8 @@ sources: - https://github.com/Budibase/budibase - https://budibase.com type: application -version: 0.2.10 -appVersion: 1.0.48 +version: 0.2.11 +appVersion: 1.0.214 dependencies: - name: couchdb version: 3.6.1 diff --git a/lerna.json b/lerna.json index beadd0ddcd..d4a28e80c2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.212-alpha.7", + "version": "1.0.218", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 0d51086e2b..915ba63eaa 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.212-alpha.7", + "version": "1.0.218", "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.0.212-alpha.7", + "@budibase/types": "^1.0.218", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", @@ -36,6 +36,7 @@ "passport-google-oauth": "2.0.0", "passport-jwt": "4.0.0", "passport-local": "1.0.0", + "passport-oauth2-refresh": "^2.1.0", "posthog-node": "1.3.0", "pouchdb": "7.3.0", "pouchdb-find": "7.2.2", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b13cd932c6..b6d6a2027f 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -2,6 +2,9 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { getGlobalDB } = require("./tenancy") +const refresh = require("passport-oauth2-refresh") +const { Configs } = require("./constants") +const { getScopedConfig } = require("./db/utils") const { jwt, local, @@ -12,6 +15,7 @@ const { tenancy, appTenancy, authError, + ssoCallbackUrl, csrf, internalApi, } = require("./middleware") @@ -34,6 +38,122 @@ passport.deserializeUser(async (user, done) => { } }) +async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) { + const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) + let enrichedConfig + let strategy + + try { + enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl) + if (!enrichedConfig) { + throw new Error("OIDC Config contents invalid") + } + strategy = await oidc.strategyFactory(enrichedConfig) + } catch (err) { + console.error(err) + throw new Error("Could not refresh OAuth Token") + } + + refresh.use(strategy, { + setRefreshOAuth2() { + return strategy._getOAuth2Client(enrichedConfig) + }, + }) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.OIDC, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshGoogleAccessToken(db, config, refreshToken) { + let callbackUrl = await google.getCallbackUrl(db, config) + + let strategy + try { + strategy = await google.strategyFactory(config, callbackUrl) + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC refresh strategy", err) + } + + refresh.use(strategy) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.GOOGLE, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshOAuthToken(refreshToken, configType, configId) { + const db = getGlobalDB() + + const config = await getScopedConfig(db, { + type: configType, + group: {}, + }) + + let chosenConfig = {} + let refreshResponse + if (configType === Configs.OIDC) { + // configId - retrieved from cookie. + chosenConfig = config.configs.filter(c => c.uuid === configId)[0] + if (!chosenConfig) { + throw new Error("Invalid OIDC configuration") + } + refreshResponse = await refreshOIDCAccessToken( + db, + chosenConfig, + refreshToken + ) + } else { + chosenConfig = config + refreshResponse = await refreshGoogleAccessToken( + db, + chosenConfig, + refreshToken + ) + } + + return refreshResponse +} + +async function updateUserOAuth(userId, oAuthConfig) { + const details = { + accessToken: oAuthConfig.accessToken, + refreshToken: oAuthConfig.refreshToken, + } + + try { + const db = getGlobalDB() + const dbUser = await db.get(userId) + + //Do not overwrite the refresh token if a valid one is not provided. + if (typeof details.refreshToken !== "string") { + delete details.refreshToken + } + + dbUser.oauth2 = { + ...dbUser.oauth2, + ...details, + } + + await db.put(dbUser) + } catch (e) { + console.error("Could not update OAuth details for current user", e) + } +} + module.exports = { buildAuthMiddleware: authenticated, passport, @@ -46,4 +166,7 @@ module.exports = { authError, buildCsrfMiddleware: csrf, internalApi, + refreshOAuthToken, + updateUserOAuth, + ssoCallbackUrl, } diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index a69a381f0f..bd4d857ef2 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -314,6 +314,7 @@ function getContextDB(key, opts) { toUseAppId = getDevelopmentAppID(appId) break } + db = dangerousGetDB(toUseAppId, opts) try { cls.setOnContext(key, db) diff --git a/packages/backend-core/src/db/constants.js b/packages/backend-core/src/db/constants.js deleted file mode 100644 index 10c6e174d7..0000000000 --- a/packages/backend-core/src/db/constants.js +++ /dev/null @@ -1,41 +0,0 @@ -exports.SEPARATOR = "_" - -const PRE_APP = "app" -const PRE_DEV = "dev" - -exports.DocumentTypes = { - USER: "us", - WORKSPACE: "workspace", - CONFIG: "config", - TEMPLATE: "template", - APP: PRE_APP, - DEV: PRE_DEV, - APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, - APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, - ROLE: "role", - MIGRATIONS: "migrations", - DEV_INFO: "devinfo", -} - -exports.StaticDatabases = { - GLOBAL: { - name: "global-db", - docs: { - apiKeys: "apikeys", - usageQuota: "usage_quota", - licenseInfo: "license_info", - }, - }, - // contains information about tenancy and so on - PLATFORM_INFO: { - name: "global-info", - docs: { - tenants: "tenants", - install: "install", - }, - }, -} - -exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR -exports.APP_DEV = exports.APP_DEV_PREFIX = - exports.DocumentTypes.APP_DEV + exports.SEPARATOR diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..be0e824e61 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,58 @@ +export const SEPARATOR = "_" +export const UNICODE_MAX = "\ufff0" + +/** + * Can be used to create a few different forms of querying a view. + */ +export enum AutomationViewModes { + ALL = "all", + AUTOMATION = "automation", + STATUS = "status", +} + +export enum ViewNames { + USER_BY_EMAIL = "by_email", + BY_API_KEY = "by_api_key", + USER_BY_BUILDERS = "by_builders", + LINK = "by_link", + ROUTING = "screen_routes", + AUTOMATION_LOGS = "automation_logs", +} + +export enum DocumentTypes { + USER = "us", + WORKSPACE = "workspace", + CONFIG = "config", + TEMPLATE = "template", + APP = "app", + DEV = "dev", + APP_DEV = "app_dev", + APP_METADATA = "app_metadata", + ROLE = "role", + MIGRATIONS = "migrations", + DEV_INFO = "devinfo", + AUTOMATION_LOG = "log_au", +} + +export const StaticDatabases = { + GLOBAL: { + name: "global-db", + docs: { + apiKeys: "apikeys", + usageQuota: "usage_quota", + licenseInfo: "license_info", + }, + }, + // contains information about tenancy and so on + PLATFORM_INFO: { + name: "global-info", + docs: { + tenants: "tenants", + install: "install", + }, + }, +} + +export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR +export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR +export const APP_DEV_PREFIX = APP_DEV diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index e7ec90b7ab..856661fe0d 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -1,7 +1,7 @@ import { newid } from "../hashing" import { DEFAULT_TENANT_ID, Configs } from "../constants" import env from "../environment" -import { SEPARATOR, DocumentTypes } from "./constants" +import { SEPARATOR, DocumentTypes, UNICODE_MAX, ViewNames } from "./constants" import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import fetch from "node-fetch" import { doWithDB, allDbs } from "./index" @@ -12,14 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" -const UNICODE_MAX = "\ufff0" - -export const ViewNames = { - USER_BY_EMAIL: "by_email", - BY_API_KEY: "by_api_key", - USER_BY_BUILDERS: "by_builders", -} - export * from "./constants" export * from "./conversions" export { default as Replication } from "./Replication" @@ -63,6 +55,13 @@ export function getDocParams( } } +/** + * Retrieve the correct index for a view based on default design DB. + */ +export function getQueryIndex(viewName: ViewNames) { + return `database/${viewName}` +} + /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. @@ -93,13 +92,17 @@ export function generateGlobalUserID(id?: any) { /** * Gets parameters for retrieving users. */ -export function getGlobalUserParams(globalId: any, otherProps = {}) { +export function getGlobalUserParams(globalId: any, otherProps: any = {}) { if (!globalId) { globalId = "" } + const startkey = otherProps?.startkey return { ...otherProps, - startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, + // need to include this incase pagination + startkey: startkey + ? startkey + : `${DocumentTypes.USER}${SEPARATOR}${globalId}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, } } @@ -385,7 +388,9 @@ export const getScopedFullConfig = async function ( if (type === Configs.SETTINGS) { if (scopedConfig && scopedConfig.doc) { // overrides affected by environment variables - scopedConfig.doc.config.platformUrl = await getPlatformUrl() + scopedConfig.doc.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + }) scopedConfig.doc.config.analyticsEnabled = await events.analytics.enabled() } else { @@ -394,7 +399,7 @@ export const getScopedFullConfig = async function ( doc: { _id: generateConfigID({ type, user, workspace }), config: { - platformUrl: await getPlatformUrl(), + platformUrl: await getPlatformUrl({ tenantAware: true }), analyticsEnabled: await events.analytics.enabled(), }, }, @@ -435,6 +440,26 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { return platformUrl } +export function pagination( + data: any[], + pageSize: number, + { paginate, property } = { paginate: true, property: "_id" } +) { + if (!paginate) { + return { data, hasNextPage: false } + } + const hasNextPage = data.length > pageSize + let nextPage = undefined + if (hasNextPage) { + nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id + } + return { + data: data.slice(0, pageSize), + hasNextPage, + nextPage, + } +} + export async function getScopedConfig(db: any, params: any) { const configDoc = await getScopedFullConfig(db, params) return configDoc && configDoc.config ? configDoc.config : configDoc diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 6eb6b14bc4..ab89eed3b2 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -13,6 +13,7 @@ import deprovisioning from "./context/deprovision" import auth from "./auth" import constants from "./constants" import * as dbConstants from "./db/constants" +import logging from "./logging" // mimic the outer package exports import * as db from "./pkg/db" @@ -49,6 +50,7 @@ const core = { deprovisioning, installation, errors, + logging, ...errorClasses, } diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 9c35336dda..4e6e0b7ba2 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -94,7 +94,6 @@ module.exports = ( user = await getUser(userId, session.tenantId) } user.csrfToken = session.csrfToken - delete user.password authenticated = true } catch (err) { error = err @@ -128,6 +127,8 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } + } else { + delete user.password } // be explicit if (authenticated !== true) { diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 6c4c0d8883..1721d56a3c 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -2,7 +2,7 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") const oidc = require("./passport/oidc") -const { authError } = require("./passport/utils") +const { authError, ssoCallbackUrl } = require("./passport/utils") const authenticated = require("./authenticated") const auditLog = require("./auditLog") const tenancy = require("./tenancy") @@ -20,6 +20,7 @@ module.exports = { tenancy, authError, internalApi, + ssoCallbackUrl, datasource: { google: datasourceGoogle, }, diff --git a/packages/backend-core/src/middleware/passport/google.js b/packages/backend-core/src/middleware/passport/google.js index 858029ca80..7419974cd7 100644 --- a/packages/backend-core/src/middleware/passport/google.js +++ b/packages/backend-core/src/middleware/passport/google.js @@ -1,6 +1,7 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy - +const { ssoCallbackUrl } = require("./utils") const { authenticateThirdParty } = require("./third-party-common") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { return (accessToken, refreshToken, profile, done) => { @@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { ) } } + +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.GOOGLE) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index 445893b1df..b955d29102 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) { if (await compare(password, dbUser.password)) { const sessionId = newid() const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/backend-core/src/middleware/passport/oidc.js b/packages/backend-core/src/middleware/passport/oidc.js index 1e93e20b1c..20dbd4669b 100644 --- a/packages/backend-core/src/middleware/passport/oidc.js +++ b/packages/backend-core/src/middleware/passport/oidc.js @@ -1,6 +1,8 @@ const fetch = require("node-fetch") const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const { authenticateThirdParty } = require("./third-party-common") +const { ssoCallbackUrl } = require("./utils") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { /** @@ -89,11 +91,24 @@ function validEmail(value) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport OIDC Strategy */ -exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { +exports.strategyFactory = async function (config, saveUserFn) { try { - const { clientID, clientSecret, configUrl } = config + const verify = buildVerifyFn(saveUserFn) + const strategy = new OIDCStrategy(config, verify) + strategy.name = "oidc" + return strategy + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC authentication strategy", err) + } +} + +exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { + try { + const { clientID, clientSecret, configUrl } = enrichedConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { + //check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) @@ -109,24 +124,24 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { const body = await response.json() - const verify = buildVerifyFn(saveUserFn) - return new OIDCStrategy( - { - issuer: body.issuer, - authorizationURL: body.authorization_endpoint, - tokenURL: body.token_endpoint, - userInfoURL: body.userinfo_endpoint, - clientID: clientID, - clientSecret: clientSecret, - callbackURL: callbackUrl, - }, - verify - ) + return { + issuer: body.issuer, + authorizationURL: body.authorization_endpoint, + tokenURL: body.token_endpoint, + userInfoURL: body.userinfo_endpoint, + clientID: clientID, + clientSecret: clientSecret, + callbackURL: callbackUrl, + } } catch (err) { console.error(err) - throw new Error("Error constructing OIDC authentication strategy", err) + throw new Error("Error constructing OIDC authentication configuration", err) } } +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.OIDC) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index c5e9fe0034..c00ab2ea7d 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -48,8 +48,8 @@ describe("oidc", () => { it("should create successfully create an oidc strategy", async () => { const oidc = require("../oidc") - - await oidc.strategyFactory(oidcConfig, callbackUrl) + const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) + await oidc.strategyFactory(enrichedConfig, callbackUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) diff --git a/packages/backend-core/src/middleware/passport/utils.js b/packages/backend-core/src/middleware/passport/utils.js index cbb93bfa3b..217130cd6d 100644 --- a/packages/backend-core/src/middleware/passport/utils.js +++ b/packages/backend-core/src/middleware/passport/utils.js @@ -1,3 +1,7 @@ +const { isMultiTenant, getTenantId } = require("../../tenancy") +const { getScopedConfig } = require("../../db/utils") +const { Configs } = require("../../constants") + /** * Utility to handle authentication errors. * @@ -5,6 +9,7 @@ * @param {*} message Message that will be returned in the response body * @param {*} err (Optional) error that will be logged */ + exports.authError = function (done, message, err = null) { return done( err, @@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) { { message: message } ) } + +exports.ssoCallbackUrl = async (db, config, type) => { + // incase there is a callback URL from before + if (config && config.callbackURL) { + return config.callbackURL + } + const publicConfig = await getScopedConfig(db, { + type: Configs.SETTINGS, + }) + + let callbackUrl = `/api/global/auth` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` + } + callbackUrl += `/${type}/callback` + + return `${publicConfig.platformUrl}${callbackUrl}` +} diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.js index 4acccda2a0..0c1350a674 100644 --- a/packages/backend-core/src/users.js +++ b/packages/backend-core/src/users.js @@ -1,5 +1,6 @@ const { ViewNames } = require("./db/utils") const { queryGlobalView } = require("./db/views") +const { UNICODE_MAX } = require("./db/constants") /** * Given an email address this will use a view to search through @@ -19,3 +20,24 @@ exports.getGlobalUserByEmail = async email => { return response } + +/** + * Performs a starts with search on the global email view. + */ +exports.searchGlobalUsersByEmail = async (email, opts) => { + if (typeof email !== "string") { + throw new Error("Must provide a string to search by") + } + const lcEmail = email.toLowerCase() + // handle if passing up startkey for pagination + const startkey = opts && opts.startkey ? opts.startkey : lcEmail + let response = await queryGlobalView(ViewNames.USER_BY_EMAIL, { + ...opts, + startkey, + endkey: `${lcEmail}${UNICODE_MAX}`, + }) + if (!response) { + response = [] + } + return Array.isArray(response) ? response : [response] +} diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index e40cddc468..77dbc61425 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -4123,6 +4123,11 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" +passport-oauth2-refresh@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4" + integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A== + passport-oauth2@1.x.x: version "1.6.1" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index e03b7263e6..f6363db979 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.0.212-alpha.7", + "version": "1.0.218", "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.0.212-alpha.7", + "@budibase/string-templates": "^1.0.218", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index b518ac3d92..2d23120046 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -13,6 +13,7 @@ export let size = "M" export let active = false export let fullWidth = false + export let noPadding = false function longPress(element) { if (!longPressable) return @@ -41,6 +42,7 @@ class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--emphasized={emphasized} class:is-selected={selected} + class:noPadding class:fullWidth class="spectrum-ActionButton spectrum-ActionButton--size{size}" class:active @@ -80,4 +82,8 @@ .active svg { color: var(--spectrum-global-color-blue-600); } + .noPadding { + padding: 0; + min-width: 0; + } diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 83927b05db..343af559cb 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -40,5 +40,6 @@ on:change={onChange} on:pick on:type + on:blur /> diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index 2a4bac4a2c..2835b3cd40 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -52,7 +52,10 @@ {id} type="text" on:focus={() => (focus = true)} - on:blur={() => (focus = false)} + on:blur={() => { + focus = false + dispatch("blur") + }} on:change={onType} value={value || ""} placeholder={placeholder || ""} diff --git a/packages/bbui/src/Notification/Notification.svelte b/packages/bbui/src/Notification/Notification.svelte index 1d21131553..53ab062701 100644 --- a/packages/bbui/src/Notification/Notification.svelte +++ b/packages/bbui/src/Notification/Notification.svelte @@ -1,15 +1,20 @@ -
+
{#if icon} {/if} -
+
{message || ""}
+ {#if action} + +
{actionMessage}
+
+ {/if}
{#if dismissable}
@@ -46,4 +56,15 @@ .spectrum-Toast { pointer-events: all; } + + .wide { + width: 100%; + } + + .actionBody { + justify-content: space-between; + display: flex; + width: 100%; + align-items: center; + } diff --git a/packages/bbui/src/Notification/NotificationDisplay.svelte b/packages/bbui/src/Notification/NotificationDisplay.svelte index eb778f3aa0..0b846f06ce 100644 --- a/packages/bbui/src/Notification/NotificationDisplay.svelte +++ b/packages/bbui/src/Notification/NotificationDisplay.svelte @@ -8,13 +8,15 @@
- {#each $notifications as { type, icon, message, id, dismissable } (id)} + {#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
notifications.dismiss(id)} />
diff --git a/packages/bbui/src/Stores/notifications.js b/packages/bbui/src/Stores/notifications.js index 74eed8628a..449d282f24 100644 --- a/packages/bbui/src/Stores/notifications.js +++ b/packages/bbui/src/Stores/notifications.js @@ -20,7 +20,16 @@ export const createNotificationStore = () => { setTimeout(() => (block = false), timeout) } - const send = (message, type = "default", icon = "", autoDismiss = true) => { + const send = ( + message, + { + type = "default", + icon = "", + autoDismiss = true, + action = null, + wide = false, + } + ) => { if (block) { return } @@ -28,7 +37,15 @@ export const createNotificationStore = () => { _notifications.update(state => { return [ ...state, - { id: _id, type, message, icon, dismissable: !autoDismiss }, + { + id: _id, + type, + message, + icon, + dismissable: !autoDismiss, + action, + wide, + }, ] }) if (autoDismiss) { @@ -50,10 +67,11 @@ export const createNotificationStore = () => { return { subscribe, send, - info: msg => send(msg, "info", "Info"), - error: msg => send(msg, "error", "Alert", false), - warning: msg => send(msg, "warning", "Alert"), - success: msg => send(msg, "success", "CheckmarkCircle"), + info: msg => send(msg, { type: "info", icon: "Info" }), + error: msg => + send(msg, { type: "error", icon: "Alert", autoDismiss: false }), + warning: msg => send(msg, { type: "warning", icon: "Alert" }), + success: msg => send(msg, { type: "success", icon: "CheckmarkCircle" }), blockNotifications, dismiss: dismissNotification, } diff --git a/packages/bbui/src/Table/ArrayRenderer.svelte b/packages/bbui/src/Table/ArrayRenderer.svelte index 679973a03a..3755850666 100644 --- a/packages/bbui/src/Table/ArrayRenderer.svelte +++ b/packages/bbui/src/Table/ArrayRenderer.svelte @@ -5,7 +5,7 @@ const displayLimit = 5 - $: badges = value?.slice(0, displayLimit) ?? [] + $: badges = Array.isArray(value) ? value.slice(0, displayLimit) : [] $: leftover = (value?.length ?? 0) - badges.length diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte index 4dda31240a..246323244a 100644 --- a/packages/bbui/src/Table/CellRenderer.svelte +++ b/packages/bbui/src/Table/CellRenderer.svelte @@ -26,12 +26,20 @@ array: ArrayRenderer, internal: InternalRenderer, } - $: type = schema?.type ?? "string" + $: type = getType(schema) $: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: width = schema?.width || "150px" $: cellValue = getCellValue(value, schema.template) + const getType = schema => { + // Use a string renderer for dates if we use a custom template + if (schema?.type === "datetime" && schema?.template) { + return "string" + } + return schema?.type || "string" + } + const getCellValue = (value, template) => { if (!template) { return value diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index baa84c91e0..e01d84e123 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -37,6 +37,7 @@ export let autoSortColumns = true export let compact = false export let customPlaceholder = false + export let placeholderText = "No rows found" const dispatch = createEventDispatcher() @@ -405,7 +406,7 @@ > -
No rows found
+
{placeholderText}
{/if}
diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index a431051075..e65c01c1b6 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -85,12 +85,12 @@ filterTests(['all'], () => { cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) }) - cy.get(interact.DEPLOYMENT_TOP_NAV).click() - cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) - cy.get(interact.UNPUBLISH_MODAL) - .within(() => { - cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }) - }) + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) + + cy.get("[data-cy='publish-popover-menu']") + .within(() => { + cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) + }) cy.get(interact.UNPUBLISH_MODAL).should("be.visible") .within(() => { diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index 097b70db30..00c875e4fa 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -52,13 +52,6 @@ filterTests(['smoke', 'all'], () => { // Start create app process. If apps already exist, click second button cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - } - }) const appName = "Cypress Tests" cy.get(interact.SPECTRUM_MODAL).within(() => { diff --git a/packages/builder/package.json b/packages/builder/package.json index 3f38918e8e..a1729e82dc 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.212-alpha.7", + "version": "1.0.218", "license": "GPL-3.0", "private": true, "scripts": { @@ -69,14 +69,15 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.212-alpha.7", - "@budibase/client": "^1.0.212-alpha.7", - "@budibase/frontend-core": "^1.0.212-alpha.7", - "@budibase/string-templates": "^1.0.212-alpha.7", + "@budibase/bbui": "^1.0.218", + "@budibase/client": "^1.0.218", + "@budibase/frontend-core": "^1.0.218", + "@budibase/string-templates": "^1.0.218", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", "codemirror": "^5.59.0", + "dayjs": "^1.11.2", "downloadjs": "1.4.7", "lodash": "4.17.21", "posthog-js": "1.4.5", diff --git a/packages/builder/src/analytics/IntercomClient.js b/packages/builder/src/analytics/IntercomClient.js index d4835d979d..6d5bf9e93e 100644 --- a/packages/builder/src/analytics/IntercomClient.js +++ b/packages/builder/src/analytics/IntercomClient.js @@ -53,7 +53,7 @@ export default class IntercomClient { * @returns Intercom global object */ show(user = {}) { - if (!this.initialised) return + if (!this.initialised || !user?.admin) return return window.Intercom("boot", { app_id: this.token, diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 8cbc629291..234f83d7cc 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -49,6 +49,95 @@ export const getBindableProperties = (asset, componentId) => { ] } +/** + * Gets all rest bindable data fields + */ +export const getRestBindings = () => { + const userBindings = getUserBindings() + return [...userBindings, ...getAuthBindings()] +} + +/** + * Gets all rest bindable auth fields + */ +export const getAuthBindings = () => { + let bindings = [] + const safeUser = makePropSafe("user") + const safeOAuth2 = makePropSafe("oauth2") + const safeAccessToken = makePropSafe("accessToken") + + const authBindings = [ + { + runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`, + readable: `Current User.OAuthToken`, + key: "accessToken", + }, + ] + + bindings = Object.keys(authBindings).map(key => { + const fieldBinding = authBindings[key] + return { + type: "context", + runtimeBinding: fieldBinding.runtime, + readableBinding: fieldBinding.readable, + fieldSchema: { type: "string", name: fieldBinding.key }, + providerId: "user", + } + }) + return bindings +} + +/** + * Utility - convert a key/value map to an array of custom 'context' bindings + * @param {object} valueMap Key/value pairings + * @param {string} prefix A contextual string prefix/path for a user readable binding + * @return {object[]} An array containing readable/runtime binding objects + */ +export const toBindingsArray = (valueMap, prefix) => { + if (!valueMap) { + return [] + } + return Object.keys(valueMap).reduce((acc, binding) => { + if (!binding || !valueMap[binding]) { + return acc + } + acc.push({ + type: "context", + runtimeBinding: binding, + readableBinding: `${prefix}.${binding}`, + }) + return acc + }, []) +} + +/** + * Utility - coverting a map of readable bindings to runtime + */ +export const readableToRuntimeMap = (bindings, ctx) => { + if (!bindings || !ctx) { + return {} + } + return Object.keys(ctx).reduce((acc, key) => { + let parsedQuery = readableToRuntimeBinding(bindings, ctx[key]) + acc[key] = parsedQuery + return acc + }, {}) +} + +/** + * Utility - coverting a map of runtime bindings to readable + */ +export const runtimeToReadableMap = (bindings, ctx) => { + if (!bindings || !ctx) { + return {} + } + return Object.keys(ctx).reduce((acc, key) => { + let parsedQuery = runtimeToReadableBinding(bindings, ctx[key]) + acc[key] = parsedQuery + return acc + }, {}) +} + /** * Gets the bindable properties exposed by a certain component. */ @@ -298,7 +387,6 @@ const getUserBindings = () => { providerId: "user", }) }) - return bindings } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index cf42492c05..dd09e3356a 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -5,6 +5,7 @@ import { cloneDeep } from "lodash/fp" const initialAutomationState = { automations: [], + showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], @@ -19,6 +20,17 @@ export const getAutomationStore = () => { } const automationActions = store => ({ + definitions: async () => { + const response = await API.getAutomationDefinitions() + store.update(state => { + state.blockDefinitions = { + TRIGGER: response.trigger, + ACTION: response.action, + } + return state + }) + return response + }, fetch: async () => { const responses = await Promise.all([ API.getAutomations(), @@ -109,6 +121,20 @@ const automationActions = store => ({ return state }) }, + getLogs: async ({ automationId, startDate, status, page } = {}) => { + return await API.getAutomationLogs({ + automationId, + startDate, + status, + page, + }) + }, + clearLogErrors: async ({ automationId, appId } = {}) => { + return await API.clearAutomationLogErrors({ + automationId, + appId, + }) + }, addTestDataToAutomation: data => { store.update(state => { state.selectedAutomation.addTestData(data) @@ -117,11 +143,10 @@ const automationActions = store => ({ }, addBlockToAutomation: (block, blockIdx) => { store.update(state => { - const newBlock = state.selectedAutomation.addBlock( + state.selectedBlock = state.selectedAutomation.addBlock( cloneDeep(block), blockIdx ) - state.selectedBlock = newBlock return state }) }, diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 3e58b25ff6..9c987c89d8 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -65,7 +65,7 @@ { - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true }} size="M">Test Details diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index a4c41c6948..291575f3f2 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,6 +1,4 @@
@@ -60,16 +80,13 @@
- {#if showTestStatus && testResult && testResult[0]} + {#if showTestStatus && testResult}
{testResult[0].outputs?.success || isTrigger - ? "Success" - : "Error"}{status?.message}
{/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index fecd0fcc7e..b86cffb1f9 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -51,7 +51,7 @@ $automationStore.selectedAutomation?.automation, testData ) - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true } catch (error) { notifications.error("Error testing notification") } diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte new file mode 100644 index 0000000000..a2eb904c94 --- /dev/null +++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte @@ -0,0 +1,137 @@ + + +
+ {#each blocks as block, idx} +
+ {#if block.stepId !== "LOOP"} + + {#if showParameters && showParameters[block.id]} + + {#if filteredResults?.[idx]?.outputs.iterations} +
+ +
+ +
+
+ {/if} + +
+ + +
+