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,
|
"strictFunctionTypes": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"incremental": false
|
||||||
},
|
},
|
||||||
"include": ["${configDir}/src"],
|
"include": ["${configDir}/src"],
|
||||||
"exclude": ["${configDir}/dist", "${configDir}/node_modules"]
|
"exclude": ["${configDir}/dist", "${configDir}/node_modules"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
generateStoreHeaders,
|
generateStoreHeaders,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||||
|
|||||||
@@ -6,10 +6,20 @@ import {
|
|||||||
storeGlobalMiddlewareMock,
|
storeGlobalMiddlewareMock,
|
||||||
} from "../__fixtures__/mocks"
|
} from "../__fixtures__/mocks"
|
||||||
import { createServer } from "../__fixtures__/server"
|
import { createServer } from "../__fixtures__/server"
|
||||||
import { RoutesLoader } from "../index"
|
import { MedusaNextFunction, RoutesLoader } from "../index"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
jest.mock("../middlewares/ensure-publishable-api-key", () => {
|
||||||
|
return {
|
||||||
|
ensurePublishableApiKeyMiddleware: async (
|
||||||
|
req: any,
|
||||||
|
res: any,
|
||||||
|
next: MedusaNextFunction
|
||||||
|
) => next(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe("RoutesLoader", function () {
|
describe("RoutesLoader", function () {
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
jest.clearAllMocks()
|
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 { configManager } from "../config"
|
||||||
import { logger } from "../logger"
|
import { logger } from "../logger"
|
||||||
import { authenticate, AuthType, errorHandler } from "./middlewares"
|
import { authenticate, AuthType, errorHandler } from "./middlewares"
|
||||||
|
import { ensurePublishableApiKeyMiddleware } from "./middlewares/ensure-publishable-api-key"
|
||||||
import {
|
import {
|
||||||
GlobalMiddlewareDescriptor,
|
GlobalMiddlewareDescriptor,
|
||||||
HTTP_METHODS,
|
HTTP_METHODS,
|
||||||
@@ -584,6 +585,20 @@ export class ApiRoutesLoader {
|
|||||||
return
|
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
|
* Applies the route middleware on a route. Encapsulates the logic
|
||||||
* needed to pass the middleware via the trace calls
|
* 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.
|
// 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") {
|
if (!config.optedOutOfAuth && config.routeType === "store") {
|
||||||
this.applyAuthMiddleware(
|
this.applyAuthMiddleware(
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||||
import { ensurePublishableApiKey } from "../../utils/middlewares/ensure-publishable-api-key"
|
|
||||||
|
|
||||||
export const storeRoutesMiddlewares: MiddlewareRoute[] = [
|
export const storeRoutesMiddlewares: MiddlewareRoute[] = []
|
||||||
{
|
|
||||||
method: "ALL",
|
|
||||||
matcher: "/store*",
|
|
||||||
middlewares: [ensurePublishableApiKey()],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -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