From fbbfb0cb6233eaec60aefc0db7da7ae7b3a0c60e Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 2 Oct 2024 18:01:50 +0200 Subject: [PATCH] feat(framework,medusa): Ensure publishable key middleware is set for all store endpoints (#9429) * feat(framework,medusa): Ensure publishable key middleware is set for all store endpoints * chore: fix tests --- _tsconfig.base.json | 3 +- .../__tests__/customer/store/customer.spec.ts | 2 +- .../src/http/__tests__/index.spec.ts | 12 ++- .../middlewares/ensure-publishable-api-key.ts | 77 +++++++++++++++++++ packages/core/framework/src/http/router.ts | 19 +++++ packages/medusa/src/api/store/middlewares.ts | 9 +-- .../middlewares/ensure-publishable-api-key.ts | 68 ---------------- 7 files changed, 111 insertions(+), 79 deletions(-) create mode 100644 packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts delete mode 100644 packages/medusa/src/utils/middlewares/ensure-publishable-api-key.ts diff --git a/_tsconfig.base.json b/_tsconfig.base.json index 8c8f0acaf5..f1992bd31c 100644 --- a/_tsconfig.base.json +++ b/_tsconfig.base.json @@ -19,7 +19,8 @@ "strictFunctionTypes": true, "noImplicitThis": true, "allowJs": true, - "skipLibCheck": true + "skipLibCheck": true, + "incremental": false }, "include": ["${configDir}/src"], "exclude": ["${configDir}/dist", "${configDir}/node_modules"] diff --git a/integration-tests/http/__tests__/customer/store/customer.spec.ts b/integration-tests/http/__tests__/customer/store/customer.spec.ts index b464f47cf2..976b526c9e 100644 --- a/integration-tests/http/__tests__/customer/store/customer.spec.ts +++ b/integration-tests/http/__tests__/customer/store/customer.spec.ts @@ -7,7 +7,7 @@ import { generateStoreHeaders, } from "../../../../helpers/create-admin-user" -jest.setTimeout(30000) +jest.setTimeout(50000) medusaIntegrationTestRunner({ testSuite: ({ dbConnection, api, getContainer }) => { diff --git a/packages/core/framework/src/http/__tests__/index.spec.ts b/packages/core/framework/src/http/__tests__/index.spec.ts index 9c0ae60aae..d81068a5e6 100644 --- a/packages/core/framework/src/http/__tests__/index.spec.ts +++ b/packages/core/framework/src/http/__tests__/index.spec.ts @@ -6,10 +6,20 @@ import { storeGlobalMiddlewareMock, } from "../__fixtures__/mocks" import { createServer } from "../__fixtures__/server" -import { RoutesLoader } from "../index" +import { MedusaNextFunction, RoutesLoader } from "../index" jest.setTimeout(30000) +jest.mock("../middlewares/ensure-publishable-api-key", () => { + return { + ensurePublishableApiKeyMiddleware: async ( + req: any, + res: any, + next: MedusaNextFunction + ) => next(), + } +}) + describe("RoutesLoader", function () { afterEach(function () { jest.clearAllMocks() diff --git a/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts b/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts new file mode 100644 index 0000000000..34dd18116b --- /dev/null +++ b/packages/core/framework/src/http/middlewares/ensure-publishable-api-key.ts @@ -0,0 +1,77 @@ +import { Query } from "@medusajs/types" +import { + ApiKeyType, + ContainerRegistrationKeys, + isPresent, + MedusaError, + PUBLISHABLE_KEY_HEADER, +} from "@medusajs/utils" +import { + MedusaNextFunction, + MedusaResponse, + MedusaStoreRequest, +} from "../../http" + +export async function ensurePublishableApiKeyMiddleware( + req: MedusaStoreRequest, + _res: MedusaResponse, + next: MedusaNextFunction +) { + const publishableApiKey = req.get(PUBLISHABLE_KEY_HEADER) + + if (!isPresent(publishableApiKey)) { + try { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Publishable API key required in the request header: ${PUBLISHABLE_KEY_HEADER}. You can manage your keys in settings in the dashboard.` + ) + } catch (e) { + return next(e) + } + } + + let apiKey + const query: Query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + try { + const { data } = await query.graph( + { + entity: "api_key", + fields: ["id", "token", "sales_channels_link.sales_channel_id"], + filters: { + token: publishableApiKey, + type: ApiKeyType.PUBLISHABLE, + $or: [ + { revoked_at: { $eq: null } }, + { revoked_at: { $gt: new Date() } }, + ], + }, + }, + { throwIfKeyNotFound: true } + ) + + apiKey = data[0] + } catch (e) { + return next(e) + } + + if (!apiKey) { + try { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `A valid publishable key is required to proceed with the request` + ) + } catch (e) { + return next(e) + } + } + + req.publishable_key_context = { + key: apiKey.token, + sales_channel_ids: apiKey.sales_channels_link.map( + (link) => link.sales_channel_id + ), + } + + return next() +} diff --git a/packages/core/framework/src/http/router.ts b/packages/core/framework/src/http/router.ts index f0fb7f1bf8..7328241df5 100644 --- a/packages/core/framework/src/http/router.ts +++ b/packages/core/framework/src/http/router.ts @@ -19,6 +19,7 @@ import { extname, join, parse, sep } from "path" import { configManager } from "../config" import { logger } from "../logger" import { authenticate, AuthType, errorHandler } from "./middlewares" +import { ensurePublishableApiKeyMiddleware } from "./middlewares/ensure-publishable-api-key" import { GlobalMiddlewareDescriptor, HTTP_METHODS, @@ -584,6 +585,20 @@ export class ApiRoutesLoader { return } + /** + * Applies middleware that checks if a valid publishable key is set on store request + */ + applyStorePublishableKeyMiddleware(route: string) { + let middleware = + ensurePublishableApiKeyMiddleware as unknown as RequestHandler + + if (ApiRoutesLoader.traceMiddleware) { + middleware = ApiRoutesLoader.traceMiddleware(middleware, { route: route }) + } + + this.#router.use(route, middleware) + } + /** * Applies the route middleware on a route. Encapsulates the logic * needed to pass the middleware via the trace calls @@ -672,6 +687,10 @@ export class ApiRoutesLoader { ) } + if (config.routeType === "store") { + this.applyStorePublishableKeyMiddleware(descriptor.route) + } + // We only apply the auth middleware to store routes to populate the auth context. For actual authentication, users can just reapply the middleware. if (!config.optedOutOfAuth && config.routeType === "store") { this.applyAuthMiddleware( diff --git a/packages/medusa/src/api/store/middlewares.ts b/packages/medusa/src/api/store/middlewares.ts index 07e54e5e29..8466e48552 100644 --- a/packages/medusa/src/api/store/middlewares.ts +++ b/packages/medusa/src/api/store/middlewares.ts @@ -1,10 +1,3 @@ import { MiddlewareRoute } from "@medusajs/framework/http" -import { ensurePublishableApiKey } from "../../utils/middlewares/ensure-publishable-api-key" -export const storeRoutesMiddlewares: MiddlewareRoute[] = [ - { - method: "ALL", - matcher: "/store*", - middlewares: [ensurePublishableApiKey()], - }, -] +export const storeRoutesMiddlewares: MiddlewareRoute[] = [] diff --git a/packages/medusa/src/utils/middlewares/ensure-publishable-api-key.ts b/packages/medusa/src/utils/middlewares/ensure-publishable-api-key.ts deleted file mode 100644 index 282d26090e..0000000000 --- a/packages/medusa/src/utils/middlewares/ensure-publishable-api-key.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - MedusaNextFunction, - MedusaResponse, - MedusaStoreRequest, -} from "@medusajs/framework/http" -import { - ApiKeyType, - isPresent, - MedusaError, - PUBLISHABLE_KEY_HEADER, -} from "@medusajs/framework/utils" -import { refetchEntity } from "../../api/utils/refetch-entity" - -export function ensurePublishableApiKey() { - return async ( - req: MedusaStoreRequest, - _res: MedusaResponse, - next: MedusaNextFunction - ) => { - const publishableApiKey = req.get("x-publishable-api-key") - - if (!isPresent(publishableApiKey)) { - try { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `Publishable API key required in the request header: ${PUBLISHABLE_KEY_HEADER}. You can manage your keys in settings in the dashboard.` - ) - } catch (e) { - return next(e) - } - } - - // TODO: Replace this with the fancy new gql fetch - const apiKey = await refetchEntity( - "api_key", - { - token: publishableApiKey, - type: ApiKeyType.PUBLISHABLE, - $or: [ - { revoked_at: { $eq: null } }, - { revoked_at: { $gt: new Date() } }, - ], - }, - req.scope, - ["id", "token", "sales_channels_link.sales_channel_id"] - ) - - if (!apiKey) { - try { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `A valid publishable key is required to proceed with the request` - ) - } catch (e) { - return next(e) - } - } - - req.publishable_key_context = { - key: apiKey.token, - sales_channel_ids: apiKey.sales_channels_link.map( - (link) => link.sales_channel_id - ), - } - - return next() - } -}