From 94fc6d8b0fca5b31e8628628f8ea076b7a48aba8 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 11 Jul 2023 12:47:17 +0100 Subject: [PATCH 001/158] Merge commit --- .../ButtonActionEditor/ButtonActionDrawer.svelte | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte index aa8e1af950..cd7b40aaa0 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte @@ -175,6 +175,11 @@ } return allBindings } + + const toDisplay = eventKey => { + const type = actionTypes.find(action => action.name == eventKey) + return type?.displayName || type?.name + } @@ -200,7 +205,9 @@ @@ -231,7 +238,7 @@ >
- {index + 1}. {action[EVENT_TYPE_KEY]} + {index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
Date: Fri, 14 Jul 2023 09:11:34 +0100 Subject: [PATCH 002/158] Binding selection fixes, delete controller refactor and some fixes --- .../builder/src/builderStore/dataBinding.js | 2 + .../actions/DeleteRow.svelte | 78 +++++++++++-------- .../controls/ButtonActionEditor/manifest.json | 1 + .../src/components/app/table/Table.svelte | 8 ++ packages/client/src/utils/buttonActions.js | 50 ++++++++++-- .../server/src/api/controllers/public/rows.ts | 2 +- .../server/src/api/controllers/row/index.ts | 17 ++++ 7 files changed, 117 insertions(+), 41 deletions(-) diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index e9c8643bce..bbe116721a 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => { readableBinding: `${table._instanceName}.Selected rows`, category: "Selected rows", icon: "ViewRow", + display: { name: table._instanceName }, })) ) @@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => { )}.${makePropSafe("selectedRows")}`, readableBinding: `${block._instanceName}.Selected rows`, category: "Selected rows", + display: { name: block._instanceName }, })) ) } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte index 109eb9a956..e4a5f171ff 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte @@ -1,5 +1,5 @@
- - Please specify one or more rows to delete. +
+ + + {/if} +
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 2ec7235c59..6ed545f541 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -24,6 +24,7 @@ }, { "name": "Delete Row", + "displayName": "Delete Rows", "type": "data", "component": "DeleteRow" }, diff --git a/packages/client/src/components/app/table/Table.svelte b/packages/client/src/components/app/table/Table.svelte index 248151a7a2..0ed76317db 100644 --- a/packages/client/src/components/app/table/Table.svelte +++ b/packages/client/src/components/app/table/Table.svelte @@ -47,6 +47,14 @@ ) } + // If the data changes, double check that the selected elements are still present. + $: if (data) { + let rowIds = data.map(row => row._id) + if (rowIds.length) { + selectedRows = selectedRows.filter(row => rowIds.includes(row._id)) + } + } + const getFields = (schema, customColumns, showAutoColumns) => { // Check for an invalid column selection let invalid = false diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 68f312f0ad..97d0d827bd 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -101,13 +101,47 @@ const fetchRowHandler = async action => { } } -const deleteRowHandler = async action => { - const { tableId, revId, rowId, notificationOverride } = action.parameters - if (tableId && rowId) { +const deleteRowHandler = async (action, context) => { + const { tableId, rowId: rowConfig, notificationOverride } = action.parameters + + if (tableId && rowConfig) { try { - await API.deleteRow({ tableId, rowId, revId }) + let requestConfig + + let parsedRowConfig = [] + if (typeof rowConfig === "string") { + try { + parsedRowConfig = JSON.parse(rowConfig) + } catch (e) { + parsedRowConfig = rowConfig + .split(",") + .map(id => id.trim()) + .filter(id => id) + } + } else { + parsedRowConfig = rowConfig + } + + if ( + typeof parsedRowConfig === "object" && + parsedRowConfig.constructor === Object + ) { + requestConfig = [parsedRowConfig] + } else if (Array.isArray(parsedRowConfig)) { + requestConfig = parsedRowConfig + } + + if (!requestConfig.length) { + notificationStore.actions.warning("No valid rows were supplied") + return false + } + + const resp = await API.deleteRows({ tableId, rows: requestConfig }) + if (!notificationOverride) { - notificationStore.actions.success("Row deleted") + notificationStore.actions.success( + resp?.length == 1 ? "Row deleted" : `${resp.length} Rows deleted` + ) } // Refresh related datasources @@ -115,8 +149,10 @@ const deleteRowHandler = async action => { invalidateRelationships: true, }) } catch (error) { - // Abort next actions - return false + console.error(error) + notificationStore.actions.error( + "An error occurred while executing the query" + ) } } } diff --git a/packages/server/src/api/controllers/public/rows.ts b/packages/server/src/api/controllers/public/rows.ts index df856f1fe0..39cf85a2a3 100644 --- a/packages/server/src/api/controllers/public/rows.ts +++ b/packages/server/src/api/controllers/public/rows.ts @@ -5,7 +5,7 @@ import { convertBookmark } from "../../../utilities" // makes sure that the user doesn't need to pass in the type, tableId or _id params for // the call to be correct -function fixRow(row: Row, params: any) { +export function fixRow(row: Row, params: any) { if (!params || !row) { return row } diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 91270429a4..6a969affaf 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -5,6 +5,8 @@ import { isExternalTable } from "../../../integrations/utils" import { Ctx } from "@budibase/types" import * as utils from "./utils" import { gridSocket } from "../../../websockets" +import { addRev } from "../public/utils" +import { fixRow } from "../public/rows" function pickApi(tableId: any) { if (isExternalTable(tableId)) { @@ -88,7 +90,22 @@ export async function destroy(ctx: any) { const inputs = ctx.request.body const tableId = utils.getTableId(ctx) let response, row + if (inputs.rows) { + const targetRows = inputs.rows.map( + (row: { [key: string]: string | string }) => { + let processedRow = typeof row == "string" ? { _id: row } : row + return !processedRow._rev + ? addRev(fixRow(processedRow, ctx.params), tableId) + : fixRow(processedRow, ctx.params) + } + ) + + const rowDeletes = await Promise.all(targetRows) + if (rowDeletes) { + ctx.request.body.rows = rowDeletes + } + let { rows } = await quotas.addQuery( () => pickApi(tableId).bulkDestroy(ctx), { From 7f3dfc8bbaab834ad655654d4d366b98070fdda0 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 14 Jul 2023 11:42:22 +0100 Subject: [PATCH 003/158] Linting --- packages/client/src/utils/buttonActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 97d0d827bd..653ed6a98c 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -101,7 +101,7 @@ const fetchRowHandler = async action => { } } -const deleteRowHandler = async (action, context) => { +const deleteRowHandler = async action => { const { tableId, rowId: rowConfig, notificationOverride } = action.parameters if (tableId && rowConfig) { From ea52013503fe67d7fb6636857724a75ccfd2608c Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Sat, 15 Jul 2023 00:12:18 +0100 Subject: [PATCH 004/158] Offline licensing integration tests --- .../src/account-api/api/apis/LicenseAPI.ts | 42 ++++++++++- qa-core/src/account-api/fixtures/accounts.ts | 3 +- .../tests/licensing/offline.spec.ts | 72 +++++++++++++++++++ .../internal-api/api/BudibaseInternalAPI.ts | 3 + qa-core/src/internal-api/api/apis/BaseAPI.ts | 4 +- .../src/internal-api/api/apis/LicenseAPI.ts | 34 +++++++++ 6 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 qa-core/src/account-api/tests/licensing/offline.spec.ts create mode 100644 qa-core/src/internal-api/api/apis/LicenseAPI.ts diff --git a/qa-core/src/account-api/api/apis/LicenseAPI.ts b/qa-core/src/account-api/api/apis/LicenseAPI.ts index e0601fe127..a7c1d63c4b 100644 --- a/qa-core/src/account-api/api/apis/LicenseAPI.ts +++ b/qa-core/src/account-api/api/apis/LicenseAPI.ts @@ -1,5 +1,5 @@ import AccountInternalAPIClient from "../AccountInternalAPIClient" -import { Account, UpdateLicenseRequest } from "@budibase/types" +import { Account, CreateOfflineLicenseRequest, GetOfflineLicenseResponse, UpdateLicenseRequest } from "@budibase/types" import { Response } from "node-fetch" export default class LicenseAPI { @@ -28,4 +28,44 @@ export default class LicenseAPI { } return [response, json] } + + // TODO: Better approach for setting tenant id header + + async createOfflineLicense( + accountId: string, + tenantId: string, + body: CreateOfflineLicenseRequest, + opts: { status?: number } = {} + ): Promise { + const [response, json] = await this.client.post( + `/api/internal/accounts/${accountId}/license/offline`, + { + body, + internal: true, + headers: { + "x-budibase-tenant-id": tenantId + } + } + ) + expect(response.status).toBe(opts.status ? opts.status : 201) + return response + } + + async getOfflineLicense( + accountId: string, + tenantId: string, + opts: { status?: number } = {} + ): Promise<[Response, GetOfflineLicenseResponse]> { + const [response, json] = await this.client.get( + `/api/internal/accounts/${accountId}/license/offline`, + { + internal: true, + headers: { + "x-budibase-tenant-id": tenantId + } + } + ) + expect(response.status).toBe(opts.status ? opts.status : 200) + return [response, json] + } } diff --git a/qa-core/src/account-api/fixtures/accounts.ts b/qa-core/src/account-api/fixtures/accounts.ts index c6c6fa163f..fdb499bdb6 100644 --- a/qa-core/src/account-api/fixtures/accounts.ts +++ b/qa-core/src/account-api/fixtures/accounts.ts @@ -1,7 +1,7 @@ import { generator } from "../../shared" import { Hosting, CreateAccountRequest } from "@budibase/types" -export const generateAccount = (): CreateAccountRequest => { +export const generateAccount = (partial: Partial): CreateAccountRequest => { const uuid = generator.guid() const email = `${uuid}@budibase.com` @@ -16,5 +16,6 @@ export const generateAccount = (): CreateAccountRequest => { size: "10+", tenantId: tenant, tenantName: tenant, + ...partial, } } diff --git a/qa-core/src/account-api/tests/licensing/offline.spec.ts b/qa-core/src/account-api/tests/licensing/offline.spec.ts new file mode 100644 index 0000000000..f9627ed7e1 --- /dev/null +++ b/qa-core/src/account-api/tests/licensing/offline.spec.ts @@ -0,0 +1,72 @@ +import TestConfiguration from "../../config/TestConfiguration" +import * as fixures from "../../fixtures" +import { Hosting, Feature } from "@budibase/types" + +describe("offline", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + // TODO: Currently requires a self host install + account portal + // How do we flag this as a self host specific test? + + it("creates, activates and deletes offline license", async () => { + // installation: Delete any token + await config.internalApi.license.deleteOfflineLicenseToken() + + // installation: Assert token not found + let [getTokenRes] = await config.internalApi.license.getOfflineLicenseToken({ status: 404 }) + + // installation: Retrieve Identifier + const [getIdentifierRes, identifier] = await config.internalApi.license.getOfflineIdentifier() + + // account-portal: Create self-host account + const createAccountRequest = fixures.accounts.generateAccount({ hosting: Hosting.SELF }) + const [createAccountRes, account] = await config.accountsApi.accounts.create(createAccountRequest) + const accountId = account.accountId! + const tenantId = account.tenantId! + + // account-portal: Enable feature on license + await config.accountsApi.licenses.updateLicense(accountId, { + overrides: { + features: [Feature.OFFLINE] + } + }) + + // account-portal: Create offline token + const expireAt = new Date() + expireAt.setDate(new Date().getDate() + 1) + await config.accountsApi.licenses.createOfflineLicense( + accountId, + tenantId, + { + expireAt: expireAt.toISOString(), + installationIdentifierBase64: identifier.identifierBase64 + }) + + // account-portal: Retrieve offline token + const [getLicenseRes, offlineLicense] = await config.accountsApi.licenses.getOfflineLicense(accountId, tenantId) + + // installation: Activate offline token + await config.internalApi.license.activateOfflineLicenseToken({ + offlineLicenseToken: offlineLicense.offlineLicenseToken + }) + + // installation: Assert token found + await config.internalApi.license.getOfflineLicenseToken() + + // TODO: Assert on license for current user + + // installation: Remove the token + await config.internalApi.license.deleteOfflineLicenseToken() + + // installation: Assert token not found + await config.internalApi.license.getOfflineLicenseToken({ status: 404 }) + }) +}) \ No newline at end of file diff --git a/qa-core/src/internal-api/api/BudibaseInternalAPI.ts b/qa-core/src/internal-api/api/BudibaseInternalAPI.ts index 316775b1b9..9b55c22deb 100644 --- a/qa-core/src/internal-api/api/BudibaseInternalAPI.ts +++ b/qa-core/src/internal-api/api/BudibaseInternalAPI.ts @@ -11,6 +11,7 @@ import DatasourcesAPI from "./apis/DatasourcesAPI" import IntegrationsAPI from "./apis/IntegrationsAPI" import QueriesAPI from "./apis/QueriesAPI" import PermissionsAPI from "./apis/PermissionsAPI" +import LicenseAPI from "./apis/LicenseAPI" import BudibaseInternalAPIClient from "./BudibaseInternalAPIClient" import { State } from "../../types" @@ -30,6 +31,7 @@ export default class BudibaseInternalAPI { integrations: IntegrationsAPI queries: QueriesAPI permissions: PermissionsAPI + license: LicenseAPI constructor(state: State) { this.client = new BudibaseInternalAPIClient(state) @@ -47,5 +49,6 @@ export default class BudibaseInternalAPI { this.integrations = new IntegrationsAPI(this.client) this.queries = new QueriesAPI(this.client) this.permissions = new PermissionsAPI(this.client) + this.license = new LicenseAPI(this.client) } } diff --git a/qa-core/src/internal-api/api/apis/BaseAPI.ts b/qa-core/src/internal-api/api/apis/BaseAPI.ts index b7eae45087..c0a3b344d6 100644 --- a/qa-core/src/internal-api/api/apis/BaseAPI.ts +++ b/qa-core/src/internal-api/api/apis/BaseAPI.ts @@ -8,9 +8,9 @@ export default class BaseAPI { this.client = client } - async get(url: string): Promise<[Response, any]> { + async get(url: string, status?: number): Promise<[Response, any]> { const [response, json] = await this.client.get(url) - expect(response).toHaveStatusCode(200) + expect(response).toHaveStatusCode(status ? status : 200) return [response, json] } diff --git a/qa-core/src/internal-api/api/apis/LicenseAPI.ts b/qa-core/src/internal-api/api/apis/LicenseAPI.ts new file mode 100644 index 0000000000..772ead9166 --- /dev/null +++ b/qa-core/src/internal-api/api/apis/LicenseAPI.ts @@ -0,0 +1,34 @@ +import { Response } from "node-fetch" +import { + ActivateOfflineLicenseTokenRequest, + GetOfflineIdentifierResponse, + GetOfflineLicenseTokenResponse, +} from "@budibase/types" +import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient" +import BaseAPI from "./BaseAPI" + +export default class LicenseAPI extends BaseAPI { + constructor(client: BudibaseInternalAPIClient) { + super(client) + } + + async getOfflineLicenseToken(opts: { status?: number } = {}): Promise<[Response, GetOfflineLicenseTokenResponse]> { + const [response, body] = await this.get(`/global/license/offline`, opts.status) + return [response, body] + } + + async deleteOfflineLicenseToken(): Promise<[Response]> { + const [response] = await this.del(`/global/license/offline`, 204) + return [response] + } + + async activateOfflineLicenseToken(body: ActivateOfflineLicenseTokenRequest): Promise<[Response]> { + const [response] = await this.post(`/global/license/offline`, body) + return [response] + } + + async getOfflineIdentifier(): Promise<[Response, GetOfflineIdentifierResponse]> { + const [response, body] = await this.get(`/global/license/offline/identifier`) + return [response, body] + } +} From 6aeb31c355bf359b9411e8414d2d7e2fd53a9414 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 6 Jul 2023 20:44:48 +0100 Subject: [PATCH 005/158] Move OFFLINE_MODE to backend-core environment --- packages/backend-core/src/environment.ts | 1 + packages/server/src/environment.ts | 1 - packages/worker/src/api/controllers/system/environment.ts | 3 ++- packages/worker/src/environment.ts | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index eab8cd4c45..c0785ef419 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -156,6 +156,7 @@ const environment = { : false, VERSION: findVersion(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, + OFFLINE_MODE: process.env.OFFLINE_MODE, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts index 79ee5d977c..64b342d577 100644 --- a/packages/server/src/environment.ts +++ b/packages/server/src/environment.ts @@ -81,7 +81,6 @@ const environment = { SELF_HOSTED: process.env.SELF_HOSTED, HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT, FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main", - OFFLINE_MODE: process.env.OFFLINE_MODE, // old CLIENT_ID: process.env.CLIENT_ID, _set(key: string, value: any) { diff --git a/packages/worker/src/api/controllers/system/environment.ts b/packages/worker/src/api/controllers/system/environment.ts index f3f917c2dd..729a00fa88 100644 --- a/packages/worker/src/api/controllers/system/environment.ts +++ b/packages/worker/src/api/controllers/system/environment.ts @@ -1,10 +1,11 @@ import { Ctx } from "@budibase/types" import env from "../../../environment" +import { env as coreEnv } from "@budibase/backend-core" export const fetch = async (ctx: Ctx) => { ctx.body = { multiTenancy: !!env.MULTI_TENANCY, - offlineMode: !!env.OFFLINE_MODE, + offlineMode: !!coreEnv.OFFLINE_MODE, cloud: !env.SELF_HOSTED, accountPortalUrl: env.ACCOUNT_PORTAL_URL, disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 74d58831d9..678ffe7f14 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -61,7 +61,6 @@ const environment = { CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY, - OFFLINE_MODE: process.env.OFFLINE_MODE, /** * Mock the email service in use - links to ethereal hosted emails are logged instead. */ From 0e80766125b5e476d06748a49ec4d647961b0803 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 6 Jul 2023 20:46:25 +0100 Subject: [PATCH 006/158] Update license endpoints to provide consistent pattern for offline license and license key (create, read, delete) --- packages/types/src/api/web/global/index.ts | 1 + packages/types/src/api/web/global/license.ts | 19 +++++ .../src/api/controllers/global/license.ts | 69 ++++++++++++++----- .../worker/src/api/routes/global/license.ts | 21 +++++- 4 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 packages/types/src/api/web/global/license.ts diff --git a/packages/types/src/api/web/global/index.ts b/packages/types/src/api/web/global/index.ts index bf4b43f0ba..efcb6dc39c 100644 --- a/packages/types/src/api/web/global/index.ts +++ b/packages/types/src/api/web/global/index.ts @@ -3,3 +3,4 @@ export * from "./auditLogs" export * from "./events" export * from "./configs" export * from "./scim" +export * from "./license" diff --git a/packages/types/src/api/web/global/license.ts b/packages/types/src/api/web/global/license.ts new file mode 100644 index 0000000000..34de2e4361 --- /dev/null +++ b/packages/types/src/api/web/global/license.ts @@ -0,0 +1,19 @@ +// LICENSE KEY + +export interface ActivateLicenseKeyRequest { + licenseKey: string +} + +export interface GetLicenseKeyResponse { + licenseKey: string, +} + +// OFFLINE LICENSE + +export interface ActivateOfflineLicenseRequest { + offlineLicense: string +} + +export interface GetOfflineLicenseResponse { + offlineLicense: string +} diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index 2bd173010f..b4670e0dbb 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -1,33 +1,66 @@ import { licensing, quotas } from "@budibase/pro" +import { + ActivateLicenseKeyRequest, + ActivateOfflineLicenseRequest, + GetLicenseKeyResponse, + GetOfflineLicenseResponse, + UserCtx, +} from "@budibase/types" -export const activate = async (ctx: any) => { +// LICENSE KEY + +export async function activateLicenseKey(ctx: UserCtx) { const { licenseKey } = ctx.request.body - if (!licenseKey) { - ctx.throw(400, "licenseKey is required") - } - await licensing.activateLicenseKey(licenseKey) ctx.status = 200 } +export async function getLicenseKey(ctx: UserCtx) { + const licenseKey = await licensing.getLicenseKey() + if (licenseKey) { + ctx.body = { licenseKey: "*" } + ctx.status = 200 + } else { + ctx.status = 404 + } +} + +export async function deleteLicenseKey(ctx: UserCtx) { + await licensing.deleteLicenseKey() + ctx.status = 204 +} + +// OFFLINE LICENSE + +export async function activateOfflineLicense(ctx: UserCtx) { + const { offlineLicense } = ctx.request.body + await licensing.activateOfflineLicense(offlineLicense) + ctx.status = 200 +} + +export async function getOfflineLicense(ctx: UserCtx) { + const offlineLicense = await licensing.getOfflineLicense() + if (offlineLicense) { + ctx.body = { offlineLicense: "*" } + ctx.status = 200 + } else { + ctx.status = 404 + } +} + +export async function deleteOfflineLicense(ctx: UserCtx) { + await licensing.deleteOfflineLicense() + ctx.status = 204 +} + +// LICENSES + export const refresh = async (ctx: any) => { await licensing.cache.refresh() ctx.status = 200 } -export const getInfo = async (ctx: any) => { - const licenseInfo = await licensing.getLicenseInfo() - if (licenseInfo) { - licenseInfo.licenseKey = "*" - ctx.body = licenseInfo - } - ctx.status = 200 -} - -export const deleteInfo = async (ctx: any) => { - await licensing.deleteLicenseInfo() - ctx.status = 200 -} +// USAGE export const getQuotaUsage = async (ctx: any) => { ctx.body = await quotas.getQuotaUsage() diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index 0fb2a6e8bd..1d6185a402 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -1,13 +1,28 @@ import Router from "@koa/router" import * as controller from "../../controllers/global/license" +import { middleware } from "@budibase/backend-core" +import Joi from "joi" + +const activateLicenseKeyValidator = middleware.joiValidator.body(Joi.object({ + licenseKey: Joi.string().required(), + }).required()) + +const activateOfflineLicenseValidator = middleware.joiValidator.body(Joi.object({ + offlineLicense: Joi.string().required(), + }).required()) const router: Router = new Router() router - .post("/api/global/license/activate", controller.activate) .post("/api/global/license/refresh", controller.refresh) - .get("/api/global/license/info", controller.getInfo) - .delete("/api/global/license/info", controller.deleteInfo) .get("/api/global/license/usage", controller.getQuotaUsage) + // LICENSE KEY + .post("/api/global/license/key", activateLicenseKeyValidator, controller.activateLicenseKey) + .get("/api/global/license/key", controller.getLicenseKey) + .delete("/api/global/license/key", controller.deleteLicenseKey) + // OFFLINE LICENSE + .post("/api/global/license/offline", activateOfflineLicenseValidator, controller.activateOfflineLicense) + .get("/api/global/license/offline", controller.getOfflineLicense) + .delete("/api/global/license/offline", controller.deleteOfflineLicense) export default router From d3f9348403b64f8b2971bcfdb1c02bcbb24755f5 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 6 Jul 2023 20:47:12 +0100 Subject: [PATCH 007/158] Updates to upgrade page to change config based on offlineMode value --- .../builder/portal/account/upgrade.svelte | 215 +++++++++++++----- packages/builder/src/stores/portal/admin.js | 1 + packages/frontend-core/src/api/licensing.js | 57 +++-- 3 files changed, 201 insertions(+), 72 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/account/upgrade.svelte b/packages/builder/src/pages/builder/portal/account/upgrade.svelte index f0ee87bde5..a37625a5dc 100644 --- a/packages/builder/src/pages/builder/portal/account/upgrade.svelte +++ b/packages/builder/src/pages/builder/portal/account/upgrade.svelte @@ -10,6 +10,8 @@ Label, ButtonGroup, notifications, + CopyInput, + File } from "@budibase/bbui" import { auth, admin } from "stores/portal" import { redirect } from "@roxi/routify" @@ -21,44 +23,122 @@ $: license = $auth.user.license $: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` + // LICENSE KEY $: activateDisabled = !licenseKey || licenseKeyDisabled - - let licenseInfo - let licenseKeyDisabled = false let licenseKeyType = "text" let licenseKey = "" let deleteLicenseKeyModal + // OFFLINE LICENSE + let installationIdentifier = undefined + let offlineLicense = undefined + const offlineLicenseExtensions = [ + ".txt", + ] + // Make sure page can't be visited directly in cloud $: { if ($admin.cloud) { $redirect("../../portal") } + + console.log({ offlineLicense }) } - const activate = async () => { + // LICENSE KEY + + const getLicenseKey = async () => { try { - await API.activateLicenseKey({ licenseKey }) - await auth.getSelf() - await setLicenseInfo() - notifications.success("Successfully activated") + licenseKey = await API.getLicenseKey() + if (licenseKey) { + licenseKey = "**********************************************" + licenseKeyType = "password" + licenseKeyDisabled = true + activateDisabled = true + } } catch (e) { - notifications.error(e.message) + console.error(e) + notifications.error("Error retrieving license key") } } - const destroy = async () => { + const activateLicenseKey = async () => { + try { + await API.activateLicenseKey({ licenseKey }) + await auth.getSelf() + await getLicenseKey() + notifications.success("Successfully activated") + } catch (e) { + console.error(e) + notifications.error("Error activating license key") + } + } + + const deleteLicenseKey = async () => { try { await API.deleteLicenseKey({ licenseKey }) await auth.getSelf() - await setLicenseInfo() + await getLicenseKey() // reset the form licenseKey = "" licenseKeyDisabled = false - notifications.success("Successfully deleted") + notifications.success("Offline license removed") } catch (e) { - notifications.error(e.message) + console.error(e) + notifications.error("Error deleting license key") + } + } + + // OFFLINE LICENSE + + const getOfflineLicense = async () => { + try { + const license = await API.getOfflineLicense() + if (license) { + offlineLicense = { + name: "license" + } + } else { + offlineLicense = undefined + } + } catch (e) { + console.error(e) + notifications.error("Error loading offline license") + } + } + + async function activateOfflineLicense(offlineLicense) { + try { + await API.activateOfflineLicense({ offlineLicense }) + await auth.getSelf() + await getOfflineLicense() + notifications.success("Successfully activated") + } catch (e) { + console.error(e) + notifications.error("Error activating offline license") + } + } + + async function deleteOfflineLicense() { + try { + await API.deleteOfflineLicense() + await auth.getSelf() + await getOfflineLicense() + notifications.success("Successfully removed ofline license") + } catch (e) { + console.error(e) + notifications.error("Error upload offline license") + } + } + + async function onOfflineLicenseChange(event) { + if (event.detail) { + const reader = new FileReader() + reader.readAsText(event.detail) + reader.onload = () => activateOfflineLicense(reader.result) + } else { + await deleteOfflineLicense() } } @@ -73,29 +153,19 @@ } } - // deactivate the license key field if there is a license key set - $: { - if (licenseInfo?.licenseKey) { - licenseKey = "**********************************************" - licenseKeyType = "password" - licenseKeyDisabled = true - activateDisabled = true - } - } - - const setLicenseInfo = async () => { - licenseInfo = await API.getLicenseInfo() - } - onMount(async () => { - await setLicenseInfo() + if ($admin.offlineMode) { + await getOfflineLicense() + } else { + await getLicenseKey() + } }) {#if $auth.isAdmin} @@ -112,38 +182,73 @@ - - Activate - Enter your license key below to activate your plan - - -
-
- - + Installation identifier + Share this with support@budibase.com to obtain your offline license + + +
+
+ +
+
+
+ + + License + Upload your license to activate your plan + + +
+
-
- - - {#if licenseInfo?.licenseKey} - - {/if} - - + {#if licenseKey} + + {/if} + + + {/if} - Plan - + Plan + You are currently on the {license.plan.type} plan +
+ If you purchase or update your plan on the account + portal, click the refresh button to sync those changes +
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { time: diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index b9467fd037..19abcfd841 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = { adminUser: { checked: false }, sso: { checked: false }, }, + offlineMode: false } export function createAdminStore() { diff --git a/packages/frontend-core/src/api/licensing.js b/packages/frontend-core/src/api/licensing.js index c27d79d740..5f7813cfca 100644 --- a/packages/frontend-core/src/api/licensing.js +++ b/packages/frontend-core/src/api/licensing.js @@ -1,32 +1,56 @@ export const buildLicensingEndpoints = API => ({ - /** - * Activates a self hosted license key - */ + + // LICENSE KEY + activateLicenseKey: async data => { return API.post({ - url: `/api/global/license/activate`, + url: `/api/global/license/key`, body: data, }) }, - - /** - * Delete a self hosted license key - */ deleteLicenseKey: async () => { return API.delete({ - url: `/api/global/license/info`, + url: `/api/global/license/key`, }) }, + getLicenseKey: async () => { + try { + return await API.get({ + url: "/api/global/license/key", + }) + } catch (e) { + if (e.status !== 404) { + throw e + } + } + }, - /** - * Get the license info - metadata about the license including the - * obfuscated license key. - */ - getLicenseInfo: async () => { - return API.get({ - url: "/api/global/license/info", + // OFFLINE LICENSE + + activateOfflineLicense: async ({ offlineLicense }) => { + return API.post({ + url: "/api/global/license/offline", + body: { + offlineLicense + }, }) }, + deleteOfflineLicense: async () => { + return API.delete({ + url: "/api/global/license/offline", + }) + }, + getOfflineLicense: async () => { + try { + return await API.get({ + url: "/api/global/license/offline", + }) + } catch (e) { + if (e.status !== 404) { + throw e + } + } + }, /** * Refreshes the license cache @@ -36,7 +60,6 @@ export const buildLicensingEndpoints = API => ({ url: "/api/global/license/refresh", }) }, - /** * Retrieve the usage information for the tenant */ From 8e2a551a1560a26e897f3a96d028baf3e1d1efc1 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 6 Jul 2023 21:26:46 +0100 Subject: [PATCH 008/158] Be more explicit about offline license vs offline license token --- packages/types/src/api/web/global/license.ts | 4 ++-- .../worker/src/api/controllers/global/license.ts | 12 ++++++------ packages/worker/src/api/routes/global/license.ts | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/types/src/api/web/global/license.ts b/packages/types/src/api/web/global/license.ts index 34de2e4361..0196b69c7f 100644 --- a/packages/types/src/api/web/global/license.ts +++ b/packages/types/src/api/web/global/license.ts @@ -11,9 +11,9 @@ export interface GetLicenseKeyResponse { // OFFLINE LICENSE export interface ActivateOfflineLicenseRequest { - offlineLicense: string + offlineLicenseToken: string } export interface GetOfflineLicenseResponse { - offlineLicense: string + offlineLicenseToken: string } diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index b4670e0dbb..b33efa1491 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -33,15 +33,15 @@ export async function deleteLicenseKey(ctx: UserCtx) { // OFFLINE LICENSE export async function activateOfflineLicense(ctx: UserCtx) { - const { offlineLicense } = ctx.request.body - await licensing.activateOfflineLicense(offlineLicense) + const { offlineLicenseToken } = ctx.request.body + await licensing.activateOfflineLicense(offlineLicenseToken) ctx.status = 200 } export async function getOfflineLicense(ctx: UserCtx) { - const offlineLicense = await licensing.getOfflineLicense() - if (offlineLicense) { - ctx.body = { offlineLicense: "*" } + const offlineLicenseToken = await licensing.getOfflineLicenseToken() + if (offlineLicenseToken) { + ctx.body = { offlineLicenseToken: "*" } ctx.status = 200 } else { ctx.status = 404 @@ -49,7 +49,7 @@ export async function getOfflineLicense(ctx: UserCtx) { - await licensing.deleteOfflineLicense() + await licensing.deleteOfflineLicenseToken() ctx.status = 204 } diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index 1d6185a402..889d7d1ed4 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -8,8 +8,8 @@ const activateLicenseKeyValidator = middleware.joiValidator.body(Joi.object({ }).required()) const activateOfflineLicenseValidator = middleware.joiValidator.body(Joi.object({ - offlineLicense: Joi.string().required(), - }).required()) + offlineLicenseToken: Joi.string().required(), +}).required()) const router: Router = new Router() From ec5381c9d58859c0ac56ef9798281127d29d0510 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 6 Jul 2023 21:58:20 +0100 Subject: [PATCH 009/158] Aesthetic UI updates --- .../builder/portal/account/upgrade.svelte | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/account/upgrade.svelte b/packages/builder/src/pages/builder/portal/account/upgrade.svelte index a37625a5dc..48ff3514b7 100644 --- a/packages/builder/src/pages/builder/portal/account/upgrade.svelte +++ b/packages/builder/src/pages/builder/portal/account/upgrade.svelte @@ -24,14 +24,16 @@ $: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` // LICENSE KEY + $: activateDisabled = !licenseKey || licenseKeyDisabled let licenseKeyDisabled = false let licenseKeyType = "text" let licenseKey = "" let deleteLicenseKeyModal - // OFFLINE LICENSE - let installationIdentifier = undefined + // OFFLINE + + let installationIdentifier = "aW5zdGFsbGF0aW9uSWQ9M2MwYmYyZjMtOGJlZi00YTBkLTllN2UtZTU4NmUxMDg2ZjVhLGluc3RhbGxhdGlvblRlbmFudElkPWU5ZWUwNDI0LTE4N2UtNDNhMS1hMDY1LTNiODhmZmE4YzJhZg==\n" let offlineLicense = undefined const offlineLicenseExtensions = [ ".txt", @@ -108,9 +110,9 @@ } } - async function activateOfflineLicense(offlineLicense) { + async function activateOfflineLicense(offlineLicenseToken) { try { - await API.activateOfflineLicense({ offlineLicense }) + await API.activateOfflineLicense({ offlineLicenseToken }) await auth.getSelf() await getOfflineLicense() notifications.success("Successfully activated") @@ -134,10 +136,16 @@ async function onOfflineLicenseChange(event) { if (event.detail) { + // prevent file preview jitter by assigning constant + // as soon as possible + offlineLicense = { + name: "license" + } const reader = new FileReader() reader.readAsText(event.detail) reader.onload = () => activateOfflineLicense(reader.result) } else { + offlineLicense = undefined await deleteOfflineLicense() } } @@ -178,6 +186,7 @@ {:else} To manage your plan visit your account +
 
{/if}
@@ -270,7 +279,7 @@ } .field { display: grid; - grid-template-columns: 100px 1fr; + grid-template-columns: 300px 1fr; grid-gap: var(--spacing-l); align-items: center; } From 7bc14235aabff2e0bd172a9ed7cfa0cd769294e0 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 6 Jul 2023 22:00:13 +0100 Subject: [PATCH 010/158] Update request body for offline license activation --- packages/frontend-core/src/api/licensing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/api/licensing.js b/packages/frontend-core/src/api/licensing.js index 5f7813cfca..cacf972971 100644 --- a/packages/frontend-core/src/api/licensing.js +++ b/packages/frontend-core/src/api/licensing.js @@ -27,11 +27,11 @@ export const buildLicensingEndpoints = API => ({ // OFFLINE LICENSE - activateOfflineLicense: async ({ offlineLicense }) => { + activateOfflineLicense: async ({ offlineLicenseToken }) => { return API.post({ url: "/api/global/license/offline", body: { - offlineLicense + offlineLicenseToken }, }) }, From 7c18a7a443b83dba4dc31b50fb6d57cd317b236f Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 7 Jul 2023 11:34:10 +0100 Subject: [PATCH 011/158] db / licenseInfo.spec.ts --- packages/worker/src/api/routes/global/tests/license.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts index be0673729e..d548ad83bb 100644 --- a/packages/worker/src/api/routes/global/tests/license.spec.ts +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -21,9 +21,6 @@ describe("/api/global/license", () => { describe("POST /api/global/license/refresh", () => {}) - describe("GET /api/global/license/info", () => {}) - - describe("DELETE /api/global/license/info", () => {}) describe("GET /api/global/license/usage", () => {}) }) From 6c3d01375b91b75de2beea8c36343d551a88816d Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 7 Jul 2023 11:48:12 +0100 Subject: [PATCH 012/158] Move license keys to their own module --- .../worker/src/api/controllers/global/license.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index b33efa1491..9310eb3739 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -11,12 +11,12 @@ import { export async function activateLicenseKey(ctx: UserCtx) { const { licenseKey } = ctx.request.body - await licensing.activateLicenseKey(licenseKey) + await licensing.keys.activateLicenseKey(licenseKey) ctx.status = 200 } export async function getLicenseKey(ctx: UserCtx) { - const licenseKey = await licensing.getLicenseKey() + const licenseKey = await licensing.keys.getLicenseKey() if (licenseKey) { ctx.body = { licenseKey: "*" } ctx.status = 200 @@ -26,7 +26,7 @@ export async function getLicenseKey(ctx: UserCtx) { } export async function deleteLicenseKey(ctx: UserCtx) { - await licensing.deleteLicenseKey() + await licensing.keys.deleteLicenseKey() ctx.status = 204 } @@ -34,12 +34,12 @@ export async function deleteLicenseKey(ctx: UserCtx) { export async function activateOfflineLicense(ctx: UserCtx) { const { offlineLicenseToken } = ctx.request.body - await licensing.activateOfflineLicense(offlineLicenseToken) + await licensing.offline.activateOfflineLicense(offlineLicenseToken) ctx.status = 200 } export async function getOfflineLicense(ctx: UserCtx) { - const offlineLicenseToken = await licensing.getOfflineLicenseToken() + const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken() if (offlineLicenseToken) { ctx.body = { offlineLicenseToken: "*" } ctx.status = 200 @@ -49,7 +49,7 @@ export async function getOfflineLicense(ctx: UserCtx) { - await licensing.deleteOfflineLicenseToken() + await licensing.offline.deleteOfflineLicenseToken() ctx.status = 204 } From 20c87b44b1479deaec0fe817acac6e4cea224818 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 7 Jul 2023 16:55:11 +0100 Subject: [PATCH 013/158] Allow pro to be mocked in worker --- packages/worker/__mocks__/@budibase/pro.ts | 26 +++++++++++++++++++ .../worker/src/tests/TestConfiguration.ts | 11 ++++---- packages/worker/src/tests/mocks/index.ts | 4 +++ 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 packages/worker/__mocks__/@budibase/pro.ts diff --git a/packages/worker/__mocks__/@budibase/pro.ts b/packages/worker/__mocks__/@budibase/pro.ts new file mode 100644 index 0000000000..a9611ba705 --- /dev/null +++ b/packages/worker/__mocks__/@budibase/pro.ts @@ -0,0 +1,26 @@ +const actual = jest.requireActual("@budibase/pro") +const pro = { + ...actual, + licensing: { + keys: { + activateLicenseKey: jest.fn(), + getLicenseKey: jest.fn(), + deleteLicenseKey: jest.fn(), + }, + offline: { + activateOfflineLicense: jest.fn(), + getOfflineLicenseToken: jest.fn(), + deleteOfflineLicenseToken: jest.fn(), + }, + cache: { + ...actual.licensing.cache, + refresh: jest.fn(), + } + }, + quotas: { + ...actual.quotas, + getQuotaUsage: jest.fn() + }, +} + +export = pro diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index a79ac0e189..b41b76efda 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -1,8 +1,7 @@ import mocks from "./mocks" // init the licensing mock -import * as pro from "@budibase/pro" -mocks.licenses.init(pro) +mocks.licenses.init(mocks.pro) // use unlimited license by default mocks.licenses.useUnlimited() @@ -238,21 +237,21 @@ class TestConfiguration { const db = context.getGlobalDB() - const id = dbCore.generateDevInfoID(this.user._id) + const id = dbCore.generateDevInfoID(this.user!._id) // TODO: dry this.apiKey = encryption.encrypt( `${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}` ) const devInfo = { _id: id, - userId: this.user._id, + userId: this.user!._id, apiKey: this.apiKey, } await db.put(devInfo) }) } - async getUser(email: string): Promise { + async getUser(email: string): Promise { return context.doInTenant(this.getTenantId(), () => { return users.getGlobalUserByEmail(email) }) @@ -264,7 +263,7 @@ class TestConfiguration { } const response = await this._req(user, null, controllers.users.save) const body = response as SaveUserResponse - return this.getUser(body.email) + return this.getUser(body.email) as Promise } // CONFIGS diff --git a/packages/worker/src/tests/mocks/index.ts b/packages/worker/src/tests/mocks/index.ts index 30bb4e1d09..cab019bb46 100644 --- a/packages/worker/src/tests/mocks/index.ts +++ b/packages/worker/src/tests/mocks/index.ts @@ -1,7 +1,11 @@ import * as email from "./email" import { mocks } from "@budibase/backend-core/tests" +import * as _pro from "@budibase/pro" +const pro = jest.mocked(_pro, true) + export default { email, + pro, ...mocks, } From c0568b9153ca16b29be00de6d4e76519d92edbd6 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 7 Jul 2023 20:07:15 +0100 Subject: [PATCH 014/158] api / license.spec.ts updates --- .../src/api/controllers/global/license.ts | 1 + .../api/routes/global/tests/license.spec.ts | 85 +++++++++++++++++-- packages/worker/src/tests/api/license.ts | 43 +++++++++- 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index 9310eb3739..b8c8566018 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -64,4 +64,5 @@ export const refresh = async (ctx: any) => { export const getQuotaUsage = async (ctx: any) => { ctx.body = await quotas.getQuotaUsage() + ctx.status = 200 } diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts index d548ad83bb..c3c423c833 100644 --- a/packages/worker/src/api/routes/global/tests/license.spec.ts +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -1,4 +1,6 @@ -import { TestConfiguration } from "../../../../tests" +import { TestConfiguration, mocks, structures } from "../../../../tests" +const licensing = mocks.pro.licensing +const quotas = mocks.pro.quotas describe("/api/global/license", () => { const config = new TestConfiguration() @@ -12,15 +14,86 @@ describe("/api/global/license", () => { }) afterEach(() => { - jest.clearAllMocks() + jest.resetAllMocks() }) - describe("POST /api/global/license/activate", () => { - it("activates license", () => {}) + describe("POST /api/global/license/refresh", () => { + it("returns 200", async () => { + const res = await config.api.license.refresh() + expect(res.status).toBe(200) + expect(licensing.cache.refresh).toBeCalledTimes(1) + }) }) - describe("POST /api/global/license/refresh", () => {}) + describe("GET /api/global/license/usage", () => { + it("returns 200", async () => { + const usage = structures.quotas.usage() + quotas.getQuotaUsage.mockResolvedValue(usage) + const res = await config.api.license.getUsage() + expect(res.status).toBe(200) + expect(res.body).toEqual(usage) + }) + }) + describe("POST /api/global/license/key", () => { + it("returns 200", async () => { + const res = await config.api.license.activateLicenseKey({ licenseKey: "licenseKey" }) + expect(res.status).toBe(200) + expect(licensing.keys.activateLicenseKey).toBeCalledWith("licenseKey") + }) + }) - describe("GET /api/global/license/usage", () => {}) + describe("GET /api/global/license/key", () => { + it("returns 404 when not found", async () => { + const res = await config.api.license.getLicenseKey() + expect(res.status).toBe(404) + }) + it("returns 200 + license key", async () => { + licensing.keys.getLicenseKey.mockResolvedValue("licenseKey") + const res = await config.api.license.getLicenseKey() + expect(res.status).toBe(200) + expect(res.body).toEqual({ + licenseKey: "*" + }) + }) + }) + + describe("DELETE /api/global/license/key", () => { + it("returns 204", async () => { + const res = await config.api.license.deleteLicenseKey() + expect(licensing.keys.deleteLicenseKey).toBeCalledTimes(1) + expect(res.status).toBe(204) + }) + }) + + describe("POST /api/global/license/offline", () => { + it("activates offline license", async () => { + const res = await config.api.license.activateOfflineLicense({ offlineLicenseToken: "offlineLicenseToken"}) + expect(licensing.offline.activateOfflineLicense).toBeCalledWith("offlineLicenseToken") + expect(res.status).toBe(200) + }) + }) + + describe("GET /api/global/license/offline", () => { + it("returns 404 when not found", async () => { + const res = await config.api.license.getOfflineLicense() + expect(res.status).toBe(404) + }) + it("returns 200", async () => { + licensing.offline.getOfflineLicenseToken.mockResolvedValue("offlineLicenseToken") + const res = await config.api.license.getOfflineLicense() + expect(res.status).toBe(200) + expect(res.body).toEqual({ + offlineLicenseToken: "*" + }) + }) + }) + + describe("DELETE /api/global/license/offline", () => { + it("deletes offline license", async () => { + const res = await config.api.license.deleteOfflineLicense() + expect(res.status).toBe(204) + expect(licensing.offline.deleteOfflineLicenseToken).toBeCalledTimes(1) + }) + }) }) diff --git a/packages/worker/src/tests/api/license.ts b/packages/worker/src/tests/api/license.ts index 9d7745a80e..89f85b25e3 100644 --- a/packages/worker/src/tests/api/license.ts +++ b/packages/worker/src/tests/api/license.ts @@ -1,17 +1,54 @@ import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" +import { ActivateLicenseKeyRequest, ActivateOfflineLicenseRequest } from "@budibase/types" export class LicenseAPI extends TestAPI { constructor(config: TestConfiguration) { super(config) } - activate = async (licenseKey: string) => { + refresh = async () => { return this.request - .post("/api/global/license/activate") - .send({ licenseKey: licenseKey }) + .post("/api/global/license/refresh") + .set(this.config.defaultHeaders()) + } + getUsage = async () => { + return this.request + .get("/api/global/license/usage") .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) } + activateLicenseKey = async (body: ActivateLicenseKeyRequest) => { + return this.request + .post("/api/global/license/key") + .send(body) + .set(this.config.defaultHeaders()) + } + getLicenseKey = async () => { + return this.request + .get("/api/global/license/key") + .set(this.config.defaultHeaders()) + } + deleteLicenseKey = async () => { + return this.request + .delete("/api/global/license/key") + .set(this.config.defaultHeaders()) + } + activateOfflineLicense = async (body: ActivateOfflineLicenseRequest) => { + return this.request + .post("/api/global/license/offline") + .send(body) + .set(this.config.defaultHeaders()) + } + getOfflineLicense = async () => { + return this.request + .get("/api/global/license/offline") + .set(this.config.defaultHeaders()) + } + deleteOfflineLicense = async () => { + return this.request + .delete("/api/global/license/offline") + .set(this.config.defaultHeaders()) + } } From 9e1e869949d103c75056b77adbca923756f786ed Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 7 Jul 2023 20:07:15 +0100 Subject: [PATCH 015/158] api / license.spec.ts updates --- packages/backend-core/src/events/identification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 5eb11d1354..948d3b692b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -264,7 +264,7 @@ const getEventTenantId = async (tenantId: string): Promise => { } } -const getUniqueTenantId = async (tenantId: string): Promise => { +export const getUniqueTenantId = async (tenantId: string): Promise => { // make sure this tenantId always matches the tenantId in context return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { From 90e869dc045abcf69852693a60a96b370f91e51b Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Sat, 8 Jul 2023 13:07:10 +0100 Subject: [PATCH 016/158] /api/global/license/offline/identifier API --- packages/types/src/api/web/global/license.ts | 6 ++++++ packages/types/src/sdk/licensing/license.ts | 10 ++++++++++ packages/worker/__mocks__/@budibase/pro.ts | 1 + .../src/api/controllers/global/license.ts | 7 +++++++ .../worker/src/api/routes/global/license.ts | 1 + .../src/api/routes/global/tests/license.spec.ts | 17 ++++++++++++++--- packages/worker/src/tests/api/license.ts | 5 +++++ 7 files changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/types/src/api/web/global/license.ts b/packages/types/src/api/web/global/license.ts index 0196b69c7f..4a36c4f1d8 100644 --- a/packages/types/src/api/web/global/license.ts +++ b/packages/types/src/api/web/global/license.ts @@ -17,3 +17,9 @@ export interface ActivateOfflineLicenseRequest { export interface GetOfflineLicenseResponse { offlineLicenseToken: string } + +// IDENTIFIER + +export interface GetOfflineIdentifierResponse { + identifierBase64: string +} diff --git a/packages/types/src/sdk/licensing/license.ts b/packages/types/src/sdk/licensing/license.ts index b287e67adf..ebc874bca0 100644 --- a/packages/types/src/sdk/licensing/license.ts +++ b/packages/types/src/sdk/licensing/license.ts @@ -1,5 +1,15 @@ import { PurchasedPlan, Quotas, Feature, Billing } from "." +export interface OfflineIdentifier { + installId: string, + tenantId: string +} + +// export interface OfflineLicense extends License { +// identifier?: OfflineIdentifier +// identifierBase64: string +// } + export interface License { features: Feature[] quotas: Quotas diff --git a/packages/worker/__mocks__/@budibase/pro.ts b/packages/worker/__mocks__/@budibase/pro.ts index a9611ba705..bd6250fede 100644 --- a/packages/worker/__mocks__/@budibase/pro.ts +++ b/packages/worker/__mocks__/@budibase/pro.ts @@ -11,6 +11,7 @@ const pro = { activateOfflineLicense: jest.fn(), getOfflineLicenseToken: jest.fn(), deleteOfflineLicenseToken: jest.fn(), + getIdentifierBase64: jest.fn() }, cache: { ...actual.licensing.cache, diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index b8c8566018..73b3c72d1e 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -3,6 +3,7 @@ import { ActivateLicenseKeyRequest, ActivateOfflineLicenseRequest, GetLicenseKeyResponse, + GetOfflineIdentifierResponse, GetOfflineLicenseResponse, UserCtx, } from "@budibase/types" @@ -53,6 +54,12 @@ export async function deleteOfflineLicense(ctx: UserCtx) { ctx.status = 204 } +export async function getOfflineLicenseIdentifier(ctx: UserCtx) { + const identifierBase64 = await licensing.offline.getIdentifierBase64() + ctx.body = { identifierBase64 } + ctx.status = 200 +} + // LICENSES export const refresh = async (ctx: any) => { diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index 889d7d1ed4..1d3d9c460b 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -24,5 +24,6 @@ router .post("/api/global/license/offline", activateOfflineLicenseValidator, controller.activateOfflineLicense) .get("/api/global/license/offline", controller.getOfflineLicense) .delete("/api/global/license/offline", controller.deleteOfflineLicense) + .get("/api/global/license/offline/identifier", controller.getOfflineLicenseIdentifier) export default router diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts index c3c423c833..512d6c9c52 100644 --- a/packages/worker/src/api/routes/global/tests/license.spec.ts +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -26,7 +26,7 @@ describe("/api/global/license", () => { }) describe("GET /api/global/license/usage", () => { - it("returns 200", async () => { + it("returns 200 + usage", async () => { const usage = structures.quotas.usage() quotas.getQuotaUsage.mockResolvedValue(usage) const res = await config.api.license.getUsage() @@ -79,7 +79,7 @@ describe("/api/global/license", () => { const res = await config.api.license.getOfflineLicense() expect(res.status).toBe(404) }) - it("returns 200", async () => { + it("returns 200 + offline license token", async () => { licensing.offline.getOfflineLicenseToken.mockResolvedValue("offlineLicenseToken") const res = await config.api.license.getOfflineLicense() expect(res.status).toBe(200) @@ -90,10 +90,21 @@ describe("/api/global/license", () => { }) describe("DELETE /api/global/license/offline", () => { - it("deletes offline license", async () => { + it("returns 204", async () => { const res = await config.api.license.deleteOfflineLicense() expect(res.status).toBe(204) expect(licensing.offline.deleteOfflineLicenseToken).toBeCalledTimes(1) }) }) + + describe("GET /api/global/license/offline/identifier", () => { + it("returns 200 + identifier base64", async () => { + licensing.offline.getIdentifierBase64.mockResolvedValue("base64") + const res = await config.api.license.getOfflineLicenseIdentifier() + expect(res.status).toBe(200) + expect(res.body).toEqual({ + identifierBase64: "base64" + }) + }) + }) }) diff --git a/packages/worker/src/tests/api/license.ts b/packages/worker/src/tests/api/license.ts index 89f85b25e3..76d3e88df7 100644 --- a/packages/worker/src/tests/api/license.ts +++ b/packages/worker/src/tests/api/license.ts @@ -51,4 +51,9 @@ export class LicenseAPI extends TestAPI { .delete("/api/global/license/offline") .set(this.config.defaultHeaders()) } + getOfflineLicenseIdentifier = async () => { + return this.request + .get("/api/global/license/offline/identifier") + .set(this.config.defaultHeaders()) + } } From c69fa2a6408d6529525be77f9f1a5942f76b0ab3 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Sat, 8 Jul 2023 13:08:51 +0100 Subject: [PATCH 018/158] Integrate UI with identifier API --- .../builder/portal/account/upgrade.svelte | 18 +++++++++++++----- packages/frontend-core/src/api/licensing.js | 5 +++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/account/upgrade.svelte b/packages/builder/src/pages/builder/portal/account/upgrade.svelte index 48ff3514b7..8e2fd5c641 100644 --- a/packages/builder/src/pages/builder/portal/account/upgrade.svelte +++ b/packages/builder/src/pages/builder/portal/account/upgrade.svelte @@ -33,7 +33,7 @@ // OFFLINE - let installationIdentifier = "aW5zdGFsbGF0aW9uSWQ9M2MwYmYyZjMtOGJlZi00YTBkLTllN2UtZTU4NmUxMDg2ZjVhLGluc3RhbGxhdGlvblRlbmFudElkPWU5ZWUwNDI0LTE4N2UtNDNhMS1hMDY1LTNiODhmZmE4YzJhZg==\n" + let offlineLicenseIdentifier = "" let offlineLicense = undefined const offlineLicenseExtensions = [ ".txt", @@ -44,8 +44,6 @@ if ($admin.cloud) { $redirect("../../portal") } - - console.log({ offlineLicense }) } // LICENSE KEY @@ -110,6 +108,16 @@ } } + const getOfflineLicenseIdentifier = async () => { + try { + const res = await API.getOfflineLicenseIdentifier() + offlineLicenseIdentifier = res.identifierBase64 + } catch (e) { + console.error(e) + notifications.error("Error loading installation identifier") + } + } + async function activateOfflineLicense(offlineLicenseToken) { try { await API.activateOfflineLicense({ offlineLicenseToken }) @@ -163,7 +171,7 @@ onMount(async () => { if ($admin.offlineMode) { - await getOfflineLicense() + await Promise.all([getOfflineLicense(), getOfflineLicenseIdentifier()]) } else { await getLicenseKey() } @@ -199,7 +207,7 @@
- +
diff --git a/packages/frontend-core/src/api/licensing.js b/packages/frontend-core/src/api/licensing.js index cacf972971..9fe98d5e74 100644 --- a/packages/frontend-core/src/api/licensing.js +++ b/packages/frontend-core/src/api/licensing.js @@ -51,6 +51,11 @@ export const buildLicensingEndpoints = API => ({ } } }, + getOfflineLicenseIdentifier: async () => { + return await API.get({ + url: "/api/global/license/offline/identifier", + }) + }, /** * Refreshes the license cache From 91d3cdff3fe8466e9ad6d2e911971f3006513573 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 10 Jul 2023 12:48:52 +0100 Subject: [PATCH 019/158] offline license sdk module --- packages/backend-core/src/errors/errors.ts | 12 ++++++++++++ packages/types/src/documents/account/account.ts | 1 + packages/types/src/sdk/licensing/feature.ts | 1 + packages/types/src/sdk/licensing/license.ts | 9 +++++---- packages/types/src/shared/typeUtils.ts | 2 ++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index 4e1f1abbb5..7d55d25e89 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -55,6 +55,18 @@ export class HTTPError extends BudibaseError { } } +export class NotFoundError extends HTTPError { + constructor(message: string) { + super(message, 404) + } +} + +export class BadRequestError extends HTTPError { + constructor(message: string) { + super(message, 400) + } +} + // LICENSING export class UsageLimitError extends HTTPError { diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index dad8abed30..5321aa7e08 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -51,6 +51,7 @@ export interface Account extends CreateAccount { licenseRequestedAt?: number licenseOverrides?: LicenseOverrides quotaUsage?: QuotaUsage + offlineLicenseToken?: string } export interface PasswordAccount extends Account { diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 1cbdd55bcf..f286a1cc44 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -9,6 +9,7 @@ export enum Feature { BRANDING = "branding", SCIM = "scim", SYNC_AUTOMATIONS = "syncAutomations", + OFFLINE = "offline", } export type PlanFeatures = { [key in PlanType]: Feature[] | undefined } diff --git a/packages/types/src/sdk/licensing/license.ts b/packages/types/src/sdk/licensing/license.ts index ebc874bca0..e8ad98fe7a 100644 --- a/packages/types/src/sdk/licensing/license.ts +++ b/packages/types/src/sdk/licensing/license.ts @@ -1,14 +1,15 @@ import { PurchasedPlan, Quotas, Feature, Billing } from "." +import { ISO8601 } from "../../shared" export interface OfflineIdentifier { installId: string, tenantId: string } -// export interface OfflineLicense extends License { -// identifier?: OfflineIdentifier -// identifierBase64: string -// } +export interface OfflineLicense extends License { + identifier: OfflineIdentifier + expireAt: ISO8601 +} export interface License { features: Feature[] diff --git a/packages/types/src/shared/typeUtils.ts b/packages/types/src/shared/typeUtils.ts index 71fadfc7aa..143865c60b 100644 --- a/packages/types/src/shared/typeUtils.ts +++ b/packages/types/src/shared/typeUtils.ts @@ -1,3 +1,5 @@ export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] } + +export type ISO8601 = string \ No newline at end of file From b8273540e0ab44f94dc3de4a1ac3f2c4db49d9d6 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 10 Jul 2023 12:49:45 +0100 Subject: [PATCH 020/158] /api/internal/accounts/:accountId/license/offline --- packages/types/src/api/account/license.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/types/src/api/account/license.ts b/packages/types/src/api/account/license.ts index a867358559..e23f6cab97 100644 --- a/packages/types/src/api/account/license.ts +++ b/packages/types/src/api/account/license.ts @@ -1,5 +1,6 @@ import { LicenseOverrides, QuotaUsage } from "../../documents" -import { PlanType } from "../../sdk" +import { OfflineLicense, PlanType } from "../../sdk" +import { ISO8601 } from "../../shared" export interface GetLicenseRequest { // All fields should be optional to cater for @@ -26,3 +27,13 @@ export interface UpdateLicenseRequest { planType?: PlanType overrides?: LicenseOverrides } + +export interface CreateOfflineLicenseRequest { + installationIdentifierBase64: string + expireAt: ISO8601 +} + +export interface GetOfflineLicenseResponse { + offlineLicenseToken: string + license: OfflineLicense +} \ No newline at end of file From 34d9f1c4f8640803bec031aaa112f8e8f17221b6 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 10 Jul 2023 16:12:19 +0100 Subject: [PATCH 021/158] Request / response renames --- packages/types/src/api/web/global/license.ts | 4 ++-- .../worker/src/api/controllers/global/license.ts | 12 ++++++------ packages/worker/src/api/routes/global/license.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/types/src/api/web/global/license.ts b/packages/types/src/api/web/global/license.ts index 4a36c4f1d8..900a0db96d 100644 --- a/packages/types/src/api/web/global/license.ts +++ b/packages/types/src/api/web/global/license.ts @@ -10,11 +10,11 @@ export interface GetLicenseKeyResponse { // OFFLINE LICENSE -export interface ActivateOfflineLicenseRequest { +export interface ActivateOfflineLicenseTokenRequest { offlineLicenseToken: string } -export interface GetOfflineLicenseResponse { +export interface GetOfflineLicenseTokenResponse { offlineLicenseToken: string } diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index 73b3c72d1e..fa95eeee0d 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -1,10 +1,10 @@ import { licensing, quotas } from "@budibase/pro" import { ActivateLicenseKeyRequest, - ActivateOfflineLicenseRequest, + ActivateOfflineLicenseTokenRequest, GetLicenseKeyResponse, GetOfflineIdentifierResponse, - GetOfflineLicenseResponse, + GetOfflineLicenseTokenResponse, UserCtx, } from "@budibase/types" @@ -33,13 +33,13 @@ export async function deleteLicenseKey(ctx: UserCtx) { // OFFLINE LICENSE -export async function activateOfflineLicense(ctx: UserCtx) { +export async function activateOfflineLicenseToken(ctx: UserCtx) { const { offlineLicenseToken } = ctx.request.body - await licensing.offline.activateOfflineLicense(offlineLicenseToken) + await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken) ctx.status = 200 } -export async function getOfflineLicense(ctx: UserCtx) { +export async function getOfflineLicenseToken(ctx: UserCtx) { const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken() if (offlineLicenseToken) { ctx.body = { offlineLicenseToken: "*" } @@ -49,7 +49,7 @@ export async function getOfflineLicense(ctx: UserCtx) { +export async function deleteOfflineLicenseToken(ctx: UserCtx) { await licensing.offline.deleteOfflineLicenseToken() ctx.status = 204 } diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index 1d3d9c460b..199dbb3846 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -21,9 +21,9 @@ router .get("/api/global/license/key", controller.getLicenseKey) .delete("/api/global/license/key", controller.deleteLicenseKey) // OFFLINE LICENSE - .post("/api/global/license/offline", activateOfflineLicenseValidator, controller.activateOfflineLicense) - .get("/api/global/license/offline", controller.getOfflineLicense) - .delete("/api/global/license/offline", controller.deleteOfflineLicense) + .post("/api/global/license/offline", activateOfflineLicenseValidator, controller.activateOfflineLicenseToken) + .get("/api/global/license/offline", controller.getOfflineLicenseToken) + .delete("/api/global/license/offline", controller.deleteOfflineLicenseToken) .get("/api/global/license/offline/identifier", controller.getOfflineLicenseIdentifier) export default router From a74469b8ae3c1c7edb71c01b00a3eda113e808d4 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 13 Jul 2023 21:53:05 +0100 Subject: [PATCH 022/158] offline license structure --- .../core/utilities/structures/licenses.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 22e73f2871..35c9156ec6 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -2,7 +2,7 @@ import { Billing, Customer, Feature, - License, + License, OfflineLicense, PlanModel, PlanType, PriceDuration, @@ -11,6 +11,7 @@ import { Quotas, Subscription, } from "@budibase/types" +import { generator } from "./generator" export function price(): PurchasedPrice { return { @@ -127,14 +128,16 @@ export function subscription(): Subscription { } } +interface GenerateLicenseOpts { + quotas?: Quotas + plan?: PurchasedPlan + planType?: PlanType + features?: Feature[] + billing?: Billing +} + export const license = ( - opts: { - quotas?: Quotas - plan?: PurchasedPlan - planType?: PlanType - features?: Feature[] - billing?: Billing - } = {} + opts: GenerateLicenseOpts = {} ): License => { return { features: opts.features || [], @@ -143,3 +146,17 @@ export const license = ( billing: opts.billing || billing(), } } + +export function offlineLicense ( + opts: GenerateLicenseOpts = {} +): OfflineLicense { + const base = license(opts) + return { + ...base, + expireAt: new Date().toISOString(), + identifier: { + installId: generator.guid(), + tenantId: generator.guid() + } + } +} \ No newline at end of file From 166156d3b5d51568d07dd8783eb9cb58d6915898 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 13 Jul 2023 22:06:52 +0100 Subject: [PATCH 023/158] use automocking in offline.spec.ts --- .../backend-core/tests/core/utilities/structures/accounts.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 807153cd09..42e9d4b63a 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -13,7 +13,7 @@ import { } from "@budibase/types" import _ from "lodash" -export const account = (): Account => { +export const account = (partial: Partial = {}): Account => { return { accountId: uuid(), tenantId: generator.word(), @@ -29,6 +29,7 @@ export const account = (): Account => { size: "10+", profession: "Software Engineer", quotaUsage: quotas.usage(), + ...partial } } From 8cdfc8dd8319a07b767afd16301b0980d486e57d Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 14 Jul 2023 09:21:23 +0100 Subject: [PATCH 024/158] Add structures for Installation type --- .../tests/core/utilities/structures/index.ts | 1 + .../tests/core/utilities/structures/installation.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 packages/backend-core/tests/core/utilities/structures/installation.ts diff --git a/packages/backend-core/tests/core/utilities/structures/index.ts b/packages/backend-core/tests/core/utilities/structures/index.ts index 2c094f43a7..c4404856e1 100644 --- a/packages/backend-core/tests/core/utilities/structures/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/index.ts @@ -2,6 +2,7 @@ export * from "./common" export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" +export * as installation from "./installation" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/core/utilities/structures/installation.ts b/packages/backend-core/tests/core/utilities/structures/installation.ts new file mode 100644 index 0000000000..bd2ae4abfe --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/installation.ts @@ -0,0 +1,10 @@ +import { generator } from "@budibase/backend-core/tests" +import { Installation } from "@budibase/types" + +export function install(): Installation { + return { + _id: "install", + installId: generator.guid(), + version: generator.string() + } +} \ No newline at end of file From 1964148581c446aee45adf5eb75a22ec990f9918 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 14 Jul 2023 11:44:05 +0100 Subject: [PATCH 025/158] core structure updates --- .../tests/core/utilities/structures/db.ts | 6 +++--- .../utilities/structures/documents/index.ts | 1 + .../structures/documents/platform/index.ts | 1 + .../{ => documents/platform}/installation.ts | 2 ++ .../tests/core/utilities/structures/index.ts | 2 +- .../core/utilities/structures/licenses.ts | 19 ++++++++++++++----- 6 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 packages/backend-core/tests/core/utilities/structures/documents/index.ts create mode 100644 packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts rename packages/backend-core/tests/core/utilities/structures/{ => documents/platform}/installation.ts (82%) diff --git a/packages/backend-core/tests/core/utilities/structures/db.ts b/packages/backend-core/tests/core/utilities/structures/db.ts index 31a52dce8b..87325573eb 100644 --- a/packages/backend-core/tests/core/utilities/structures/db.ts +++ b/packages/backend-core/tests/core/utilities/structures/db.ts @@ -1,4 +1,4 @@ -import { structures } from ".." +import { generator } from "./generator" import { newid } from "../../../../src/docIds/newid" export function id() { @@ -6,7 +6,7 @@ export function id() { } export function rev() { - return `${structures.generator.character({ + return `${generator.character({ numeric: true, - })}-${structures.uuid().replace(/-/, "")}` + })}-${generator.guid().replace(/-/, "")}` } diff --git a/packages/backend-core/tests/core/utilities/structures/documents/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/index.ts new file mode 100644 index 0000000000..1c82c5b7d4 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/index.ts @@ -0,0 +1 @@ +export * from "./platform" \ No newline at end of file diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts new file mode 100644 index 0000000000..46b85f0435 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts @@ -0,0 +1 @@ +export * as installation from "./installation" \ No newline at end of file diff --git a/packages/backend-core/tests/core/utilities/structures/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts similarity index 82% rename from packages/backend-core/tests/core/utilities/structures/installation.ts rename to packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts index bd2ae4abfe..30d58fd349 100644 --- a/packages/backend-core/tests/core/utilities/structures/installation.ts +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -1,9 +1,11 @@ import { generator } from "@budibase/backend-core/tests" import { Installation } from "@budibase/types" +import * as db from "../../db" export function install(): Installation { return { _id: "install", + _rev: db.rev(), installId: generator.guid(), version: generator.string() } diff --git a/packages/backend-core/tests/core/utilities/structures/index.ts b/packages/backend-core/tests/core/utilities/structures/index.ts index c4404856e1..1a49e912fc 100644 --- a/packages/backend-core/tests/core/utilities/structures/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/index.ts @@ -2,7 +2,7 @@ export * from "./common" export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" -export * as installation from "./installation" +export * as docs from "./documents" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 35c9156ec6..fae0c7d807 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -2,7 +2,9 @@ import { Billing, Customer, Feature, - License, OfflineLicense, + License, + OfflineIdentifier, + OfflineLicense, PlanModel, PlanType, PriceDuration, @@ -154,9 +156,16 @@ export function offlineLicense ( return { ...base, expireAt: new Date().toISOString(), - identifier: { - installId: generator.guid(), - tenantId: generator.guid() - } + identifier: offlineIdentifier() + } +} + +export function offlineIdentifier( + installId: string = generator.guid(), + tenantId: string = generator.guid(), +): OfflineIdentifier { + return { + installId, + tenantId } } \ No newline at end of file From c8fc67d230dcf4e7c322ba8730665d66f78209b7 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 14 Jul 2023 16:55:48 +0100 Subject: [PATCH 026/158] Lint --- .../core/utilities/structures/accounts.ts | 2 +- .../utilities/structures/documents/index.ts | 2 +- .../structures/documents/platform/index.ts | 2 +- .../documents/platform/installation.ts | 4 +-- .../core/utilities/structures/licenses.ts | 16 ++++------ .../builder/portal/account/upgrade.svelte | 22 ++++++++------ packages/builder/src/stores/portal/admin.js | 2 +- packages/frontend-core/src/api/licensing.js | 3 +- packages/pro | 2 +- packages/types/src/api/account/license.ts | 2 +- packages/types/src/api/web/global/license.ts | 2 +- packages/types/src/sdk/licensing/license.ts | 2 +- packages/types/src/shared/typeUtils.ts | 2 +- packages/worker/__mocks__/@budibase/pro.ts | 6 ++-- .../src/api/controllers/global/license.ts | 16 +++++++--- .../worker/src/api/routes/global/license.ts | 29 ++++++++++++++----- .../api/routes/global/tests/license.spec.ts | 22 +++++++++----- packages/worker/src/tests/api/license.ts | 7 +++-- 18 files changed, 88 insertions(+), 55 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 42e9d4b63a..8476399aa3 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -29,7 +29,7 @@ export const account = (partial: Partial = {}): Account => { size: "10+", profession: "Software Engineer", quotaUsage: quotas.usage(), - ...partial + ...partial, } } diff --git a/packages/backend-core/tests/core/utilities/structures/documents/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/index.ts index 1c82c5b7d4..c3bfba3597 100644 --- a/packages/backend-core/tests/core/utilities/structures/documents/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/documents/index.ts @@ -1 +1 @@ -export * from "./platform" \ No newline at end of file +export * from "./platform" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts index 46b85f0435..98a6314999 100644 --- a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts @@ -1 +1 @@ -export * as installation from "./installation" \ No newline at end of file +export * as installation from "./installation" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts index 30d58fd349..25c5e50e00 100644 --- a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -7,6 +7,6 @@ export function install(): Installation { _id: "install", _rev: db.rev(), installId: generator.guid(), - version: generator.string() + version: generator.string(), } -} \ No newline at end of file +} diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index fae0c7d807..5cce84edfd 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -138,9 +138,7 @@ interface GenerateLicenseOpts { billing?: Billing } -export const license = ( - opts: GenerateLicenseOpts = {} -): License => { +export const license = (opts: GenerateLicenseOpts = {}): License => { return { features: opts.features || [], quotas: opts.quotas || quotas(), @@ -149,23 +147,21 @@ export const license = ( } } -export function offlineLicense ( - opts: GenerateLicenseOpts = {} -): OfflineLicense { +export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense { const base = license(opts) return { ...base, expireAt: new Date().toISOString(), - identifier: offlineIdentifier() + identifier: offlineIdentifier(), } } export function offlineIdentifier( installId: string = generator.guid(), - tenantId: string = generator.guid(), + tenantId: string = generator.guid() ): OfflineIdentifier { return { installId, - tenantId + tenantId, } -} \ No newline at end of file +} diff --git a/packages/builder/src/pages/builder/portal/account/upgrade.svelte b/packages/builder/src/pages/builder/portal/account/upgrade.svelte index 8e2fd5c641..8f76286c8e 100644 --- a/packages/builder/src/pages/builder/portal/account/upgrade.svelte +++ b/packages/builder/src/pages/builder/portal/account/upgrade.svelte @@ -11,7 +11,7 @@ ButtonGroup, notifications, CopyInput, - File + File, } from "@budibase/bbui" import { auth, admin } from "stores/portal" import { redirect } from "@roxi/routify" @@ -35,9 +35,7 @@ let offlineLicenseIdentifier = "" let offlineLicense = undefined - const offlineLicenseExtensions = [ - ".txt", - ] + const offlineLicenseExtensions = [".txt"] // Make sure page can't be visited directly in cloud $: { @@ -97,7 +95,7 @@ const license = await API.getOfflineLicense() if (license) { offlineLicense = { - name: "license" + name: "license", } } else { offlineLicense = undefined @@ -147,7 +145,7 @@ // prevent file preview jitter by assigning constant // as soon as possible offlineLicense = { - name: "license" + name: "license", } const reader = new FileReader() reader.readAsText(event.detail) @@ -202,7 +200,9 @@ {#if $admin.offlineMode} Installation identifier - Share this with support@budibase.com to obtain your offline license + Share this with support@budibase.com to obtain your offline license
@@ -263,8 +263,12 @@ You are currently on the {license.plan.type} plan
- If you purchase or update your plan on the account - portal, click the refresh button to sync those changes + If you purchase or update your plan on the account + portal, click the refresh button to sync those changes
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index 19abcfd841..2106acac27 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -17,7 +17,7 @@ export const DEFAULT_CONFIG = { adminUser: { checked: false }, sso: { checked: false }, }, - offlineMode: false + offlineMode: false, } export function createAdminStore() { diff --git a/packages/frontend-core/src/api/licensing.js b/packages/frontend-core/src/api/licensing.js index 9fe98d5e74..987fc34cf5 100644 --- a/packages/frontend-core/src/api/licensing.js +++ b/packages/frontend-core/src/api/licensing.js @@ -1,5 +1,4 @@ export const buildLicensingEndpoints = API => ({ - // LICENSE KEY activateLicenseKey: async data => { @@ -31,7 +30,7 @@ export const buildLicensingEndpoints = API => ({ return API.post({ url: "/api/global/license/offline", body: { - offlineLicenseToken + offlineLicenseToken, }, }) }, diff --git a/packages/pro b/packages/pro index 1a5207d91f..b5124e76b9 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1a5207d91fb9e0835562c708dd9c421973026543 +Subproject commit b5124e76b9fa8020641e8d019ac1713c6245d6e6 diff --git a/packages/types/src/api/account/license.ts b/packages/types/src/api/account/license.ts index e23f6cab97..edb1267ecf 100644 --- a/packages/types/src/api/account/license.ts +++ b/packages/types/src/api/account/license.ts @@ -36,4 +36,4 @@ export interface CreateOfflineLicenseRequest { export interface GetOfflineLicenseResponse { offlineLicenseToken: string license: OfflineLicense -} \ No newline at end of file +} diff --git a/packages/types/src/api/web/global/license.ts b/packages/types/src/api/web/global/license.ts index 900a0db96d..21d8876412 100644 --- a/packages/types/src/api/web/global/license.ts +++ b/packages/types/src/api/web/global/license.ts @@ -5,7 +5,7 @@ export interface ActivateLicenseKeyRequest { } export interface GetLicenseKeyResponse { - licenseKey: string, + licenseKey: string } // OFFLINE LICENSE diff --git a/packages/types/src/sdk/licensing/license.ts b/packages/types/src/sdk/licensing/license.ts index e8ad98fe7a..105c3680a3 100644 --- a/packages/types/src/sdk/licensing/license.ts +++ b/packages/types/src/sdk/licensing/license.ts @@ -2,7 +2,7 @@ import { PurchasedPlan, Quotas, Feature, Billing } from "." import { ISO8601 } from "../../shared" export interface OfflineIdentifier { - installId: string, + installId: string tenantId: string } diff --git a/packages/types/src/shared/typeUtils.ts b/packages/types/src/shared/typeUtils.ts index 143865c60b..fbe215fdb9 100644 --- a/packages/types/src/shared/typeUtils.ts +++ b/packages/types/src/shared/typeUtils.ts @@ -2,4 +2,4 @@ export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] } -export type ISO8601 = string \ No newline at end of file +export type ISO8601 = string diff --git a/packages/worker/__mocks__/@budibase/pro.ts b/packages/worker/__mocks__/@budibase/pro.ts index bd6250fede..f1e79bd7c7 100644 --- a/packages/worker/__mocks__/@budibase/pro.ts +++ b/packages/worker/__mocks__/@budibase/pro.ts @@ -11,16 +11,16 @@ const pro = { activateOfflineLicense: jest.fn(), getOfflineLicenseToken: jest.fn(), deleteOfflineLicenseToken: jest.fn(), - getIdentifierBase64: jest.fn() + getIdentifierBase64: jest.fn(), }, cache: { ...actual.licensing.cache, refresh: jest.fn(), - } + }, }, quotas: { ...actual.quotas, - getQuotaUsage: jest.fn() + getQuotaUsage: jest.fn(), }, } diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index fa95eeee0d..111cb5cea3 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -10,7 +10,9 @@ import { // LICENSE KEY -export async function activateLicenseKey(ctx: UserCtx) { +export async function activateLicenseKey( + ctx: UserCtx +) { const { licenseKey } = ctx.request.body await licensing.keys.activateLicenseKey(licenseKey) ctx.status = 200 @@ -33,13 +35,17 @@ export async function deleteLicenseKey(ctx: UserCtx) { // OFFLINE LICENSE -export async function activateOfflineLicenseToken(ctx: UserCtx) { +export async function activateOfflineLicenseToken( + ctx: UserCtx +) { const { offlineLicenseToken } = ctx.request.body await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken) ctx.status = 200 } -export async function getOfflineLicenseToken(ctx: UserCtx) { +export async function getOfflineLicenseToken( + ctx: UserCtx +) { const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken() if (offlineLicenseToken) { ctx.body = { offlineLicenseToken: "*" } @@ -54,7 +60,9 @@ export async function deleteOfflineLicenseToken(ctx: UserCtx) { ctx.status = 204 } -export async function getOfflineLicenseIdentifier(ctx: UserCtx) { +export async function getOfflineLicenseIdentifier( + ctx: UserCtx +) { const identifierBase64 = await licensing.offline.getIdentifierBase64() ctx.body = { identifierBase64 } ctx.status = 200 diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index 199dbb3846..b0e474e4d5 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -3,13 +3,17 @@ import * as controller from "../../controllers/global/license" import { middleware } from "@budibase/backend-core" import Joi from "joi" -const activateLicenseKeyValidator = middleware.joiValidator.body(Joi.object({ +const activateLicenseKeyValidator = middleware.joiValidator.body( + Joi.object({ licenseKey: Joi.string().required(), - }).required()) + }).required() +) -const activateOfflineLicenseValidator = middleware.joiValidator.body(Joi.object({ +const activateOfflineLicenseValidator = middleware.joiValidator.body( + Joi.object({ offlineLicenseToken: Joi.string().required(), -}).required()) + }).required() +) const router: Router = new Router() @@ -17,13 +21,24 @@ router .post("/api/global/license/refresh", controller.refresh) .get("/api/global/license/usage", controller.getQuotaUsage) // LICENSE KEY - .post("/api/global/license/key", activateLicenseKeyValidator, controller.activateLicenseKey) + .post( + "/api/global/license/key", + activateLicenseKeyValidator, + controller.activateLicenseKey + ) .get("/api/global/license/key", controller.getLicenseKey) .delete("/api/global/license/key", controller.deleteLicenseKey) // OFFLINE LICENSE - .post("/api/global/license/offline", activateOfflineLicenseValidator, controller.activateOfflineLicenseToken) + .post( + "/api/global/license/offline", + activateOfflineLicenseValidator, + controller.activateOfflineLicenseToken + ) .get("/api/global/license/offline", controller.getOfflineLicenseToken) .delete("/api/global/license/offline", controller.deleteOfflineLicenseToken) - .get("/api/global/license/offline/identifier", controller.getOfflineLicenseIdentifier) + .get( + "/api/global/license/offline/identifier", + controller.getOfflineLicenseIdentifier + ) export default router diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts index 512d6c9c52..26e3b3dfb5 100644 --- a/packages/worker/src/api/routes/global/tests/license.spec.ts +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -37,7 +37,9 @@ describe("/api/global/license", () => { describe("POST /api/global/license/key", () => { it("returns 200", async () => { - const res = await config.api.license.activateLicenseKey({ licenseKey: "licenseKey" }) + const res = await config.api.license.activateLicenseKey({ + licenseKey: "licenseKey", + }) expect(res.status).toBe(200) expect(licensing.keys.activateLicenseKey).toBeCalledWith("licenseKey") }) @@ -53,7 +55,7 @@ describe("/api/global/license", () => { const res = await config.api.license.getLicenseKey() expect(res.status).toBe(200) expect(res.body).toEqual({ - licenseKey: "*" + licenseKey: "*", }) }) }) @@ -68,8 +70,12 @@ describe("/api/global/license", () => { describe("POST /api/global/license/offline", () => { it("activates offline license", async () => { - const res = await config.api.license.activateOfflineLicense({ offlineLicenseToken: "offlineLicenseToken"}) - expect(licensing.offline.activateOfflineLicense).toBeCalledWith("offlineLicenseToken") + const res = await config.api.license.activateOfflineLicense({ + offlineLicenseToken: "offlineLicenseToken", + }) + expect(licensing.offline.activateOfflineLicenseToken).toBeCalledWith( + "offlineLicenseToken" + ) expect(res.status).toBe(200) }) }) @@ -80,11 +86,13 @@ describe("/api/global/license", () => { expect(res.status).toBe(404) }) it("returns 200 + offline license token", async () => { - licensing.offline.getOfflineLicenseToken.mockResolvedValue("offlineLicenseToken") + licensing.offline.getOfflineLicenseToken.mockResolvedValue( + "offlineLicenseToken" + ) const res = await config.api.license.getOfflineLicense() expect(res.status).toBe(200) expect(res.body).toEqual({ - offlineLicenseToken: "*" + offlineLicenseToken: "*", }) }) }) @@ -103,7 +111,7 @@ describe("/api/global/license", () => { const res = await config.api.license.getOfflineLicenseIdentifier() expect(res.status).toBe(200) expect(res.body).toEqual({ - identifierBase64: "base64" + identifierBase64: "base64", }) }) }) diff --git a/packages/worker/src/tests/api/license.ts b/packages/worker/src/tests/api/license.ts index 76d3e88df7..a6645226af 100644 --- a/packages/worker/src/tests/api/license.ts +++ b/packages/worker/src/tests/api/license.ts @@ -1,6 +1,9 @@ import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" -import { ActivateLicenseKeyRequest, ActivateOfflineLicenseRequest } from "@budibase/types" +import { + ActivateLicenseKeyRequest, + ActivateOfflineLicenseTokenRequest, +} from "@budibase/types" export class LicenseAPI extends TestAPI { constructor(config: TestConfiguration) { @@ -35,7 +38,7 @@ export class LicenseAPI extends TestAPI { .delete("/api/global/license/key") .set(this.config.defaultHeaders()) } - activateOfflineLicense = async (body: ActivateOfflineLicenseRequest) => { + activateOfflineLicense = async (body: ActivateOfflineLicenseTokenRequest) => { return this.request .post("/api/global/license/offline") .send(body) From 55efb6e618b827cd8fa46fd800fa368d168e66f6 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 14 Jul 2023 20:56:50 +0100 Subject: [PATCH 027/158] fix pro mock --- packages/worker/__mocks__/@budibase/pro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/__mocks__/@budibase/pro.ts b/packages/worker/__mocks__/@budibase/pro.ts index f1e79bd7c7..59c7939111 100644 --- a/packages/worker/__mocks__/@budibase/pro.ts +++ b/packages/worker/__mocks__/@budibase/pro.ts @@ -8,7 +8,7 @@ const pro = { deleteLicenseKey: jest.fn(), }, offline: { - activateOfflineLicense: jest.fn(), + activateOfflineLicenseToken: jest.fn(), getOfflineLicenseToken: jest.fn(), deleteOfflineLicenseToken: jest.fn(), getIdentifierBase64: jest.fn(), From 797f281f6d6ce6d1a4303d089d9783b0010785ef Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 17 Jul 2023 20:55:26 +0100 Subject: [PATCH 028/158] OpenAPI 3.0 docs for offline license and license key endpoints --- packages/worker/openapi-3.0.yaml | 133 +++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 packages/worker/openapi-3.0.yaml diff --git a/packages/worker/openapi-3.0.yaml b/packages/worker/openapi-3.0.yaml new file mode 100644 index 0000000000..c68654fffb --- /dev/null +++ b/packages/worker/openapi-3.0.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.0 +info: + title: Worker API Specification + version: 1.0.0 +servers: + - url: "http://localhost:10000" + description: localhost + - url: "https://budibaseqa.app" + description: QA + - url: "https://preprod.qa.budibase.net" + description: Preprod + - url: "https://budibase.app" + description: Production + +tags: + - name: license + description: License operations + +paths: + /api/global/license/key: + post: + tags: + - license + summary: Activate license key + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ActivateLicenseKeyRequest' + responses: + '200': + description: Success + get: + tags: + - license + summary: Get license key + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/GetLicenseKeyResponse' + delete: + tags: + - license + summary: Delete license key + responses: + '204': + description: No content + /api/global/license/offline: + post: + tags: + - license + summary: Activate offline license + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ActivateOfflineLicenseTokenRequest' + responses: + '200': + description: Success + get: + tags: + - license + summary: Get offline license + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/GetOfflineLicenseTokenResponse' + delete: + tags: + - license + summary: Delete offline license + responses: + '204': + description: No content + /api/global/license/offline/identifier: + get: + tags: + - license + summary: Get offline identifier + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/GetOfflineIdentifierResponse' + +components: + schemas: + ActivateOfflineLicenseTokenRequest: + type: object + properties: + offlineLicenseToken: + type: string + required: + - offlineLicenseToken + GetOfflineLicenseTokenResponse: + type: object + properties: + offlineLicenseToken: + type: string + required: + - offlineLicenseToken + ActivateLicenseKeyRequest: + type: object + properties: + licenseKey: + type: string + required: + - licenseKey + GetLicenseKeyResponse: + type: object + properties: + licenseKey: + type: string + required: + - licenseKey + GetOfflineIdentifierResponse: + type: object + properties: + identifierBase64: + type: string + required: + - identifierBase64 \ No newline at end of file From a6073e0ecd834cb17b802486ee7b7b4e926c6a12 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 20 Jul 2023 14:56:01 +0100 Subject: [PATCH 030/158] Build fixes --- .../utilities/structures/documents/platform/installation.ts | 2 +- packages/pro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts index 25c5e50e00..711c6cf14f 100644 --- a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -1,4 +1,4 @@ -import { generator } from "@budibase/backend-core/tests" +import { generator } from "../../generator" import { Installation } from "@budibase/types" import * as db from "../../db" diff --git a/packages/pro b/packages/pro index b5124e76b9..5775dda6b9 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit b5124e76b9fa8020641e8d019ac1713c6245d6e6 +Subproject commit 5775dda6b9fe8badd5253a20ef4c78b0bd687bfa From c3412f0cfbcb2795083926db6f92100c3daf7b4e Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 20 Jul 2023 15:06:43 +0100 Subject: [PATCH 031/158] Update openapi.json --- packages/server/specs/openapi.json | 9 ++++++--- packages/server/specs/openapi.yaml | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index bcff78e861..d97b09568c 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -841,7 +841,8 @@ "auto", "json", "internal", - "barcodeqr" + "barcodeqr", + "bigint" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1045,7 +1046,8 @@ "auto", "json", "internal", - "barcodeqr" + "barcodeqr", + "bigint" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1260,7 +1262,8 @@ "auto", "json", "internal", - "barcodeqr" + "barcodeqr", + "bigint" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index a9daed6c76..86807c9981 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -768,6 +768,7 @@ components: - json - internal - barcodeqr + - bigint description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -931,6 +932,7 @@ components: - json - internal - barcodeqr + - bigint description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -1101,6 +1103,7 @@ components: - json - internal - barcodeqr + - bigint description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: From 4e88779b5ed5d5e3fa91171e8d9c576080e10707 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 20 Jul 2023 15:31:19 +0100 Subject: [PATCH 032/158] Remove 'nightly' from test discord notification --- qa-core/scripts/testResultsWebhook.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa-core/scripts/testResultsWebhook.js b/qa-core/scripts/testResultsWebhook.js index 40cc42082d..5fbdd3a32e 100644 --- a/qa-core/scripts/testResultsWebhook.js +++ b/qa-core/scripts/testResultsWebhook.js @@ -42,7 +42,7 @@ async function discordResultsNotification(report) { Accept: "application/json", }, body: JSON.stringify({ - content: `**Nightly Tests Status**: ${OUTCOME}`, + content: `**Tests Status**: ${OUTCOME}`, embeds: [ { title: `Budi QA Bot - ${env}`, From 3fb0380eec3d06c2136789204f51a3b3476518b1 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 20 Jul 2023 16:50:57 +0100 Subject: [PATCH 033/158] Update pro ref --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 5775dda6b9..1b8fd8ed44 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 5775dda6b9fe8badd5253a20ef4c78b0bd687bfa +Subproject commit 1b8fd8ed445c4c25210f8faf07ff404c41fbc805 From 552f96fb81c78c6f3be9882bf4b5fcdb783753d2 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 20 Jul 2023 21:37:24 +0100 Subject: [PATCH 034/158] Update pro ref --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 1b8fd8ed44..88b3c19262 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1b8fd8ed445c4c25210f8faf07ff404c41fbc805 +Subproject commit 88b3c192624d19f37d5743afc36b92f1c1ebb1db From 8f0043157d885a2f6861c7c767b7b55a1853825a Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 21 Jul 2023 08:43:53 +0100 Subject: [PATCH 035/158] Styling fix for license key input --- .../src/pages/builder/portal/account/upgrade.svelte | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/account/upgrade.svelte b/packages/builder/src/pages/builder/portal/account/upgrade.svelte index 8f76286c8e..5c1e8bee0e 100644 --- a/packages/builder/src/pages/builder/portal/account/upgrade.svelte +++ b/packages/builder/src/pages/builder/portal/account/upgrade.svelte @@ -205,10 +205,8 @@ >
-
-
+
-
@@ -291,8 +289,11 @@ } .field { display: grid; - grid-template-columns: 300px 1fr; + grid-template-columns: 100px 1fr; grid-gap: var(--spacing-l); align-items: center; } + .identifier-input { + width: 300px; + } From 3b36970c588fac417dbc79f2982a86c4aeba13a2 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 24 Jul 2023 09:08:10 +0100 Subject: [PATCH 036/158] Review updates --- .../server/src/api/controllers/row/index.ts | 27 ++++++------- .../server/src/api/routes/tests/row.spec.ts | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index a1f5664754..7183aa6cd7 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -2,7 +2,7 @@ import { quotas } from "@budibase/pro" import * as internal from "./internal" import * as external from "./external" import { isExternalTable } from "../../../integrations/utils" -import { Ctx } from "@budibase/types" +import { Ctx, UserCtx, DeleteRowRequest, Row } from "@budibase/types" import * as utils from "./utils" import { gridSocket } from "../../../websockets" import { addRev } from "../public/utils" @@ -100,25 +100,24 @@ export async function find(ctx: any) { }) } -export async function destroy(ctx: any) { +export async function destroy(ctx: UserCtx) { const appId = ctx.appId - const inputs = ctx.request.body + const inputs: DeleteRowRequest = ctx.request.body + const tableId = utils.getTableId(ctx) let response, row - if (inputs.rows) { - const targetRows = inputs.rows.map( - (row: { [key: string]: string | string }) => { - let processedRow = typeof row == "string" ? { _id: row } : row - return !processedRow._rev - ? addRev(fixRow(processedRow, ctx.params), tableId) - : fixRow(processedRow, ctx.params) - } - ) + if ("rows" in inputs && Array.isArray(inputs?.rows)) { + const targetRows = inputs.rows.map(row => { + let processedRow: Row = typeof row == "string" ? { _id: row } : row + return !processedRow._rev + ? addRev(fixRow(processedRow, ctx.params), tableId) + : fixRow(processedRow, ctx.params) + }) - const rowDeletes = await Promise.all(targetRows) + const rowDeletes: Row[] = await Promise.all(targetRows) if (rowDeletes) { - ctx.request.body.rows = rowDeletes + inputs.rows = rowDeletes } let { rows } = await quotas.addQuery( diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 8e99c30246..aef5ac57ac 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -517,6 +517,46 @@ describe("/rows", () => { await assertRowUsage(rowUsage - 2) await assertQueryUsage(queryUsage + 1) }) + + it("should be able to delete a variety of row set types", async () => { + const row1 = await config.createRow() + const row2 = await config.createRow() + const row3 = await config.createRow() + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await request + .delete(`/api/${table._id}/rows`) + .send({ + rows: [row1, row2._id, { _id: row3._id }], + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + expect(res.body.length).toEqual(3) + await loadRow(row1._id!, table._id!, 404) + await assertRowUsage(rowUsage - 3) + await assertQueryUsage(queryUsage + 1) + }) + + it("should accept a valid row object and delete the row", async () => { + const row1 = await config.createRow() + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await request + .delete(`/api/${table._id}/rows`) + .send(row1) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + expect(res.body.id).toEqual(row1._id) + await loadRow(row1._id!, table._id!, 404) + await assertRowUsage(rowUsage - 1) + await assertQueryUsage(queryUsage + 1) + }) }) describe("fetchView", () => { From 5691be3c4a2280b81aca664c8fdf6e5058a53496 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 24 Jul 2023 09:15:13 +0100 Subject: [PATCH 037/158] Added missing types --- packages/types/src/documents/app/row.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index a2295c4a42..3a302589cc 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -32,3 +32,13 @@ export interface Row extends Document { tableId?: string [key: string]: any } + +export interface DeleteRows { + rows: (Row | string)[] +} + +export interface DeleteRow { + _id: string +} + +export type DeleteRowRequest = DeleteRows | DeleteRow From 4091dff6d3832f9889360972df1278fa9f13be4f Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 24 Jul 2023 10:30:29 +0100 Subject: [PATCH 038/158] PR Feedback --- packages/server/src/api/controllers/row/index.ts | 2 +- packages/types/src/api/web/app/index.ts | 1 + packages/types/src/api/web/app/row.ts | 11 +++++++++++ packages/types/src/documents/app/row.ts | 10 ---------- 4 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 packages/types/src/api/web/app/row.ts diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 7183aa6cd7..c217f06ad4 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -102,7 +102,7 @@ export async function find(ctx: any) { export async function destroy(ctx: UserCtx) { const appId = ctx.appId - const inputs: DeleteRowRequest = ctx.request.body + const inputs = ctx.request.body const tableId = utils.getTableId(ctx) let response, row diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts index 9be15ecfe3..0b878f38de 100644 --- a/packages/types/src/api/web/app/index.ts +++ b/packages/types/src/api/web/app/index.ts @@ -1,2 +1,3 @@ export * from "./backup" export * from "./datasource" +export * from "./row" diff --git a/packages/types/src/api/web/app/row.ts b/packages/types/src/api/web/app/row.ts new file mode 100644 index 0000000000..f9623a3daf --- /dev/null +++ b/packages/types/src/api/web/app/row.ts @@ -0,0 +1,11 @@ +import { Row } from "../../../documents/app/row" + +export interface DeleteRows { + rows: (Row | string)[] +} + +export interface DeleteRow { + _id: string +} + +export type DeleteRowRequest = DeleteRows | DeleteRow diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index 3a302589cc..a2295c4a42 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -32,13 +32,3 @@ export interface Row extends Document { tableId?: string [key: string]: any } - -export interface DeleteRows { - rows: (Row | string)[] -} - -export interface DeleteRow { - _id: string -} - -export type DeleteRowRequest = DeleteRows | DeleteRow From 869cb0777b3409c2e7fd695704caa870cb3abfd7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 24 Jul 2023 12:02:24 +0100 Subject: [PATCH 039/158] Validate query names to avoid parentheses --- .../components/integration/QueryViewer.svelte | 27 ++++++++++++------- .../server/src/api/controllers/query/index.ts | 6 +++++ packages/shared-core/src/constants.ts | 1 + 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte index 3377ff3a88..4683bc6335 100644 --- a/packages/builder/src/components/integration/QueryViewer.svelte +++ b/packages/builder/src/components/integration/QueryViewer.svelte @@ -14,8 +14,9 @@ Tab, Modal, ModalContent, + notifications, + Divider, } from "@budibase/bbui" - import { notifications, Divider } from "@budibase/bbui" import ExtraQueryConfig from "./ExtraQueryConfig.svelte" import IntegrationQueryEditor from "components/integration/index.svelte" import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" @@ -28,6 +29,7 @@ import KeyValueBuilder from "./KeyValueBuilder.svelte" import { fieldsToSchema, schemaToFields } from "helpers/data/utils" import AccessLevelSelect from "./AccessLevelSelect.svelte" + import { ValidQueryNameRegex } from "@budibase/shared-core" export let query @@ -47,6 +49,7 @@ let saveModal let override = false let navigateTo = null + let nameError = null // seed the transformer if (query && !query.transformer) { @@ -77,7 +80,7 @@ $: queryConfig = integrationInfo?.query $: shouldShowQueryConfig = queryConfig && query.queryVerb $: readQuery = query.queryVerb === "read" || query.readable - $: queryInvalid = !query.name || (readQuery && data.length === 0) + $: queryInvalid = !query.name || nameError || (readQuery && data.length === 0) //Cast field in query preview response to number if specified by schema $: { @@ -139,9 +142,10 @@ queryStr = JSON.stringify(query) } + notifications.success("Query saved successfully") return response } catch (error) { - notifications.error("Error saving query") + notifications.error(error.message || "Error saving query") } } @@ -183,8 +187,14 @@ value={query.name} on:input={e => { let newValue = e.target.value || "" - query.name = newValue.trim() + if (newValue.match(ValidQueryNameRegex)) { + query.name = newValue.trim() + nameError = null + } else { + nameError = "Invalid query name" + } }} + error={nameError} />
{#if queryConfig} @@ -250,9 +260,9 @@ size="L" />
- Add a JavaScript function to transform the query result. + + Add a JavaScript function to transform the query result. +
Results - +