diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 854bc2e6dc..4b37418621 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: bug, linear +labels: bug assignees: '' --- diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index c64adb010f..1a2e74f863 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -56,11 +56,11 @@ jobs: run: yarn install:pro $BRANCH $BASE_BRANCH - run: yarn - run: yarn bootstrap + - run: yarn build:client - run: yarn test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml name: codecov-umbrella verbose: true diff --git a/.github/workflows/deploy-preprod.yml b/.github/workflows/deploy-preprod.yml index 803dd6af52..a1eee2c465 100644 --- a/.github/workflows/deploy-preprod.yml +++ b/.github/workflows/deploy-preprod.yml @@ -1,6 +1,10 @@ name: "deploy-preprod" on: workflow_dispatch: + inputs: + version: + description: Budibase release version. For example - 1.0.0 + required: false workflow_call: jobs: @@ -8,10 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + - name: Get the latest budibase release version + id: version + run: | + if [ -z "${{ github.event.inputs.version }}" ]; then + release_version=$(cat lerna.json | jq -r '.version') + else + release_version=${{ github.event.inputs.version }} + fi + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -26,7 +36,6 @@ jobs: -o values.preprod.yaml \ -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml wc -l values.preprod.yaml - - name: Deploy to Preprod Environment uses: budibase/helm@v1.8.0 with: @@ -37,7 +46,7 @@ jobs: helm: helm3 values: | globals: - appVersion: ${{ steps.previoustag.outputs.tag }} + appVersion: v${{ env.RELEASE_VERSION }} ingress: enabled: true nginx: true @@ -52,5 +61,5 @@ jobs: uses: tsickert/discord-webhook@v4.0.0 with: webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Preprod Deployment Complete: ${{ steps.previoustag.outputs.tag }} deployed to Budibase Pre-prod." - embed-title: ${{ steps.previoustag.outputs.tag }} \ No newline at end of file + content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod." + embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 3ae265fa21..20a48f5802 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -91,9 +91,11 @@ jobs: uses: azure/setup-helm@v1 id: helm-install - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV # due to helm repo index issue: https://github.com/helm/helm/issues/7363 # we need to create new package in a different dir, merge the index and move the package back @@ -116,8 +118,6 @@ jobs: git add -A git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}" git push - env: - RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }} deploy-to-legacy-preprod-env: needs: [release-images] @@ -130,13 +130,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - uses: passeidireto/trigger-external-workflow-action@main env: - PAYLOAD_VERSION: ${{ steps.previoustag.outputs.tag }} + PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} with: repository: budibase/budibase-deploys event: budicloud-preprod-deploy diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 6b0a0338d6..9ff7dc1ddc 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -62,16 +62,22 @@ spec: {{ end }} - name: ENABLE_ANALYTICS value: {{ .Values.globals.enableAnalytics | quote }} + - name: API_ENCRYPTION_KEY + value: {{ .Values.globals.apiEncryptionKey | quote }} - name: INTERNAL_API_KEY valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: internalApiKey + - name: INTERNAL_API_KEY_FALLBACK + value: {{ .Values.globals.internalApiKeyFallback | quote }} - name: JWT_SECRET valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: jwtSecret + - name: JWT_SECRET_FALLBACK + value: {{ .Values.globals.jwtSecretFallback | quote }} {{ if .Values.services.objectStore.region }} - name: AWS_REGION value: {{ .Values.services.objectStore.region }} @@ -125,9 +131,9 @@ spec: - name: SELF_HOSTED value: {{ .Values.globals.selfHosted | quote }} - name: SENTRY_DSN - value: {{ .Values.globals.sentryDSN }} + value: {{ .Values.globals.sentryDSN | quote }} - name: POSTHOG_TOKEN - value: {{ .Values.globals.posthogToken }} + value: {{ .Values.globals.posthogToken | quote }} - name: WORKER_URL value: http://worker-service:{{ .Values.services.worker.port }} - name: PLATFORM_URL @@ -198,8 +204,6 @@ spec: - name: GLOBAL_AGENT_NO_PROXY value: {{ .Values.globals.globalAgentNoProxy | quote }} {{ end }} - - name: CDN_URL - value: {{ .Values.globals.cdnUrl }} {{ if .Values.services.tlsRejectUnauthorized }} - name: NODE_TLS_REJECT_UNAUTHORIZED value: {{ .Values.services.tlsRejectUnauthorized }} @@ -228,6 +232,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{ end }} {{ if .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 6 }} diff --git a/charts/budibase/templates/couchdb-backup.yaml b/charts/budibase/templates/couchdb-backup.yaml index 68e5eab617..7396f97476 100644 --- a/charts/budibase/templates/couchdb-backup.yaml +++ b/charts/budibase/templates/couchdb-backup.yaml @@ -50,5 +50,8 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{ end }} status: {} {{- end }} diff --git a/charts/budibase/templates/minio-service-deployment.yaml b/charts/budibase/templates/minio-service-deployment.yaml index d0a367653d..41af2624bf 100644 --- a/charts/budibase/templates/minio-service-deployment.yaml +++ b/charts/budibase/templates/minio-service-deployment.yaml @@ -72,6 +72,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{ end }} {{ if .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 6 }} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index 0dea38fcbd..42af458a8c 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -78,6 +78,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{ end }} {{ if .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 6 }} diff --git a/charts/budibase/templates/redis-service-deployment.yaml b/charts/budibase/templates/redis-service-deployment.yaml index 5916c6d3f9..9b39d14291 100644 --- a/charts/budibase/templates/redis-service-deployment.yaml +++ b/charts/budibase/templates/redis-service-deployment.yaml @@ -50,6 +50,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{ end }} {{ if .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 6 }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index f4305fbb00..f998e5dfb9 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -62,16 +62,22 @@ spec: {{ else }} value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }} {{ end }} + - name: API_ENCRYPTION_KEY + value: {{ .Values.globals.apiEncryptionKey | quote }} - name: INTERNAL_API_KEY valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: internalApiKey + - name: INTERNAL_API_KEY_FALLBACK + value: {{ .Values.globals.internalApiKeyFallback | quote }} - name: JWT_SECRET valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: jwtSecret + - name: JWT_SECRET_FALLBACK + value: {{ .Values.globals.jwtSecretFallback | quote }} {{ if .Values.services.objectStore.region }} - name: AWS_REGION value: {{ .Values.services.objectStore.region }} @@ -188,8 +194,6 @@ spec: - name: GLOBAL_AGENT_NO_PROXY value: {{ .Values.globals.globalAgentNoProxy | quote }} {{ end }} - - name: CDN_URL - value: {{ .Values.globals.cdnUrl }} {{ if .Values.services.tlsRejectUnauthorized }} - name: NODE_TLS_REJECT_UNAUTHORIZED value: {{ .Values.services.tlsRejectUnauthorized }} @@ -218,6 +222,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{ end }} {{ if .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 6 }} diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 536af8560f..ed4ff014a9 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -96,9 +96,13 @@ globals: createSecrets: true # creates an internal API key, JWT secrets and redis password for you # if createSecrets is set to false, you can hard-code your secrets here + apiEncryptionKey: "" internalApiKey: "" jwtSecret: "" cdnUrl: "" + # fallback values used during live rotation + internalApiKeyFallback: "" + jwtSecretFallback: "" smtp: enabled: false diff --git a/hosting/.env b/hosting/.env index 07b506a6b2..c2b6d55eef 100644 --- a/hosting/.env +++ b/hosting/.env @@ -3,6 +3,7 @@ MAIN_PORT=10000 # This section contains all secrets pertaining to the system # These should be updated +API_ENCRYPTION_KEY=testsecret JWT_SECRET=testsecret MINIO_ACCESS_KEY=budibase MINIO_SECRET_KEY=budibase diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index d36937910f..bad34a20ea 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -17,6 +17,7 @@ services: INTERNAL_API_KEY: ${INTERNAL_API_KEY} BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} PORT: 4002 + API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY} JWT_SECRET: ${JWT_SECRET} LOG_LEVEL: info SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131 @@ -40,6 +41,7 @@ services: SELF_HOSTED: 1 PORT: 4003 CLUSTER_PORT: ${MAIN_PORT} + API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY} JWT_SECRET: ${JWT_SECRET} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} diff --git a/hosting/hosting.properties b/hosting/hosting.properties index c5638a266f..6c1d9e5dbd 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -3,6 +3,7 @@ MAIN_PORT=10000 # This section contains all secrets pertaining to the system # These should be updated +API_ENCRYPTION_KEY=testsecret JWT_SECRET=testsecret MINIO_ACCESS_KEY=budibase MINIO_SECRET_KEY=budibase diff --git a/lerna.json b/lerna.json index 1b740fff84..d0fa1c5436 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.4.12-alpha.3", + "version": "2.4.27-alpha.9", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 815e470916..592d389dec 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev", "bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh", "build": "lerna run build", + "build:client": "lerna run build --ignore @budibase/backend-core --ignore @budibase/worker --ignore @budibase/server --ignore @budibase/builder --ignore @budibase/cli --ignore @budibase/sdk", "build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput", "build:backend": "lerna run build --ignore @budibase/client --ignore @budibase/bbui --ignore @budibase/builder --ignore @budibase/cli", "build:sdk": "lerna run build:sdk", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index effd2038c0..0e3ab5e001 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.4.12-alpha.3", + "version": "2.4.27-alpha.9", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.2", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.4.12-alpha.3", + "@budibase/types": "2.4.27-alpha.9", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/backend-core/scripts/test.sh b/packages/backend-core/scripts/test.sh index 4bf1900984..2965134399 100644 --- a/packages/backend-core/scripts/test.sh +++ b/packages/backend-core/scripts/test.sh @@ -3,10 +3,10 @@ if [[ -n $CI ]] then # --runInBand performs better in ci where resources are limited - echo "jest --coverage --runInBand" - jest --coverage --runInBand + echo "jest --coverage --runInBand --forceExit" + jest --coverage --runInBand --forceExit else # --maxWorkers performs better in development echo "jest --coverage" jest --coverage -fi \ No newline at end of file +fi diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index 7e6fe4bcee..26c7cd4e26 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,6 +1,5 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy -const JwtStrategy = require("passport-jwt").Strategy import { getGlobalDB } from "../context" import { Cookie } from "../constants" import { getSessionsForUser, invalidateSessions } from "../security/sessions" @@ -8,7 +7,6 @@ import { authenticated, csrf, google, - jwt as jwtPassport, local, oidc, tenancy, @@ -21,14 +19,11 @@ import { OIDCInnerConfig, PlatformLogoutOpts, SSOProviderType, - User, } from "@budibase/types" -import { logAlert } from "../logging" import * as events from "../events" import * as configs from "../configs" import { clearCookie, getCookie } from "../utils" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" -import env from "../environment" const refresh = require("passport-oauth2-refresh") export { @@ -51,25 +46,6 @@ export const jwt = require("jsonwebtoken") // Strategies _passport.use(new LocalStrategy(local.options, local.authenticate)) -if (jwtPassport.options.secretOrKey) { - _passport.use(new JwtStrategy(jwtPassport.options, jwtPassport.authenticate)) -} else if (!env.DISABLE_JWT_WARNING) { - logAlert("No JWT Secret supplied, cannot configure JWT strategy") -} - -_passport.serializeUser((user: User, done: any) => done(null, user)) - -_passport.deserializeUser(async (user: User, done: any) => { - const db = getGlobalDB() - - try { - const dbUser = await db.get(user._id) - return done(null, dbUser) - } catch (err) { - console.error(`User not found`, err) - return done(null, false, { message: "User not found" }) - } -}) async function refreshOIDCAccessToken( chosenConfig: OIDCInnerConfig, diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index cba2f0138a..71ce4ba9ac 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -199,6 +199,10 @@ export class QueryBuilder { return this } + setAllOr() { + this.query.allOr = true + } + handleSpaces(input: string) { if (this.noEscaping) { return input @@ -236,6 +240,36 @@ export class QueryBuilder { return value } + isMultiCondition() { + let count = 0 + for (let filters of Object.values(this.query)) { + // not contains is one massive filter in allOr mode + if (typeof filters === "object") { + count += Object.keys(filters).length + } + } + return count > 1 + } + + compressFilters(filters: Record) { + const compressed: typeof filters = {} + for (let key of Object.keys(filters)) { + const finalKey = removeKeyNumbering(key) + if (compressed[finalKey]) { + compressed[finalKey] = compressed[finalKey].concat(filters[key]) + } else { + compressed[finalKey] = filters[key] + } + } + // add prefixes back + const final: typeof filters = {} + let count = 1 + for (let [key, value] of Object.entries(compressed)) { + final[`${count++}:${key}`] = value + } + return final + } + buildSearchQuery() { const builder = this let allOr = this.query && this.query.allOr @@ -272,9 +306,9 @@ export class QueryBuilder { } const notContains = (key: string, value: any) => { - // @ts-ignore - const allPrefix = allOr === "" ? "*:* AND" : "" - return allPrefix + "NOT " + contains(key, value) + const allPrefix = allOr ? "*:* AND " : "" + const mode = allOr ? "AND" : undefined + return allPrefix + "NOT " + contains(key, value, mode) } const containsAny = (key: string, value: any) => { @@ -299,21 +333,32 @@ export class QueryBuilder { return `${key}:(${orStatement})` } - function build(structure: any, queryFn: any) { + function build( + structure: any, + queryFn: (key: string, value: any) => string | null, + opts?: { returnBuilt?: boolean; mode?: string } + ) { + let built = "" for (let [key, value] of Object.entries(structure)) { // check for new format - remove numbering if needed key = removeKeyNumbering(key) key = builder.preprocess(builder.handleSpaces(key), { escape: true, }) - const expression = queryFn(key, value) + let expression = queryFn(key, value) if (expression == null) { continue } - if (query.length > 0) { - query += ` ${allOr ? "OR" : "AND"} ` + if (built.length > 0 || query.length > 0) { + const mode = opts?.mode ? opts.mode : allOr ? "OR" : "AND" + built += ` ${mode} ` } - query += expression + built += expression + } + if (opts?.returnBuilt) { + return built + } else { + query += built } } @@ -384,14 +429,14 @@ export class QueryBuilder { build(this.query.contains, contains) } if (this.query.notContains) { - build(this.query.notContains, notContains) + build(this.compressFilters(this.query.notContains), notContains) } if (this.query.containsAny) { build(this.query.containsAny, containsAny) } // make sure table ID is always added as an AND if (tableId) { - query = `(${query})` + query = this.isMultiCondition() ? `(${query})` : query allOr = false build({ tableId }, equal) } diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index 23b01e18df..52017cc94c 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -6,9 +6,13 @@ import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene" const INDEX_NAME = "main" const index = `function(doc) { - let props = ["property", "number"] + let props = ["property", "number", "array"] for (let key of props) { - if (doc[key]) { + if (Array.isArray(doc[key])) { + for (let val of doc[key]) { + index(key, val) + } + } else if (doc[key]) { index(key, doc[key]) } } @@ -21,9 +25,14 @@ describe("lucene", () => { dbName = `db-${newid()}` // create the DB for testing db = getDB(dbName) - await db.put({ _id: newid(), property: "word" }) - await db.put({ _id: newid(), property: "word2" }) - await db.put({ _id: newid(), property: "word3", number: 1 }) + await db.put({ _id: newid(), property: "word", array: ["1", "4"] }) + await db.put({ _id: newid(), property: "word2", array: ["3", "1"] }) + await db.put({ + _id: newid(), + property: "word3", + number: 1, + array: ["1", "2"], + }) }) it("should be able to create a lucene index", async () => { @@ -118,6 +127,15 @@ describe("lucene", () => { const resp = await builder.run() expect(resp.rows.length).toBe(2) }) + + it("should be able to perform an or not contains search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotContains("array", ["1"]) + builder.addNotContains("array", ["2"]) + builder.setAllOr() + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) }) describe("paginated search", () => { diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 8dc2cce487..f1c96c7fec 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -30,6 +30,12 @@ const DefaultBucketName = { const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") +function getAPIEncryptionKey() { + return process.env.API_ENCRYPTION_KEY + ? process.env.API_ENCRYPTION_KEY + : process.env.JWT_SECRET // fallback to the JWT_SECRET used historically +} + const environment = { isTest, isJest, @@ -39,7 +45,9 @@ const environment = { }, JS_BCRYPT: process.env.JS_BCRYPT, JWT_SECRET: process.env.JWT_SECRET, + JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, + API_ENCRYPTION_KEY: getAPIEncryptionKey(), COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_USERNAME: process.env.COUCH_DB_USER, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, @@ -55,6 +63,7 @@ const environment = { MINIO_URL: process.env.MINIO_URL, MINIO_ENABLED: process.env.MINIO_ENABLED || 1, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, + INTERNAL_API_KEY_FALLBACK: process.env.INTERNAL_API_KEY_FALLBACK, MULTI_TENANCY: process.env.MULTI_TENANCY, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", diff --git a/packages/backend-core/src/errors/base.ts b/packages/backend-core/src/errors/base.ts deleted file mode 100644 index 801dcf168d..0000000000 --- a/packages/backend-core/src/errors/base.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class BudibaseError extends Error { - code: string - type: string - - constructor(message: string, code: string, type: string) { - super(message) - this.code = code - this.type = type - } -} diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index 83e2ab5072..54ca8456ab 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -1,37 +1,99 @@ -import * as licensing from "./licensing" +// BASE -// combine all error codes into single object +export abstract class BudibaseError extends Error { + code: string -export const codes = { - ...licensing.codes, + constructor(message: string, code: ErrorCode) { + super(message) + this.code = code + } + + protected getPublicError?(): any } -// combine all error types -export const types = [licensing.type] +// ERROR HANDLING -// combine all error contexts -const context = { - ...licensing.context, +export enum ErrorCode { + USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded", + FEATURE_DISABLED = "feature_disabled", + INVALID_API_KEY = "invalid_api_key", + HTTP = "http", } -// derive a public error message using codes, types and any custom contexts +/** + * For the given error, build the public representation that is safe + * to be exposed over an api. + */ export const getPublicError = (err: any) => { let error - if (err.code || err.type) { + if (err.code) { // add generic error information error = { code: err.code, - type: err.type, } - if (err.code && context[err.code]) { + if (err.getPublicError) { error = { ...error, // get any additional context from this error - ...context[err.code](err), + ...err.getPublicError(), } } } return error } + +// HTTP + +export class HTTPError extends BudibaseError { + status: number + + constructor(message: string, httpStatus: number, code = ErrorCode.HTTP) { + super(message, code) + this.status = httpStatus + } +} + +// LICENSING + +export class UsageLimitError extends HTTPError { + limitName: string + + constructor(message: string, limitName: string) { + super(message, 400, ErrorCode.USAGE_LIMIT_EXCEEDED) + this.limitName = limitName + } + + getPublicError() { + return { + limitName: this.limitName, + } + } +} + +export class FeatureDisabledError extends HTTPError { + featureName: string + + constructor(message: string, featureName: string) { + super(message, 400, ErrorCode.FEATURE_DISABLED) + this.featureName = featureName + } + + getPublicError() { + return { + featureName: this.featureName, + } + } +} + +// AUTH + +export class InvalidAPIKeyError extends BudibaseError { + constructor() { + super( + "Invalid API key - may need re-generated, or user doesn't exist", + ErrorCode.INVALID_API_KEY + ) + } +} diff --git a/packages/backend-core/src/errors/generic.ts b/packages/backend-core/src/errors/generic.ts deleted file mode 100644 index 71b3352438..0000000000 --- a/packages/backend-core/src/errors/generic.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BudibaseError } from "./base" - -export class GenericError extends BudibaseError { - constructor(message: string, code: string, type: string) { - super(message, code, type ? type : "generic") - } -} diff --git a/packages/backend-core/src/errors/http.ts b/packages/backend-core/src/errors/http.ts deleted file mode 100644 index 182e009f58..0000000000 --- a/packages/backend-core/src/errors/http.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { GenericError } from "./generic" - -export class HTTPError extends GenericError { - status: number - - constructor( - message: string, - httpStatus: number, - code = "http", - type = "generic" - ) { - super(message, code, type) - this.status = httpStatus - } -} diff --git a/packages/backend-core/src/errors/index.ts b/packages/backend-core/src/errors/index.ts index 814d836590..a079f46484 100644 --- a/packages/backend-core/src/errors/index.ts +++ b/packages/backend-core/src/errors/index.ts @@ -1,3 +1 @@ export * from "./errors" -export { UsageLimitError, FeatureDisabledError } from "./licensing" -export { HTTPError } from "./http" diff --git a/packages/backend-core/src/errors/licensing.ts b/packages/backend-core/src/errors/licensing.ts deleted file mode 100644 index 7ffcefa167..0000000000 --- a/packages/backend-core/src/errors/licensing.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { HTTPError } from "./http" - -export const type = "license_error" - -export const codes = { - USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", - FEATURE_DISABLED: "feature_disabled", -} - -export const context = { - [codes.USAGE_LIMIT_EXCEEDED]: (err: any) => { - return { - limitName: err.limitName, - } - }, - [codes.FEATURE_DISABLED]: (err: any) => { - return { - featureName: err.featureName, - } - }, -} - -export class UsageLimitError extends HTTPError { - limitName: string - - constructor(message: string, limitName: string) { - super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type) - this.limitName = limitName - } -} - -export class FeatureDisabledError extends HTTPError { - featureName: string - - constructor(message: string, featureName: string) { - super(message, 400, codes.FEATURE_DISABLED, type) - this.featureName = featureName - } -} diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 48569548e3..a6d5423756 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -24,6 +24,7 @@ export * as redis from "./redis" export * as locks from "./redis/redlockImpl" export * as utils from "./utils" export * as errors from "./errors" +export * as timers from "./timers" export { default as env } from "./environment" export { SearchParams } from "./db" // Add context to tenancy for backwards compatibility diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 0708581570..8a97319586 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -1,5 +1,10 @@ import { Cookie, Header } from "../constants" -import { getCookie, clearCookie, openJwt } from "../utils" +import { + getCookie, + clearCookie, + openJwt, + isValidInternalAPIKey, +} from "../utils" import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" import { buildMatcherRegex, matches } from "./matchers" @@ -9,6 +14,7 @@ import { decrypt } from "../security/encryption" import * as identity from "../context/identity" import env from "../environment" import { Ctx, EndpointMatcher } from "@budibase/types" +import { InvalidAPIKeyError, ErrorCode } from "../errors" const ONE_MINUTE = env.SESSION_UPDATE_PERIOD ? parseInt(env.SESSION_UPDATE_PERIOD) @@ -35,28 +41,35 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) { } async function checkApiKey(apiKey: string, populateUser?: Function) { - if (apiKey === env.INTERNAL_API_KEY) { + // check both the primary and the fallback internal api keys + // this allows for rotation + if (isValidInternalAPIKey(apiKey)) { return { valid: true } } const decrypted = decrypt(apiKey) const tenantId = decrypted.split(SEPARATOR)[0] return doInTenant(tenantId, async () => { - const db = getGlobalDB() - // api key is encrypted in the database - const userId = (await queryGlobalView( - ViewName.BY_API_KEY, - { - key: apiKey, - }, - db - )) as string + let userId + try { + const db = getGlobalDB() + // api key is encrypted in the database + userId = (await queryGlobalView( + ViewName.BY_API_KEY, + { + key: apiKey, + }, + db + )) as string + } catch (err) { + userId = undefined + } if (userId) { return { valid: true, user: await getUser(userId, tenantId, populateUser), } } else { - throw "Invalid API key" + throw new InvalidAPIKeyError() } }) } @@ -157,8 +170,10 @@ export default function ( console.error(`Auth Error: ${err.message}`) console.error(err) // invalid token, clear the cookie - if (err && err.name === "JsonWebTokenError") { + if (err?.name === "JsonWebTokenError") { clearCookie(ctx, Cookie.Auth) + } else if (err?.code === ErrorCode.INVALID_API_KEY) { + ctx.throw(403, err.message) } // allow configuring for public access if ((opts && opts.publicAllowed) || publicEndpoint) { diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index addeac6a1a..dce07168d4 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -1,4 +1,3 @@ -export * as jwt from "./passport/jwt" export * as local from "./passport/local" export * as google from "./passport/sso/google" export * as oidc from "./passport/sso/oidc" diff --git a/packages/backend-core/src/middleware/internalApi.ts b/packages/backend-core/src/middleware/internalApi.ts index fff761928b..dc73cd6b66 100644 --- a/packages/backend-core/src/middleware/internalApi.ts +++ b/packages/backend-core/src/middleware/internalApi.ts @@ -1,13 +1,21 @@ -import env from "../environment" import { Header } from "../constants" import { BBContext } from "@budibase/types" +import { isValidInternalAPIKey } from "../utils" /** * API Key only endpoint. */ export default async (ctx: BBContext, next: any) => { const apiKey = ctx.request.headers[Header.API_KEY] - if (apiKey !== env.INTERNAL_API_KEY) { + if (!apiKey) { + ctx.throw(403, "Unauthorized") + } + + if (Array.isArray(apiKey)) { + ctx.throw(403, "Unauthorized") + } + + if (!isValidInternalAPIKey(apiKey)) { ctx.throw(403, "Unauthorized") } diff --git a/packages/backend-core/src/middleware/passport/jwt.ts b/packages/backend-core/src/middleware/passport/jwt.ts deleted file mode 100644 index 95dc8f2656..0000000000 --- a/packages/backend-core/src/middleware/passport/jwt.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Cookie } from "../../constants" -import env from "../../environment" -import { authError } from "./utils" -import { BBContext } from "@budibase/types" - -export const options = { - secretOrKey: env.JWT_SECRET, - jwtFromRequest: function (ctx: BBContext) { - return ctx.cookies.get(Cookie.Auth) - }, -} - -export async function authenticate(jwt: Function, done: Function) { - try { - return done(null, jwt) - } catch (err) { - return authError(done, "JWT invalid", err) - } -} diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index c57ebafb1f..0658147709 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -4,6 +4,7 @@ import { JobQueue } from "./constants" import InMemoryQueue from "./inMemoryQueue" import BullQueue from "bull" import { addListeners, StalledFn } from "./listeners" +import * as timers from "../timers" const CLEANUP_PERIOD_MS = 60 * 1000 let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] @@ -29,8 +30,8 @@ export function createQueue( } addListeners(queue, jobQueue, opts?.removeStalledCb) QUEUES.push(queue) - if (!cleanupInterval) { - cleanupInterval = setInterval(cleanup, CLEANUP_PERIOD_MS) + if (!cleanupInterval && !env.isTest()) { + cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS) // fire off an initial cleanup cleanup().catch(err => { console.error(`Unable to cleanup automation queue initially - ${err}`) @@ -41,7 +42,7 @@ export function createQueue( export async function shutdown() { if (cleanupInterval) { - clearInterval(cleanupInterval) + timers.clear(cleanupInterval) } if (QUEUES.length) { for (let queue of QUEUES) { diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 951369496a..186865ccda 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -8,6 +8,7 @@ import { SEPARATOR, SelectableDatabase, } from "./utils" +import * as timers from "../timers" const RETRY_PERIOD_MS = 2000 const STARTUP_TIMEOUT_MS = 5000 @@ -117,9 +118,9 @@ function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) { return } // check if the connection is ready - const interval = setInterval(() => { + const interval = timers.set(() => { if (CONNECTED) { - clearInterval(interval) + timers.clear(interval) resolve("") } }, 500) diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index d0707cb850..d2c8f18f73 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -8,7 +8,7 @@ const RANDOM_BYTES = 16 const STRETCH_LENGTH = 32 export enum SecretOption { - JWT = "jwt", + API = "api", ENCRYPTION = "encryption", } @@ -19,10 +19,10 @@ function getSecret(secretOption: SecretOption): string { secret = env.ENCRYPTION_KEY secretName = "ENCRYPTION_KEY" break - case SecretOption.JWT: + case SecretOption.API: default: - secret = env.JWT_SECRET - secretName = "JWT_SECRET" + secret = env.API_ENCRYPTION_KEY + secretName = "API_ENCRYPTION_KEY" break } if (!secret) { @@ -37,7 +37,7 @@ function stretchString(string: string, salt: Buffer) { export function encrypt( input: string, - secretOption: SecretOption = SecretOption.JWT + secretOption: SecretOption = SecretOption.API ) { const salt = crypto.randomBytes(RANDOM_BYTES) const stretched = stretchString(getSecret(secretOption), salt) @@ -50,7 +50,7 @@ export function encrypt( export function decrypt( input: string, - secretOption: SecretOption = SecretOption.JWT + secretOption: SecretOption = SecretOption.API ) { const [salt, encrypted] = input.split(SEPARATOR) const saltBuffer = Buffer.from(salt, "hex") diff --git a/packages/backend-core/src/timers/index.ts b/packages/backend-core/src/timers/index.ts new file mode 100644 index 0000000000..c9d642709f --- /dev/null +++ b/packages/backend-core/src/timers/index.ts @@ -0,0 +1 @@ +export * from "./timers" diff --git a/packages/backend-core/src/timers/timers.ts b/packages/backend-core/src/timers/timers.ts new file mode 100644 index 0000000000..000be74821 --- /dev/null +++ b/packages/backend-core/src/timers/timers.ts @@ -0,0 +1,22 @@ +let intervals: NodeJS.Timeout[] = [] + +export function set(callback: () => any, period: number) { + const interval = setInterval(callback, period) + intervals.push(interval) + return interval +} + +export function clear(interval: NodeJS.Timeout) { + const idx = intervals.indexOf(interval) + if (idx !== -1) { + intervals.splice(idx, 1) + } + clearInterval(interval) +} + +export function cleanup() { + for (let interval of intervals) { + clearInterval(interval) + } + intervals = [] +} diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index 3efd40ca80..7c222a9831 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -1,5 +1,4 @@ import { getAllApps, queryGlobalView } from "../db" -import { options } from "../middleware/passport/jwt" import { Header, MAX_VALID_DATE, @@ -133,7 +132,30 @@ export function openJwt(token: string) { if (!token) { return token } - return jwt.verify(token, options.secretOrKey) + try { + return jwt.verify(token, env.JWT_SECRET) + } catch (e) { + if (env.JWT_SECRET_FALLBACK) { + // fallback to enable rotation + return jwt.verify(token, env.JWT_SECRET_FALLBACK) + } else { + throw e + } + } +} + +export function isValidInternalAPIKey(apiKey: string) { + if (env.INTERNAL_API_KEY && env.INTERNAL_API_KEY === apiKey) { + return true + } + // fallback to enable rotation + if ( + env.INTERNAL_API_KEY_FALLBACK && + env.INTERNAL_API_KEY_FALLBACK === apiKey + ) { + return true + } + return false } /** @@ -165,7 +187,7 @@ export function setCookie( opts = { sign: true } ) { if (value && opts && opts.sign) { - value = jwt.sign(value, options.secretOrKey) + value = jwt.sign(value, env.JWT_SECRET) } const config: SetOption = { diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts index ec8de2942e..3555973928 100644 --- a/packages/backend-core/tests/jestEnv.ts +++ b/packages/backend-core/tests/jestEnv.ts @@ -4,3 +4,4 @@ process.env.NODE_ENV = "jest" process.env.MOCK_REDIS = "1" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.ENABLE_4XX_HTTP_LOGGING = "0" +process.env.REDIS_PASSWORD = "budibase" diff --git a/packages/backend-core/tests/jestSetup.ts b/packages/backend-core/tests/jestSetup.ts index e786086de6..be81fbff75 100644 --- a/packages/backend-core/tests/jestSetup.ts +++ b/packages/backend-core/tests/jestSetup.ts @@ -1,5 +1,6 @@ import "./logging" import env from "../src/environment" +import { cleanup } from "../src/timers" import { mocks, testContainerUtils } from "./utilities" // must explicitly enable fetch mock @@ -21,3 +22,7 @@ if (!process.env.CI) { } testContainerUtils.setupEnv(env) + +afterAll(() => { + cleanup() +}) diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 6876f5748b..1160bbc322 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": "2.4.12-alpha.3", + "version": "2.4.27-alpha.9", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,8 +38,8 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/shared-core": "2.4.12-alpha.3", - "@budibase/string-templates": "2.4.12-alpha.3", + "@budibase/shared-core": "2.4.27-alpha.9", + "@budibase/string-templates": "2.4.27-alpha.9", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/Form/Core/File.svelte b/packages/bbui/src/Form/Core/File.svelte new file mode 100644 index 0000000000..618cccd941 --- /dev/null +++ b/packages/bbui/src/Form/Core/File.svelte @@ -0,0 +1,115 @@ + + + + +
+ {#if value} +
+ {#if previewUrl} + + {/if} +
{value.name}
+ {#if value.size} +
+ {#if value.size <= BYTES_IN_MB} + {`${value.size / BYTES_IN_KB} KB`} + {:else} + {`${value.size / BYTES_IN_MB} MB`} + {/if} +
+ {/if} + {#if !disabled || (allowClear === true && disabled)} +
+ +
+ {/if} +
+ {/if} + {title} +
+ + diff --git a/packages/bbui/src/Form/Core/index.js b/packages/bbui/src/Form/Core/index.js index 7c81cfd70b..b0edf52748 100644 --- a/packages/bbui/src/Form/Core/index.js +++ b/packages/bbui/src/Form/Core/index.js @@ -13,3 +13,4 @@ export { default as CoreDropzone } from "./Dropzone.svelte" export { default as CoreStepper } from "./Stepper.svelte" export { default as CoreRichTextField } from "./RichTextField.svelte" export { default as CoreSlider } from "./Slider.svelte" +export { default as CoreFile } from "./File.svelte" diff --git a/packages/bbui/src/Form/File.svelte b/packages/bbui/src/Form/File.svelte new file mode 100644 index 0000000000..03cacea814 --- /dev/null +++ b/packages/bbui/src/Form/File.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/bbui/src/Skeleton/Skeleton.svelte b/packages/bbui/src/Skeleton/Skeleton.svelte deleted file mode 100644 index 92357a8c75..0000000000 --- a/packages/bbui/src/Skeleton/Skeleton.svelte +++ /dev/null @@ -1,56 +0,0 @@ -
-
- -
-
- - diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index d7457508fd..d26b938dd5 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -4,7 +4,6 @@ import "./bbui.css" import "@spectrum-css/icon/dist/index-vars.css" // Components -export { default as Skeleton } from "./Skeleton/Skeleton.svelte" export { default as Input } from "./Form/Input.svelte" export { default as Stepper } from "./Form/Stepper.svelte" export { default as TextArea } from "./Form/TextArea.svelte" @@ -78,6 +77,7 @@ export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte" export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte" export { default as Slider } from "./Form/Slider.svelte" export { default as Accordion } from "./Accordion/Accordion.svelte" +export { default as File } from "./Form/File.svelte" // Renderers export { default as BoldRenderer } from "./Table/BoldRenderer.svelte" diff --git a/packages/builder/index.html b/packages/builder/index.html index e3383cda39..96abc8e582 100644 --- a/packages/builder/index.html +++ b/packages/builder/index.html @@ -1,17 +1,17 @@ + Budibase - - + + + \ No newline at end of file diff --git a/packages/builder/package.json b/packages/builder/package.json index b889d045bf..031bdf2315 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.4.12-alpha.3", + "version": "2.4.27-alpha.9", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,11 +58,11 @@ } }, "dependencies": { - "@budibase/bbui": "2.4.12-alpha.3", - "@budibase/client": "2.4.12-alpha.3", - "@budibase/frontend-core": "2.4.12-alpha.3", - "@budibase/shared-core": "2.4.12-alpha.3", - "@budibase/string-templates": "2.4.12-alpha.3", + "@budibase/bbui": "2.4.27-alpha.9", + "@budibase/client": "2.4.27-alpha.9", + "@budibase/frontend-core": "2.4.27-alpha.9", + "@budibase/shared-core": "2.4.27-alpha.9", + "@budibase/string-templates": "2.4.27-alpha.9", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 352f094507..b4293a2a0a 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -308,7 +308,7 @@ { name: "Auto Column", type: AUTO_TYPE }, ] } else { - return [ + let fields = [ FIELDS.STRING, FIELDS.BARCODEQR, FIELDS.LONGFORM, @@ -316,10 +316,13 @@ FIELDS.DATETIME, FIELDS.NUMBER, FIELDS.BOOLEAN, - FIELDS.ARRAY, FIELDS.FORMULA, - FIELDS.LINK, ] + // no-sql or a spreadsheet + if (!external || table.sql) { + fields = [...fields, FIELDS.LINK, FIELDS.ARRAY] + } + return fields } } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/CreateExternalTableModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/CreateExternalTableModal.svelte index 45269a365c..664b5629d4 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/CreateExternalTableModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/CreateExternalTableModal.svelte @@ -35,7 +35,9 @@ await datasources.fetch() $goto(`../../table/${table._id}`) } catch (error) { - notifications.error("Error saving table") + notifications.error( + `Error saving table - ${error?.message || "unknown error"}` + ) } } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte index 53d50d57a3..3bc1a1cdd9 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte @@ -74,6 +74,14 @@ } return capitalise(name) } + + function getDisplayError(error, configKey) { + return error?.replace( + new RegExp(`${configKey}`, "i"), + getDisplayName(configKey) + ) + } + function getFieldGroupKeys(fieldGroup) { return Object.entries(schema[fieldGroup].fields || {}) .filter(el => filter(el)) @@ -147,7 +155,7 @@ type={schema[configKey].type} on:change bind:value={config[configKey]} - error={$validation.errors[configKey]} + error={getDisplayError($validation.errors[configKey], configKey)} /> {:else if schema[configKey].type === "fieldGroup"} @@ -180,7 +188,7 @@ type={configKey === "port" ? "string" : schema[configKey].type} on:change bind:value={config[configKey]} - error={$validation.errors[configKey]} + error={getDisplayError($validation.errors[configKey], configKey)} environmentVariablesEnabled={$licensing.environmentVariablesEnabled} {handleUpgradePanel} /> diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 7d03dafeb9..c12ddab78d 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -1,15 +1,22 @@ - - Authenticate with your google account to use the {IntegrationNames[ - datasource.type - ]} integration. + {#if isGoogleConfigured === true} + + Authenticate with your google account to use the {IntegrationNames[ + datasource.type + ]} integration. + + save(datasource, true)} /> + {:else if isGoogleConfigured === false} + Google authentication is not enabled, please complete Google SSO + configuration. - - save(datasource, true)} /> + Configure Google SSO + {/if} diff --git a/packages/builder/src/components/portal/onboarding/TourPopover.svelte b/packages/builder/src/components/portal/onboarding/TourPopover.svelte index 37ed0c6350..68e2e68a49 100644 --- a/packages/builder/src/components/portal/onboarding/TourPopover.svelte +++ b/packages/builder/src/components/portal/onboarding/TourPopover.svelte @@ -15,20 +15,12 @@ $: tourKey = $store.tourKey $: tourStepKey = $store.tourStepKey - const initTour = targetKey => { - if (!targetKey) { + const updateTourStep = (targetStepKey, tourKey) => { + if (!tourKey) { return } - tourSteps = [...TOURS[targetKey]] - tourStepIdx = 0 - tourStep = { ...tourSteps[tourStepIdx] } - } - - $: initTour(tourKey) - - const updateTourStep = targetStepKey => { if (!tourSteps?.length) { - return + tourSteps = [...TOURS[tourKey]] } tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey) lastStep = tourStepIdx + 1 == tourSteps.length @@ -36,7 +28,7 @@ tourStep.onLoad() } - $: updateTourStep(tourStepKey) + $: updateTourStep(tourStepKey, tourKey) const showPopover = (tourStep, tourNodes, popover) => { if (!tourStep) { diff --git a/packages/builder/src/components/portal/onboarding/TourWrap.svelte b/packages/builder/src/components/portal/onboarding/TourWrap.svelte index 2fff8507c5..fd47d7b48e 100644 --- a/packages/builder/src/components/portal/onboarding/TourWrap.svelte +++ b/packages/builder/src/components/portal/onboarding/TourWrap.svelte @@ -8,20 +8,28 @@ let currentTourStep let ready = false + let registered = false let handler + const registerTourNode = (tourKey, stepKey) => { + if (ready && !registered && tourKey) { + currentTourStep = TOURS[tourKey].find(step => step.id === stepKey) + if (!currentTourStep) { + return + } + const elem = document.querySelector(currentTourStep.query) + handler = tourHandler(elem, stepKey) + registered = true + } + } + + $: tourKeyWatch = $store.tourKey + $: registerTourNode(tourKeyWatch, tourStepKey, ready) + onMount(() => { - if (!$store.tourKey) return - - currentTourStep = TOURS[$store.tourKey].find( - step => step.id === tourStepKey - ) - if (!currentTourStep) return - - const elem = document.querySelector(currentTourStep.query) - handler = tourHandler(elem, tourStepKey) ready = true }) + onDestroy(() => { if (handler) { handler.destroy() diff --git a/packages/builder/src/pages/builder/Branding.svelte b/packages/builder/src/pages/builder/Branding.svelte new file mode 100644 index 0000000000..142473abb8 --- /dev/null +++ b/packages/builder/src/pages/builder/Branding.svelte @@ -0,0 +1,32 @@ + + + + + + {platformTitle} + + {#if loaded && !$auth.user && faviconUrl} + + {:else} + + + {/if} + diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index 8d604e8790..086f2513b4 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -4,6 +4,7 @@ import { onMount } from "svelte" import { CookieUtils, Constants } from "@budibase/frontend-core" import { API } from "api" + import Branding from "./Branding.svelte" let loaded = false @@ -146,6 +147,9 @@ } + + + {#if loaded} {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte index 033881c0ac..092e429515 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte @@ -182,12 +182,13 @@ } const handleKeyDown = e => { - if (e.key === "Tab") { + if (e.key === "Tab" || e.key === "ArrowDown" || e.key === "ArrowUp") { // Cycle selected components on tab press if (selectedIndex == null) { selectedIndex = 0 } else { - selectedIndex = (selectedIndex + 1) % componentList.length + const direction = e.key === "ArrowUp" ? -1 : 1 + selectedIndex = (selectedIndex + direction) % componentList.length } e.preventDefault() e.stopPropagation() diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 06e09e4fee..547c914621 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -30,7 +30,7 @@ async function login() { form.validate() if (Object.keys(errors).length > 0) { - console.log("errors") + console.log("errors", errors) return } try { @@ -64,99 +64,106 @@ - - - - - {#if loaded} - logo - {/if} - Log in to Budibase - - - {#if loaded && ($organisation.google || $organisation.oidc)} - - - - - {/if} +{#if loaded} + + + + {#if loaded} + logo + {/if} + + {$organisation.loginHeading || "Log in to Budibase"} + + + + {#if loaded && ($organisation.google || $organisation.oidc)} + + + + + {/if} + {#if !$organisation.isSSOEnforced} + + + { + formData = { + ...formData, + username: e.detail, + } + }} + validate={() => { + let fieldError = { + username: !formData.username + ? "Please enter a valid email" + : undefined, + } + errors = handleError({ ...errors, ...fieldError }) + }} + error={errors.username} + /> + { + formData = { + ...formData, + password: e.detail, + } + }} + validate={() => { + let fieldError = { + password: !formData.password + ? "Please enter your password" + : undefined, + } + errors = handleError({ ...errors, ...fieldError }) + }} + error={errors.password} + /> + + {/if} + {#if !$organisation.isSSOEnforced} - - - { - formData = { - ...formData, - username: e.detail, - } - }} - validate={() => { - let fieldError = { - username: !formData.username - ? "Please enter a valid email" - : undefined, - } - errors = handleError({ ...errors, ...fieldError }) - }} - error={errors.username} - /> - { - formData = { - ...formData, - password: e.detail, - } - }} - validate={() => { - let fieldError = { - password: !formData.password - ? "Please enter your password" - : undefined, - } - errors = handleError({ ...errors, ...fieldError }) - }} - error={errors.password} - /> - + + + + + + + {/if} + + {#if cloud} + + By using Budibase Cloud +
+ you are agreeing to our + + License Agreement + + {/if}
- {#if !$organisation.isSSOEnforced} - - - - - - - {/if} - - {#if cloud} - - By using Budibase Cloud -
- you are agreeing to our - - License Agreement - - - {/if} -
-
+ +{/if} diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index bba046ce10..d2af329cb9 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -7,12 +7,10 @@ Divider, Label, Input, - Dropzone, notifications, Toggle, } from "@budibase/bbui" import { auth, organisation, admin } from "stores/portal" - import { API } from "api" import { writable } from "svelte/store" import { redirect } from "@roxi/routify" @@ -28,32 +26,14 @@ company: $organisation.company, platformUrl: $organisation.platformUrl, analyticsEnabled: $organisation.analyticsEnabled, - logo: $organisation.logoUrl - ? { url: $organisation.logoUrl, type: "image", name: "Logo" } - : null, }) - let loading = false - async function uploadLogo(file) { - try { - let data = new FormData() - data.append("file", file) - await API.uploadLogo(data) - } catch (error) { - notifications.error("Error uploading logo") - } - } + let loading = false async function saveConfig() { loading = true try { - // Upload logo if required - if ($values.logo && !$values.logo.url) { - await uploadLogo($values.logo) - await organisation.init() - } - const config = { isSSOEnforced: $values.isSSOEnforced, company: $values.company ?? "", @@ -61,11 +41,6 @@ analyticsEnabled: $values.analyticsEnabled, } - // Remove logo if required - if (!$values.logo) { - config.logoUrl = "" - } - // Update settings await organisation.save(config) } catch (error) { @@ -87,21 +62,7 @@ - + {#if !$admin.cloud}