feat: Add publishable key scopes middleware (#7301)

**What**

Add pub key + sales channel middlewares to the store carts API
- Assign sales channel associated with pub key, if sales channel is not passed in request
  - Throw if pub key has multiple associated sales channels
- Throw if sales channel ID in payload is not associated with publishable API key in header
This commit is contained in:
Oli Juhl
2024-05-13 16:17:52 +00:00
committed by GitHub
parent 6c94d0205c
commit 5b26f5f2cf
6 changed files with 264 additions and 3 deletions
@@ -1,6 +1,7 @@
export * from "./link-sales-channels-to-publishable-key"
export * from "./create-api-keys"
export * from "./delete-api-keys"
export * from "./link-sales-channels-to-publishable-key"
export * from "./revoke-api-keys"
export * from "./update-api-keys"
export * from "./validate-sales-channel-exists"
@@ -1,5 +1,6 @@
export * from "./create-api-keys"
export * from "./delete-api-keys"
export * from "./update-api-keys"
export * from "./revoke-api-keys"
export * from "./link-sales-channels-to-publishable-key"
export * from "./revoke-api-keys"
export * from "./update-api-keys"
@@ -1,5 +1,7 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
import { ensurePublishableKeyAndSalesChannelMatch } from "../../utils/middlewares/common/ensure-pub-key-sales-channel-match"
import { maybeAttachPublishableKeyScopes } from "../../utils/middlewares/common/maybe-attach-pub-key-scopes"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import * as OrderQueryConfig from "../orders/query-config"
@@ -47,6 +49,8 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
StoreGetCartsCart,
QueryConfig.retrieveTransformQueryConfig
),
maybeAttachPublishableKeyScopes,
ensurePublishableKeyAndSalesChannelMatch,
],
},
{
@@ -0,0 +1,52 @@
import { NextFunction } from "express"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { StoreCreateCartType } from "../../../store/carts/validators"
/**
* If a publishable key (PK) is passed in the header of the request AND
* the request carries a sales channel id param in the url or body,
* we check if the sales channel is valid for the key.
*
* If the request does not carry a sales channel id, we attempt to assign
* a sales channel associated with the PK.
*
* @throws If sales channel id is passed as a url or body param
* but that id is not in the scope defined by the PK from the header.
* If the PK is associated with multiple sales channels but no
* sales channel id is passed in the request.
*/
export async function ensurePublishableKeyAndSalesChannelMatch(
req: MedusaRequest<StoreCreateCartType> & { publishableApiKeyScopes },
res: MedusaResponse,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const pubKeySalesChannels =
req.publishableApiKeyScopes?.sales_channel_ids ?? []
const channelId = req.validatedBody?.sales_channel_id
req.errors = req.errors ?? []
if (pubKeySalesChannels.length) {
if (channelId && !pubKeySalesChannels.includes(channelId)) {
req.errors.push(
`Sales channel ID in payload ${channelId} is not associated with the Publishable API Key in the header.`
)
}
if (!channelId) {
if (pubKeySalesChannels.length > 1) {
req.errors.push(
`Cannot assign sales channel to cart. The Publishable API Key in the header has multiple associated sales channels. Please provide a sales channel ID in the request body.`
)
} else {
req.validatedBody.sales_channel_id = pubKeySalesChannels[0]
}
}
}
}
next()
}
@@ -0,0 +1,48 @@
import { RemoteQueryFunction } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { NextFunction } from "express"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
/**
* If a publishable key (PK) is passed in the header of the request, we attach
* the IDs of resources within the scope of the key.
*
* @param req - request object
* @param res - response object
* @param next - next middleware call
*
* @throws if sales channel id is passed as a url or body param
* but that id is not in the scope defined by the PK from the header
*/
export async function maybeAttachPublishableKeyScopes(
req: MedusaRequest & { publishableApiKeyScopes: any },
res: MedusaResponse,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const remoteQuery = req.scope.resolve<RemoteQueryFunction>(
ContainerRegistrationKeys.REMOTE_QUERY
)
const queryObject = remoteQueryObjectFromString({
entryPoint: "api_key",
fields: ["sales_channels.id"],
variables: {
filters: { token: pubKey },
},
})
const [apiKey] = await remoteQuery(queryObject)
req.publishableApiKeyScopes = {
sales_channel_ids: apiKey?.sales_channels.map((sc) => sc.id) ?? [],
}
}
next()
}