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/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index ddc725d302..27a4dea654 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -122,6 +122,10 @@ spec: value: {{ .Values.globals.automationMaxIterations | quote }} - name: TENANT_FEATURE_FLAGS value: {{ .Values.globals.tenantFeatureFlags | quote }} + - name: BB_ADMIN_USER_EMAIL + value: { { .Values.globals.bbAdminUserEmail | quote } } + - name: BB_ADMIN_USER_PASSWORD + value: { { .Values.globals.bbAdminUserPassword | quote } } image: budibase/apps:{{ .Values.globals.appVersion }} imagePullPolicy: Always diff --git a/hosting/.env b/hosting/.env index 39df76d01e..11dd661bf1 100644 --- a/hosting/.env +++ b/hosting/.env @@ -18,4 +18,8 @@ MINIO_PORT=4004 COUCH_DB_PORT=4005 REDIS_PORT=6379 WATCHTOWER_PORT=6161 -BUDIBASE_ENVIRONMENT=PRODUCTION \ No newline at end of file +BUDIBASE_ENVIRONMENT=PRODUCTION + +# An admin user can be automatically created initially if these are set +BB_ADMIN_USER_EMAIL= +BB_ADMIN_USER_PASSWORD= \ No newline at end of file diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index f9d9eaf1c5..57cbf33709 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -23,6 +23,8 @@ services: ENABLE_ANALYTICS: "true" REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} + BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} + BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} depends_on: - worker-service - redis-service diff --git a/hosting/hosting.properties b/hosting/hosting.properties index c8e2f5c606..11dd661bf1 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -19,3 +19,7 @@ COUCH_DB_PORT=4005 REDIS_PORT=6379 WATCHTOWER_PORT=6161 BUDIBASE_ENVIRONMENT=PRODUCTION + +# An admin user can be automatically created initially if these are set +BB_ADMIN_USER_EMAIL= +BB_ADMIN_USER_PASSWORD= \ No newline at end of file diff --git a/lerna.json b/lerna.json index f94f39b1c8..938bdf0f6b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.212-alpha.8", + "version": "1.0.219", "npmClient": "yarn", "packages": [ "packages/*" @@ -15,4 +15,4 @@ ] } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 9c35af497f..a1a63fa0b2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1", "dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server", - "test": "lerna run test", + "test": "lerna run test && yarn test:pro", + "test:pro": "bash scripts/pro/test.sh", "lint:eslint": "eslint packages", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"", "lint": "yarn run lint:eslint && yarn run lint:prettier", diff --git a/packages/backend-core/cache.js b/packages/backend-core/cache.js index 6b319357c4..c8bd3c9b6f 100644 --- a/packages/backend-core/cache.js +++ b/packages/backend-core/cache.js @@ -5,4 +5,5 @@ module.exports = { app: require("./src/cache/appMetadata"), writethrough: require("./src/cache/writethrough"), ...generic, + cache: generic, } diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 108927231c..49e82205e5 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.8", + "version": "1.0.219", "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.8", + "@budibase/types": "^1.0.219-alpha.17", "@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", @@ -58,6 +59,7 @@ ] }, "devDependencies": { + "@budibase/types": "^1.0.219", "@shopify/jest-koa-mocks": "3.1.5", "@types/jest": "27.5.1", "@types/koa": "2.0.52", @@ -78,4 +80,4 @@ "typescript": "4.7.3" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" -} +} \ No newline at end of file diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b13cd932c6..b60144a0de 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,10 +15,13 @@ const { tenancy, appTenancy, authError, + ssoCallbackUrl, csrf, internalApi, } = require("./middleware") +const { invalidateUser } = require("./cache/user") + // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) @@ -34,6 +40,124 @@ 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) + + await invalidateUser(userId) + } catch (e) { + console.error("Could not update OAuth details for current user", e) + } +} + module.exports = { buildAuthMiddleware: authenticated, passport, @@ -46,4 +170,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 12626fb90e..0000000000 --- a/packages/backend-core/src/db/constants.js +++ /dev/null @@ -1,42 +0,0 @@ -exports.SEPARATOR = "_" -exports.UNICODE_MAX = "\ufff0" - -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 54af4fc7a2..ba3f1dd3e9 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, UNICODE_MAX } 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,12 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" -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" @@ -61,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. @@ -386,7 +387,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 { @@ -395,7 +398,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(), }, }, diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 0a17c82873..845504fdc9 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -40,7 +40,7 @@ const env = { DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, - PLATFORM_URL: process.env.PLATFORM_URL, + PLATFORM_URL: process.env.PLATFORM_URL || "", POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS, 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/logging.js b/packages/backend-core/src/logging.ts similarity index 72% rename from packages/backend-core/src/logging.js rename to packages/backend-core/src/logging.ts index 6ab5bff44a..68c3307b2f 100644 --- a/packages/backend-core/src/logging.js +++ b/packages/backend-core/src/logging.ts @@ -1,10 +1,10 @@ const NonErrors = ["AccountError"] -function isSuppressed(e) { +function isSuppressed(e?: any) { return e && e["suppressAlert"] } -module.exports.logAlert = (message, e) => { +export function logAlert(message: string, e?: any) { if (e && NonErrors.includes(e.name) && isSuppressed(e)) { return } @@ -14,3 +14,7 @@ module.exports.logAlert = (message, e) => { } console.error(`bb-alert: ${message} ${errorJson}`) } + +export default { + logAlert, +} 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/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 1bf13f6eaf..80abf8ce04 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.8", + "version": "1.0.219", "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.8", + "@budibase/string-templates": "^1.0.219", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", @@ -86,4 +86,4 @@ "svelte-portal": "^1.0.0" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" -} +} \ No newline at end of file diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index 996e1268b4..53ba6c7e51 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 @@ -86,4 +88,8 @@ .is-selected:not(.spectrum-ActionButton--emphasized) { background: var(--spectrum-global-color-gray-300); } + .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.json b/packages/builder/cypress.json index 46f85a52c8..f1eada481f 100644 --- a/packages/builder/cypress.json +++ b/packages/builder/cypress.json @@ -16,4 +16,4 @@ "runMode": 1, "openMode": 0 } -} +} \ No newline at end of file diff --git a/packages/builder/cypress/integration/addRadioButtons.spec.js b/packages/builder/cypress/integration/addRadioButtons.spec.js index 8f5b1a527b..578b519341 100644 --- a/packages/builder/cypress/integration/addRadioButtons.spec.js +++ b/packages/builder/cypress/integration/addRadioButtons.spec.js @@ -10,6 +10,7 @@ filterTests(['all'], () => { it("should add Radio Buttons options picker on form, add data, and confirm", () => { cy.navigateToFrontend() + cy.wait(500) cy.addComponent("Form", "Form") cy.addComponent("Form", "Options Picker").then((componentId) => { // Provide field setting @@ -36,5 +37,9 @@ filterTests(['all'], () => { }) cy.addCustomSourceOptions(totalRadioButtons) } + + after(() => { + cy.deleteAllApps() + }) }) }) diff --git a/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js index c615b2b4e6..491a4abc44 100644 --- a/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js +++ b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js @@ -9,10 +9,11 @@ filterTests(["smoke", "all"], () => { before(() => { cy.login() cy.deleteApp("Cypress Tests") - cy.createApp("Cypress Tests") + cy.createApp("Cypress Tests", false) // Create new user - cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) + cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) cy.createUser(bbUserEmail) cy.contains("bbuser").click() cy.wait(500) diff --git a/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js index 3c23086136..562e1e149f 100644 --- a/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js +++ b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js @@ -6,11 +6,11 @@ filterTests(["smoke", "all"], () => { before(() => { cy.login() cy.deleteApp("Cypress Tests") - cy.createApp("Cypress Tests") + cy.createApp("Cypress Tests", false) }) it("should create a user via basic onboarding", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) cy.createUser("bbuser@test.com") cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser") }) @@ -43,19 +43,20 @@ filterTests(["smoke", "all"], () => { const uuid = () => Cypress._.random(0, 1e6) const name = uuid() if(i < 1){ - cy.createApp(name) + cy.createApp(name, false) } else { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) + cy.wait(1000) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 2000 }).click({ force: true }) cy.createAppFromScratch(name) } } } }) // Navigate back to the user - cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 500}) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click() - cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).contains("bbuser").click() + cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click() for (let i = 0; i < 3; i++) { cy.get(interact.SPECTRUM_TABLE, { timeout: 3000}) .eq(1) @@ -64,24 +65,24 @@ filterTests(["smoke", "all"], () => { .find(interact.SPECTRUM_TABLE_CELL) .eq(0) .click() - cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 500 }) + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }) .contains("Choose an option") .click() .then(() => { if (i == 0) { - cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Admin").click({ force: true }) + cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Admin").click({ force: true }) } else if (i == 1) { - cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Power").click({ force: true }) + cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Power").click({ force: true }) } else if (i == 2) { - cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Basic").click({ force: true }) + cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Basic").click({ force: true }) } - cy.get(interact.SPECTRUM_BUTTON, { timeout: 1000 }) + cy.get(interact.SPECTRUM_BUTTON, { timeout: 2000 }) .contains("Update role") .click({ force: true }) }) - cy.reload() + cy.reload({ timeout: 5000 }) cy.wait(1000) } // Confirm roles exist within Configure roles table @@ -173,14 +174,16 @@ filterTests(["smoke", "all"], () => { it("Should edit user details within user details page", () => { // Add First name - cy.get(interact.FIELD, { timeout: 500 }).eq(2).within(() => { - cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).type("bb") + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.wait(500) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb") }) // Add Last name - cy.get(interact.FIELD).eq(3).within(() => { - cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("test") + cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => { + cy.wait(500) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test") }) - cy.get(interact.FIELD).eq(0).click() + cy.get(interact.FIELD, { timeout: 1000 }).eq(0).click() // Reload page cy.reload() @@ -188,8 +191,8 @@ filterTests(["smoke", "all"], () => { cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") }) - cy.get(interact.FIELD).eq(3).within(() => { - cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).should('have.value', "test") + cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test") }) }) diff --git a/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js index 7827275620..95af9f7841 100644 --- a/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js +++ b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js @@ -103,6 +103,8 @@ filterTests(["smoke", "all"], () => { } cy.get("button").contains("Update password").click({ force: true }) }) + // Remove users name + cy.updateUserInformation() }) }) }) diff --git a/packages/builder/cypress/integration/appOverview.spec.js b/packages/builder/cypress/integration/appOverview.spec.js index d718f95b9f..dbfce3ce63 100644 --- a/packages/builder/cypress/integration/appOverview.spec.js +++ b/packages/builder/cypress/integration/appOverview.spec.js @@ -5,7 +5,8 @@ filterTests(["all"], () => { context("Application Overview screen", () => { before(() => { cy.login() - cy.createTestApp() + cy.deleteAllApps() + cy.createApp("Cypress Tests") }) it("Should be accessible from the applications list", () => { @@ -81,13 +82,14 @@ filterTests(["all"], () => { }) it("Should reflect the app deployment state", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.get(".appTable .app-row-actions button") .contains("Edit") .eq(0) .click({ force: true }) - cy.get(".toprightnav button.spectrum-Button") + cy.wait(500) + cy.get(".toprightnav button.spectrum-Button", { timeout: 2000 }) .contains("Publish") .click({ force: true }) cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") @@ -300,7 +302,7 @@ filterTests(["all"], () => { }) it("Should allow editing of the app details.", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.get(".appTable .app-row-actions button") .contains("Manage") .eq(0) @@ -315,7 +317,8 @@ filterTests(["all"], () => { cy.updateAppName("sample name") //publish and check its disabled - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.wait(500) cy.get(".appTable .app-row-actions button") .contains("Edit") .eq(0) @@ -331,8 +334,8 @@ filterTests(["all"], () => { cy.wait(1000) }) - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button", { timeout: 1000 }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) + cy.get(".appTable .app-row-actions button", { timeout: 5000 }) .contains("Manage") .eq(0) .click({ force: true }) diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index a431051075..edca7ee3af 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -6,11 +6,12 @@ filterTests(['all'], () => { context("Publish Application Workflow", () => { before(() => { cy.login() - cy.createTestApp() + cy.deleteAllApps() + cy.createApp("Cypress Tests", false) }) it("Should reflect the unpublished status correctly", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { @@ -29,6 +30,7 @@ filterTests(['all'], () => { it("Should publish an application and correctly reflect that", () => { //Assuming the previous test was run and the unpublished app is open in edit mode. + cy.closeModal() cy.get(interact.TOPRIGHTNAV_BUTTON_SPECTRUM).contains("Publish").click({ force : true }) cy.get(interact.DEPLOY_APP_MODAL).should("be.visible") @@ -72,7 +74,7 @@ filterTests(['all'], () => { it("Should unpublish an application using the link and reflect the status change", () => { //Assuming the previous test app exists and is published - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.get(interact.APP_TABLE_STATUS).eq(0) .within(() => { @@ -85,19 +87,21 @@ 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.closeModal() + 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(() => { cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true } )}) - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 }) + cy.wait(500) cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished") }) diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index 097b70db30..ca52c66e35 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -51,14 +51,8 @@ filterTests(['smoke', 'all'], () => { cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) // 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 }) - } - }) + cy.wait(1000) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 3000 }).click({ force: true }) const appName = "Cypress Tests" cy.get(interact.SPECTRUM_MODAL).within(() => { @@ -93,7 +87,7 @@ filterTests(['smoke', 'all'], () => { const appName = "Cypress Tests" cy.createApp(appName, false) - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.applicationInAppTable(appName) cy.deleteApp(appName) diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index a2d09d97bf..9adc486f70 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -5,7 +5,6 @@ filterTests(['smoke', 'all'], () => { context("Create a View", () => { before(() => { cy.login() - cy.createTestApp() cy.createTable("data") cy.addColumn("data", "group", "Text") diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js index b79f5af9c6..4c24ea9280 100644 --- a/packages/builder/cypress/integration/datasources/mySql.spec.js +++ b/packages/builder/cypress/integration/datasources/mySql.spec.js @@ -111,6 +111,7 @@ filterTests(["all"], () => { // Save relationship & reload page cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.reload() + cy.wait(1000) }) // Confirm table length & relationship name cy.get(".spectrum-Table", { timeout: 1000 }) diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js index de959e203c..ccecfbd5df 100644 --- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js +++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js @@ -151,7 +151,7 @@ filterTests(["all"], () => { cy.get("@query").its("response.body").should("not.be.empty") // Save query cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) - cy.get(".hierarchy-items-container").should("contain", queryName) + cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName) }) it("should switch to schema with no tables", () => { @@ -217,24 +217,24 @@ filterTests(["all"], () => { it("should edit a query name", () => { // Access query - cy.get(".hierarchy-items-container") + cy.get(".hierarchy-items-container", { timeout: 2000 }) .contains(queryName + " (1)") .click() // Rename query - cy.get(".spectrum-Form-item") + cy.wait(1000) + cy.get(".spectrum-Form-item", { timeout: 2000 }) .eq(0) .within(() => { cy.get("input").clear().type(queryRename) }) // Run and Save query - cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Button", { timeout: 500 }).contains("Save Query").click({ force: true }) - //cy.reload() - //cy.wait(500) - cy.get(".nav-item").should("contain", queryRename) + cy.get(".spectrum-Button", { timeout: 2000 }).contains("Run Query").click({ force: true }) + cy.wait(1000) + cy.get(".spectrum-Button", { timeout: 2000 }).contains("Save Query").click({ force: true }) + cy.reload({ timeout: 5000 }) + cy.get(".nav-item", { timeout: 2000 }).should("contain", queryRename) }) it("should delete a query", () => { @@ -251,6 +251,7 @@ filterTests(["all"], () => { .contains("Delete Query") .click({ force: true }) // Confirm deletion + cy.reload({ timeout: 5000 }) cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName) }) diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js index 370efadff2..4460750b07 100644 --- a/packages/builder/cypress/integration/renameAnApplication.spec.js +++ b/packages/builder/cypress/integration/renameAnApplication.spec.js @@ -12,7 +12,7 @@ filterTests(["all"], () => { const appName = "Cypress Tests" const appRename = "Cypress Renamed" // Rename app, Search for app, Confirm name was changed - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, appRename) cy.reload() cy.searchForApplication(appRename) @@ -39,7 +39,7 @@ filterTests(["all"], () => { .click({ force: true }) }) // Rename app, Search for app, Confirm name was changed - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, appRename, true) cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1) cy.applicationInAppTable(appRename) @@ -47,7 +47,7 @@ filterTests(["all"], () => { it("Should try to rename an application to have no name", () => { const appName = "Cypress Tests" - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, " ", false, true) // Close modal and confirm name has not been changed cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click() @@ -57,7 +57,7 @@ filterTests(["all"], () => { xit("Should create two applications with the same name", () => { // It is not possible to have applications with the same name const appName = "Cypress Tests" - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 } .contains("Create app") .click({ force: true }) @@ -80,18 +80,15 @@ filterTests(["all"], () => { const appName = "Cypress Tests" const numberName = 12345 const specialCharName = "£$%^" - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, numberName) - cy.reload() cy.applicationInAppTable(numberName) - cy.reload() renameApp(numberName, specialCharName) cy.get(interact.ERROR).should( "have.text", "App name must be letters, numbers and spaces only" ) // Set app name back to Cypress Tests - cy.reload() renameApp(numberName, appName) }) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index 9d77b89c57..b2ab8e678c 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -134,15 +134,18 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { const shouldCreateDefaultTable = typeof addDefaultTable != "boolean" ? true : addDefaultTable - cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) - cy.get(`[data-cy="create-app-btn"]`, { timeout: 2000 }).click({ force: true }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) + cy.wait(1000) + cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true }) // If apps already exist cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") .then(val => { if (val.length > 0) { - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ + force: true, + }) } }) @@ -400,17 +403,19 @@ Cypress.Commands.add("createAppFromScratch", appName => { Cypress.Commands.add("createTable", (tableName, initialTable) => { if (!initialTable) { cy.navigateToDataSection() - cy.get(`[data-cy="new-table"]`).click() + cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click() } cy.wait(2000) - cy.get(".item") + cy.get(".item", { timeout: 2000 }) .contains("Budibase DB") .click({ force: true }) .then(() => { - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 2000 }) + .contains("Continue") + .click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { - cy.get("input", { timeout: 1000 }).first().type(tableName).blur() + cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => { + cy.get("input", { timeout: 2000 }).first().type(tableName).blur() cy.get(".spectrum-ButtonGroup").contains("Create").click() }) cy.contains(tableName).should("be.visible") @@ -504,12 +509,13 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => { // DESIGN AREA Cypress.Commands.add("addComponent", (category, component) => { if (category) { - cy.get(`[data-cy="category-${category}"]`, { timeout: 1000 }).click({ + cy.get(`[data-cy="category-${category}"]`, { timeout: 3000 }).click({ force: true, }) } + cy.wait(500) if (component) { - cy.get(`[data-cy="component-${component}"]`, { timeout: 1000 }).click({ + cy.get(`[data-cy="component-${component}"]`, { timeout: 3000 }).click({ force: true, }) } @@ -517,7 +523,7 @@ Cypress.Commands.add("addComponent", (category, component) => { cy.location().then(loc => { const params = loc.pathname.split("/") const componentId = params[params.length - 1] - cy.getComponent(componentId).should("exist") + cy.getComponent(componentId, { timeout: 3000 }).should("exist") return cy.wrap(componentId) }) }) @@ -621,8 +627,8 @@ Cypress.Commands.add("navigateToFrontend", () => { // Clicks on Design tab and then the Home nav item cy.wait(500) cy.contains("Design").click() - cy.get(".spectrum-Search").type("/") - cy.get(".nav-item").contains("home").click() + cy.get(".spectrum-Search", { timeout: 2000 }).type("/") + cy.get(".nav-item", { timeout: 2000 }).contains("home").click() }) Cypress.Commands.add("navigateToDataSection", () => { @@ -782,7 +788,7 @@ Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => { // MISC Cypress.Commands.add("closeModal", () => { - cy.get(".spectrum-Modal").within(() => { + cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => { cy.get(".close-icon").click() cy.wait(1000) // Wait for modal to close }) diff --git a/packages/builder/package.json b/packages/builder/package.json index 5b77765034..37ecfdcad1 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.212-alpha.8", + "version": "1.0.219", "license": "GPL-3.0", "private": true, "scripts": { @@ -69,14 +69,15 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.212-alpha.8", - "@budibase/client": "^1.0.212-alpha.8", - "@budibase/frontend-core": "^1.0.212-alpha.8", - "@budibase/string-templates": "^1.0.212-alpha.8", + "@budibase/bbui": "^1.0.219", + "@budibase/client": "^1.0.219", + "@budibase/frontend-core": "^1.0.219", + "@budibase/string-templates": "^1.0.219", "@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", @@ -120,4 +121,4 @@ "vite": "^2.1.5" }, "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" -} +} \ No newline at end of file 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 2147d0be5b..bebd06c6d7 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} + +
+ + +
+