diff --git a/packages/backend-core/src/utils/tests/utils.spec.ts b/packages/backend-core/src/utils/tests/utils.spec.ts index bb76a93653..498aff1555 100644 --- a/packages/backend-core/src/utils/tests/utils.spec.ts +++ b/packages/backend-core/src/utils/tests/utils.spec.ts @@ -1,7 +1,8 @@ import { structures } from "../../../tests" import * as utils from "../../utils" import * as events from "../../events" -import { DEFAULT_TENANT_ID } from "../../constants" +import * as db from "../../db" +import { DEFAULT_TENANT_ID, Header } from "../../constants" import { doInTenant } from "../../context" describe("utils", () => { @@ -14,4 +15,95 @@ describe("utils", () => { }) }) }) + + describe("getAppIdFromCtx", () => { + it("gets appId from header", async () => { + const ctx = structures.koa.newContext() + const expected = db.generateAppID() + ctx.request.headers = { + [Header.APP_ID]: expected, + } + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) + + it("gets appId from body", async () => { + const ctx = structures.koa.newContext() + const expected = db.generateAppID() + ctx.request.body = { + appId: expected, + } + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) + + it("gets appId from path", async () => { + const ctx = structures.koa.newContext() + const expected = db.generateAppID() + ctx.path = `/apps/${expected}` + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) + + it("gets appId from url", async () => { + const ctx = structures.koa.newContext() + const expected = db.generateAppID() + const app = structures.apps.app(expected) + + // set custom url + const appUrl = "custom-url" + app.url = `/${appUrl}` + ctx.path = `/app/${appUrl}` + + // save the app + const database = db.getDB(expected) + await database.put(app) + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) + + it("doesn't get appId from url when previewing", async () => { + const ctx = structures.koa.newContext() + const appId = db.generateAppID() + const app = structures.apps.app(appId) + + // set custom url + const appUrl = "preview" + app.url = `/${appUrl}` + ctx.path = `/app/${appUrl}` + + // save the app + const database = db.getDB(appId) + await database.put(app) + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(undefined) + }) + + it("gets appId from referer", async () => { + const ctx = structures.koa.newContext() + const expected = db.generateAppID() + ctx.request.headers = { + referer: `http://test.com/builder/app/${expected}/design/screen_123/screens`, + } + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) + + it("doesn't get appId from referer when not builder", async () => { + const ctx = structures.koa.newContext() + const appId = db.generateAppID() + ctx.request.headers = { + referer: `http://test.com/foo/app/${appId}/bar`, + } + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(undefined) + }) + }) }) diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index fd8d31b13f..c608686431 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -25,13 +25,16 @@ const jwt = require("jsonwebtoken") const APP_PREFIX = DocumentType.APP + SEPARATOR const PROD_APP_PREFIX = "/app/" +const BUILDER_PREVIEW_PATH = "/app/preview" +const BUILDER_REFERER_PREFIX = "/builder/app/" + function confirmAppId(possibleAppId: string | undefined) { return possibleAppId && possibleAppId.startsWith(APP_PREFIX) ? possibleAppId : undefined } -async function resolveAppUrl(ctx: Ctx) { +export async function resolveAppUrl(ctx: Ctx) { const appUrl = ctx.path.split("/")[2] let possibleAppUrl = `/${appUrl.toLowerCase()}` @@ -75,7 +78,7 @@ export function isServingApp(ctx: Ctx) { */ export async function getAppIdFromCtx(ctx: Ctx) { // look in headers - const options = [ctx.headers[Header.APP_ID]] + const options = [ctx.request.headers[Header.APP_ID]] let appId for (let option of options) { appId = confirmAppId(option as string) @@ -95,15 +98,23 @@ export async function getAppIdFromCtx(ctx: Ctx) { appId = confirmAppId(pathId) } - // look in the referer - const refererId = parseAppIdFromUrl(ctx.request.headers.referer) - if (!appId && refererId) { - appId = confirmAppId(refererId) + // lookup using custom url - prod apps only + // filter out the builder preview path which collides with the prod app path + // to ensure we don't load all apps excessively + const isBuilderPreview = ctx.path.startsWith(BUILDER_PREVIEW_PATH) + const isViewingProdApp = + ctx.path.startsWith(PROD_APP_PREFIX) && !isBuilderPreview + if (!appId && isViewingProdApp) { + appId = confirmAppId(await resolveAppUrl(ctx)) } - // look in the url - prod app - if (!appId && ctx.path.startsWith(PROD_APP_PREFIX)) { - appId = confirmAppId(await resolveAppUrl(ctx)) + // look in the referer - builder only + // make sure this is performed after prod app url resolution, in case the + // referer header is present from a builder redirect + const referer = ctx.request.headers.referer + if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) { + const refererId = parseAppIdFromUrl(ctx.request.headers.referer) + appId = confirmAppId(refererId) } return appId diff --git a/packages/backend-core/tests/utilities/structures/apps.ts b/packages/backend-core/tests/utilities/structures/apps.ts new file mode 100644 index 0000000000..f3743d99b2 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/apps.ts @@ -0,0 +1,21 @@ +import { generator } from "." +import { App } from "@budibase/types" +import { DEFAULT_TENANT_ID, DocumentType } from "../../../src/constants" + +export function app(id: string): App { + return { + _id: DocumentType.APP_METADATA, + appId: id, + type: "", + version: "0.0.1", + componentLibraries: [], + name: generator.name(), + url: `/custom-url`, + instance: { + _id: id, + }, + tenantId: DEFAULT_TENANT_ID, + status: "", + template: undefined, + } +} diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index e0ed4df9c4..0d0f0c507f 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -3,7 +3,8 @@ export * from "./common" import Chance from "chance" export const generator = new Chance() -export * as koa from "./koa" export * as accounts from "./accounts" +export * as apps from "./apps" +export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/utilities/structures/koa.ts b/packages/backend-core/tests/utilities/structures/koa.ts index a33dca1546..102fe029de 100644 --- a/packages/backend-core/tests/utilities/structures/koa.ts +++ b/packages/backend-core/tests/utilities/structures/koa.ts @@ -5,9 +5,11 @@ export const newContext = (): BBContext => { const ctx = createMockContext() return { ...ctx, + path: "/", cookies: createMockCookies(), request: { ...ctx.request, + headers: {}, body: {}, }, } diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index b7cd21a76d..76d18eba7d 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -3645,9 +3645,9 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== jsonc-parser@^3.2.0: version "3.2.0"