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
This commit is contained in:
Riqwan Thamir
2024-10-02 18:01:50 +02:00
committed by GitHub
parent c4f26120b0
commit fbbfb0cb62
7 changed files with 111 additions and 79 deletions

View File

@@ -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"]

View File

@@ -7,7 +7,7 @@ import {
generateStoreHeaders,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(30000)
jest.setTimeout(50000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, api, getContainer }) => {

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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(

View File

@@ -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[] = []

View File

@@ -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()
}
}