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:
@@ -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"]
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
jest.setTimeout(50000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user