feat(medusa): API routes body parser config (#5537)
This commit is contained in:
1 parent
f90ba02087
commit
c68da6d685
17 files changed
+863
-175
No files matched your search
@@ -1,3 +1,15 @@
|
|||||||
export const customersGlobalMiddlewareMock = jest.fn()
|
export const customersGlobalMiddlewareMock = jest.fn()
|
||||||
export const customersCreateMiddlewareMock = jest.fn()
|
export const customersCreateMiddlewareMock = jest.fn()
|
||||||
export const storeCorsMiddlewareMock = jest.fn()
|
export const storeGlobalMiddlewareMock = jest.fn()
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
projectConfig: {
|
||||||
|
store_cors: "http://localhost:8000",
|
||||||
|
admin_cors: "http://localhost:7001",
|
||||||
|
database_logging: false,
|
||||||
|
jwt_secret: "supersecret",
|
||||||
|
cookie_secret: "superSecret",
|
||||||
|
},
|
||||||
|
featureFlags: {},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import { MiddlewaresConfig } from "../../types"
|
||||||
|
|
||||||
|
export const config: MiddlewaresConfig = {
|
||||||
|
errorHandler: (err, _req, res, _next) => {
|
||||||
|
const { code, message } = err
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case "NOT_ALLOWED":
|
||||||
|
res.status(405).json({
|
||||||
|
type: code.toLowerCase(),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "INVALID_DATA":
|
||||||
|
res.status(400).json({
|
||||||
|
type: code.toLowerCase(),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "CONFLICT":
|
||||||
|
res.status(409).json({
|
||||||
|
type: code.toLowerCase(),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "TEAPOT":
|
||||||
|
res.status(418).json({
|
||||||
|
type: code.toLowerCase(),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
res.status(500).json({
|
||||||
|
type: "unknown_error",
|
||||||
|
message: "An unknown error occurred.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
export const GET = async (req: Request, res: Response) => {
|
||||||
|
throw {
|
||||||
|
code: "NOT_ALLOWED",
|
||||||
|
message: "Not allowed to perform this action",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = async (req: Request, res: Response) => {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_DATA",
|
||||||
|
message: "Invalid data provided",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PUT = async (req: Request, res: Response) => {
|
||||||
|
throw {
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Conflict with another request",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE = async (req: Request, res: Response) => {
|
||||||
|
throw {
|
||||||
|
code: "TEAPOT",
|
||||||
|
message: "I'm a teapot",
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
|
export const GET = (req: Request, res: Response) => {
|
||||||
|
res.send(`GET /admin/protected`)
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
|
export const AUTHENTICATE = false
|
||||||
|
|
||||||
|
export const GET = (req: Request, res: Response) => {
|
||||||
|
res.send(`GET /admin/unprotected`)
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export const GET = async (req: Request, res: Response) => {
|
||||||
|
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not allowed")
|
||||||
|
}
|
||||||
+11
-5
@@ -1,9 +1,9 @@
|
|||||||
import { NextFunction, Request, Response } from "express"
|
import { NextFunction, Request, Response, raw } from "express"
|
||||||
import { MiddlewaresConfig } from "../../types"
|
import { MiddlewaresConfig } from "../../types"
|
||||||
import {
|
import {
|
||||||
customersCreateMiddlewareMock,
|
customersCreateMiddlewareMock,
|
||||||
customersGlobalMiddlewareMock,
|
customersGlobalMiddlewareMock,
|
||||||
storeCorsMiddlewareMock,
|
storeGlobalMiddlewareMock,
|
||||||
} from "../mocks"
|
} from "../mocks"
|
||||||
|
|
||||||
const customersGlobalMiddleware = (
|
const customersGlobalMiddleware = (
|
||||||
@@ -24,8 +24,8 @@ const customersCreateMiddleware = (
|
|||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeCors = (req: Request, res: Response, next: NextFunction) => {
|
const storeGlobal = (req: Request, res: Response, next: NextFunction) => {
|
||||||
storeCorsMiddlewareMock()
|
storeGlobalMiddlewareMock()
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,13 @@ export const config: MiddlewaresConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
matcher: "/store/*",
|
matcher: "/store/*",
|
||||||
middlewares: [storeCors],
|
middlewares: [storeGlobal],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: "/webhooks/*",
|
||||||
|
method: "POST",
|
||||||
|
bodyParser: false,
|
||||||
|
middlewares: [raw({ type: "application/json" })],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
|
export const GET = async (req: Request, res: Response) => {
|
||||||
|
res.send(`GET /store/protected`)
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
|
export const AUTHENTICATE = false
|
||||||
|
|
||||||
|
export const GET = async (req: Request, res: Response) => {
|
||||||
|
res.send(`GET /store/unprotected`)
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
|
export const POST = (req: Request, res: Response) => {
|
||||||
|
if (!(req.body instanceof Buffer)) {
|
||||||
|
res.status(400).send("Invalid body")
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send("OK")
|
||||||
|
}
|
||||||
+10
-2
@@ -1,9 +1,17 @@
|
|||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
export async function GET(req: Request, res: Response): Promise<void> {
|
export async function GET(req: Request, res: Response): Promise<void> {
|
||||||
res.send(`GET order ${req.params.id}`)
|
try {
|
||||||
|
res.send(`GET order ${req.params.id}`)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).send(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: Request, res: Response): Promise<void> {
|
export async function POST(req: Request, res: Response): Promise<void> {
|
||||||
res.send(`POST order ${req.params.id}`)
|
try {
|
||||||
|
res.send(`POST order ${req.params.id}`)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).send(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import {
|
||||||
|
moduleLoader,
|
||||||
|
ModulesDefinition,
|
||||||
|
registerMedusaModule,
|
||||||
|
} from "@medusajs/modules-sdk"
|
||||||
|
import { asValue, createContainer } from "awilix"
|
||||||
|
import express from "express"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
import { MockManager } from "medusa-test-utils"
|
||||||
|
import querystring from "querystring"
|
||||||
|
import supertest from "supertest"
|
||||||
|
import apiLoader from "../../../../api"
|
||||||
|
import featureFlagLoader, { featureFlagRouter } from "../../../../feature-flags"
|
||||||
|
import modelsLoader from "../../../../models"
|
||||||
|
import passportLoader from "../../../../passport"
|
||||||
|
import repositoriesLoader from "../../../../repositories"
|
||||||
|
import servicesLoader from "../../../../services"
|
||||||
|
import strategiesLoader from "../../../../strategies"
|
||||||
|
|
||||||
|
import RoutesLoader from "../.."
|
||||||
|
import { config } from "../mocks"
|
||||||
|
|
||||||
|
function asArray(resolvers) {
|
||||||
|
return {
|
||||||
|
resolve: (container) =>
|
||||||
|
resolvers.map((resolver) => container.build(resolver)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a test server that injects API Routes using the RoutesLoader
|
||||||
|
*
|
||||||
|
* @param {String} rootDir - The root directory of the project
|
||||||
|
*/
|
||||||
|
export const createServer = async (rootDir) => {
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
const moduleResolutions = {}
|
||||||
|
Object.entries(ModulesDefinition).forEach(([moduleKey, module]) => {
|
||||||
|
moduleResolutions[moduleKey] = registerMedusaModule(
|
||||||
|
moduleKey,
|
||||||
|
module.defaultModuleDeclaration,
|
||||||
|
undefined,
|
||||||
|
module
|
||||||
|
)[moduleKey]
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = createContainer()
|
||||||
|
|
||||||
|
container.registerAdd = function (name, registration) {
|
||||||
|
const storeKey = name + "_STORE"
|
||||||
|
|
||||||
|
if (this.registrations[storeKey] === undefined) {
|
||||||
|
this.register(storeKey, asValue([]))
|
||||||
|
}
|
||||||
|
const store = this.resolve(storeKey)
|
||||||
|
|
||||||
|
if (this.registrations[name] === undefined) {
|
||||||
|
this.register(name, asArray(store))
|
||||||
|
}
|
||||||
|
store.unshift(registration)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}.bind(container)
|
||||||
|
|
||||||
|
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
||||||
|
container.register("configModule", asValue(config))
|
||||||
|
container.register({
|
||||||
|
logger: asValue({
|
||||||
|
error: () => {},
|
||||||
|
}),
|
||||||
|
manager: asValue(MockManager),
|
||||||
|
})
|
||||||
|
|
||||||
|
app.set("trust proxy", 1)
|
||||||
|
app.use((req, _res, next) => {
|
||||||
|
req["session"] = {}
|
||||||
|
const data = req.get("Cookie")
|
||||||
|
if (data) {
|
||||||
|
req["session"] = {
|
||||||
|
...req["session"],
|
||||||
|
...JSON.parse(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
featureFlagLoader(config)
|
||||||
|
modelsLoader({ container, isTest: true })
|
||||||
|
repositoriesLoader({ container, isTest: true })
|
||||||
|
servicesLoader({ container, configModule: config })
|
||||||
|
strategiesLoader({ container, configModule: config })
|
||||||
|
await passportLoader({ app: app, container, configModule: config })
|
||||||
|
await moduleLoader({ container, moduleResolutions })
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.scope = container.createScope()
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// This where plugins normally load, but we simply load the routes
|
||||||
|
await new RoutesLoader({
|
||||||
|
app,
|
||||||
|
rootDir,
|
||||||
|
configModule: config,
|
||||||
|
}).load()
|
||||||
|
|
||||||
|
// the apiLoader needs to be called after plugins otherwise the core middleware bleads into the plugins
|
||||||
|
await apiLoader({ container, app: app, configModule: config })
|
||||||
|
|
||||||
|
const superRequest = supertest(app)
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: async (method, url, opts = {}) => {
|
||||||
|
const { payload, query, headers = {} } = opts
|
||||||
|
|
||||||
|
const queryParams = query && querystring.stringify(query)
|
||||||
|
const req = superRequest[method.toLowerCase()](
|
||||||
|
`${url}${queryParams ? "?" + queryParams : ""}`
|
||||||
|
)
|
||||||
|
headers.Cookie = headers.Cookie || ""
|
||||||
|
if (opts.adminSession) {
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
user_id: opts.adminSession.userId || opts.adminSession.jwt?.userId,
|
||||||
|
domain: "admin",
|
||||||
|
},
|
||||||
|
config.projectConfig.jwt_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
if (opts.clientSession) {
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
customer_id:
|
||||||
|
opts.clientSession.customer_id ||
|
||||||
|
opts.clientSession.jwt?.customer_id,
|
||||||
|
domain: "store",
|
||||||
|
},
|
||||||
|
config.projectConfig.jwt_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name in headers) {
|
||||||
|
if ({}.hasOwnProperty.call(headers, name)) {
|
||||||
|
req.set(name, headers[name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload && !req.get("content-type")) {
|
||||||
|
req.set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.get("accept")) {
|
||||||
|
req.set("Accept", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.set("Host", "localhost")
|
||||||
|
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = await req.send(JSON.stringify(payload))
|
||||||
|
} catch (e) {
|
||||||
|
if (e.response) {
|
||||||
|
res = e.response
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
import express from "express"
|
import express from "express"
|
||||||
import http from "http"
|
import { IdMap } from "medusa-test-utils"
|
||||||
import { resolve } from "path"
|
import { resolve } from "path"
|
||||||
import request from "supertest"
|
|
||||||
import {
|
import {
|
||||||
|
config,
|
||||||
customersCreateMiddlewareMock,
|
customersCreateMiddlewareMock,
|
||||||
customersGlobalMiddlewareMock,
|
customersGlobalMiddlewareMock,
|
||||||
storeCorsMiddlewareMock,
|
storeGlobalMiddlewareMock,
|
||||||
} from "../__fixtures__/mocks"
|
} from "../__fixtures__/mocks"
|
||||||
|
import { createServer } from "../__fixtures__/server"
|
||||||
import { RoutesLoader } from "../index"
|
import { RoutesLoader } from "../index"
|
||||||
|
|
||||||
const mockConfigModule = {
|
jest.setTimeout(30000)
|
||||||
projectConfig: {
|
|
||||||
store_cors: "http://localhost:8000",
|
|
||||||
admin_cors: "http://localhost:7001",
|
|
||||||
database_logging: false,
|
|
||||||
},
|
|
||||||
featureFlags: {},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("RoutesLoader", function () {
|
describe("RoutesLoader", function () {
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
@@ -25,44 +18,53 @@ describe("RoutesLoader", function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("Routes", function () {
|
describe("Routes", function () {
|
||||||
const app = express()
|
let request
|
||||||
const server = http.createServer(app)
|
|
||||||
|
|
||||||
beforeAll(async function () {
|
beforeAll(async function () {
|
||||||
const rootDir = resolve(__dirname, "../__fixtures__/routers")
|
const rootDir = resolve(__dirname, "../__fixtures__/routers")
|
||||||
|
|
||||||
await new RoutesLoader({
|
const { request: request_ } = await createServer(rootDir)
|
||||||
app,
|
|
||||||
rootDir,
|
request = request_
|
||||||
configModule: mockConfigModule,
|
|
||||||
}).load()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return a status 200 on GET admin/order/:id", async function () {
|
it("should return a status 200 on GET admin/order/:id", async function () {
|
||||||
await request(server)
|
const res = await request("GET", "/admin/orders/1000", {
|
||||||
.get("/admin/orders/1000")
|
adminSession: {
|
||||||
.expect(200)
|
jwt: {
|
||||||
.expect("GET order 1000")
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("GET order 1000")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return a status 200 on POST admin/order/:id", async function () {
|
it("should return a status 200 on POST admin/order/:id", async function () {
|
||||||
await request(server)
|
const res = await request("POST", "/admin/orders/1000", {
|
||||||
.post("/admin/orders/1000")
|
adminSession: {
|
||||||
.expect(200)
|
jwt: {
|
||||||
.expect("POST order 1000")
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("POST order 1000")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should call GET /customers/[customer_id]/orders/[order_id]", async function () {
|
it("should call GET /customers/[customer_id]/orders/[order_id]", async function () {
|
||||||
await request(server)
|
const res = await request("GET", "/customers/test-customer/orders/test")
|
||||||
.get("/customers/test-customer/orders/test-order")
|
|
||||||
.expect(200)
|
expect(res.status).toBe(200)
|
||||||
.expect(
|
expect(res.text).toBe(
|
||||||
'list customers {"customer_id":"test-customer","order_id":"test-order"}'
|
'list customers {"customer_id":"test-customer","order_id":"test"}'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not be able to GET /_private as the folder is prefixed with an underscore", async function () {
|
it("should not be able to GET /_private as the folder is prefixed with an underscore", async function () {
|
||||||
const res = await request(server).get("/_private")
|
const res = await request("GET", "/_private")
|
||||||
|
|
||||||
expect(res.status).toBe(404)
|
expect(res.status).toBe(404)
|
||||||
expect(res.text).toContain("Cannot GET /_private")
|
expect(res.text).toContain("Cannot GET /_private")
|
||||||
@@ -70,58 +72,179 @@ describe("RoutesLoader", function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("Middlewares", function () {
|
describe("Middlewares", function () {
|
||||||
const app = express()
|
let request
|
||||||
const server = http.createServer(app)
|
|
||||||
|
|
||||||
beforeAll(async function () {
|
beforeAll(async function () {
|
||||||
const rootDir = resolve(__dirname, "../__fixtures__/routers-middleware")
|
const rootDir = resolve(__dirname, "../__fixtures__/routers-middleware")
|
||||||
|
|
||||||
await new RoutesLoader({
|
const { request: request_ } = await createServer(rootDir)
|
||||||
app,
|
|
||||||
rootDir,
|
request = request_
|
||||||
configModule: mockConfigModule,
|
|
||||||
}).load()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should call middleware applied to `/customers`", async function () {
|
it("should call middleware applied to `/customers`", async function () {
|
||||||
await request(server)
|
const res = await request("GET", "/customers")
|
||||||
.get("/customers")
|
|
||||||
.expect(200)
|
|
||||||
.expect("list customers")
|
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("list customers")
|
||||||
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not call middleware applied to POST `/customers` when GET `/customers`", async function () {
|
it("should not call middleware applied to POST `/customers` when GET `/customers`", async function () {
|
||||||
await request(server)
|
const res = await request("GET", "/customers")
|
||||||
.get("/customers")
|
|
||||||
.expect(200)
|
|
||||||
.expect("list customers")
|
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("list customers")
|
||||||
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
||||||
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
|
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should call middleware applied to POST `/customers` when POST `/customers`", async function () {
|
it("should call middleware applied to POST `/customers` when POST `/customers`", async function () {
|
||||||
await request(server)
|
const res = await request("POST", "/customers")
|
||||||
.post("/customers")
|
|
||||||
.expect(200)
|
|
||||||
.expect("create customer")
|
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("create customer")
|
||||||
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
expect(customersGlobalMiddlewareMock).toHaveBeenCalled()
|
||||||
expect(customersCreateMiddlewareMock).toHaveBeenCalled()
|
expect(customersCreateMiddlewareMock).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should call store cors middleware on `/store/*` routes", async function () {
|
it("should call store global middleware on `/store/*` routes", async function () {
|
||||||
await request(server)
|
const res = await request("POST", "/store/products/1000/sync")
|
||||||
.post("/store/products/1000/sync")
|
|
||||||
.expect(200)
|
expect(res.status).toBe(200)
|
||||||
.expect("sync product 1000")
|
expect(res.text).toBe("sync product 1000")
|
||||||
|
expect(storeGlobalMiddlewareMock).toHaveBeenCalled()
|
||||||
|
|
||||||
expect(customersGlobalMiddlewareMock).not.toHaveBeenCalled()
|
expect(customersGlobalMiddlewareMock).not.toHaveBeenCalled()
|
||||||
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
|
expect(customersCreateMiddlewareMock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
expect(storeCorsMiddlewareMock).toHaveBeenCalled()
|
it("should apply raw middleware on POST `/webhooks/payment` route", async function () {
|
||||||
|
const res = await request("POST", "/webhooks/payment", {
|
||||||
|
payload: { test: "test" },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 200 when admin is authenticated", async () => {
|
||||||
|
const res = await request("GET", "/admin/protected", {
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("GET /admin/protected")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 401 when admin is not authenticated", async () => {
|
||||||
|
const res = await request("GET", "/admin/protected")
|
||||||
|
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
expect(res.text).toBe("Unauthorized")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 200 when admin route is opted out of authentication", async () => {
|
||||||
|
const res = await request("GET", "/admin/unprotected")
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("GET /admin/unprotected")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 200 when customer is authenticated", async () => {
|
||||||
|
const res = await request("GET", "/store/me/protected", {
|
||||||
|
clientSession: {
|
||||||
|
jwt: {
|
||||||
|
customer_id: IdMap.getId("lebron"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("GET /store/protected")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 401 when customer is not authenticated", async () => {
|
||||||
|
const res = await request("GET", "/store/me/protected")
|
||||||
|
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
expect(res.text).toBe("Unauthorized")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 200 when customer route is opted out of authentication", async () => {
|
||||||
|
const res = await request("GET", "/store/me/unprotected")
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.text).toBe("GET /store/unprotected")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the error as JSON when an error is thrown with default error handling", async () => {
|
||||||
|
const res = await request("GET", "/customers/error")
|
||||||
|
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "Not allowed",
|
||||||
|
type: "not_allowed",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Custom error handling", function () {
|
||||||
|
let request
|
||||||
|
|
||||||
|
beforeAll(async function () {
|
||||||
|
const rootDir = resolve(
|
||||||
|
__dirname,
|
||||||
|
"../__fixtures__/routers-error-handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
const { request: request_ } = await createServer(rootDir)
|
||||||
|
|
||||||
|
request = request_
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 405 when NOT_ALLOWED error is thrown", async () => {
|
||||||
|
const res = await request("GET", "/store")
|
||||||
|
|
||||||
|
expect(res.status).toBe(405)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "Not allowed to perform this action",
|
||||||
|
type: "not_allowed",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 400 when INVALID_DATA error is thrown", async () => {
|
||||||
|
const res = await request("POST", "/store")
|
||||||
|
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "Invalid data provided",
|
||||||
|
type: "invalid_data",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 409 when CONFLICT error is thrown", async () => {
|
||||||
|
const res = await request("PUT", "/store")
|
||||||
|
|
||||||
|
expect(res.status).toBe(409)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "Conflict with another request",
|
||||||
|
type: "conflict",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 418 when TEAPOT error is thrown", async () => {
|
||||||
|
const res = await request("DELETE", "/store")
|
||||||
|
|
||||||
|
expect(res.status).toBe(418)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "I'm a teapot",
|
||||||
|
type: "teapot",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,7 +259,7 @@ describe("RoutesLoader", function () {
|
|||||||
const err = await new RoutesLoader({
|
const err = await new RoutesLoader({
|
||||||
app,
|
app,
|
||||||
rootDir,
|
rootDir,
|
||||||
configModule: mockConfigModule,
|
configModule: config,
|
||||||
})
|
})
|
||||||
.load()
|
.load()
|
||||||
.catch((e) => e)
|
.catch((e) => e)
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import { promiseAll } from "@medusajs/utils"
|
import { promiseAll } from "@medusajs/utils"
|
||||||
import cors from "cors"
|
import cors from "cors"
|
||||||
import { Express, json, urlencoded } from "express"
|
import { Router, json, text, urlencoded, type Express } from "express"
|
||||||
import { readdir } from "fs/promises"
|
import { readdir } from "fs/promises"
|
||||||
import { parseCorsOrigins } from "medusa-core-utils"
|
import { parseCorsOrigins } from "medusa-core-utils"
|
||||||
import { extname, join, sep } from "path"
|
import { extname, join, sep } from "path"
|
||||||
import {
|
import {
|
||||||
authenticate,
|
authenticate,
|
||||||
authenticateCustomer,
|
authenticateCustomer,
|
||||||
|
errorHandler,
|
||||||
requireCustomerAuthentication,
|
requireCustomerAuthentication,
|
||||||
|
wrapHandler,
|
||||||
} from "../../../api/middlewares"
|
} from "../../../api/middlewares"
|
||||||
import { ConfigModule } from "../../../types/global"
|
import { ConfigModule } from "../../../types/global"
|
||||||
import logger from "../../logger"
|
import logger from "../../logger"
|
||||||
import {
|
import {
|
||||||
|
AsyncRouteHandler,
|
||||||
GlobalMiddlewareDescriptor,
|
GlobalMiddlewareDescriptor,
|
||||||
HTTP_METHODS,
|
HTTP_METHODS,
|
||||||
|
MiddlewareRoute,
|
||||||
|
MiddlewareVerb,
|
||||||
MiddlewaresConfig,
|
MiddlewaresConfig,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
RouteDescriptor,
|
RouteDescriptor,
|
||||||
@@ -85,11 +90,81 @@ function calculatePriority(path: string): number {
|
|||||||
return depth + specifity + catchall
|
return depth + specifity + catchall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchMethod(
|
||||||
|
method: RouteVerb,
|
||||||
|
configMethod: MiddlewareRoute["method"]
|
||||||
|
): boolean {
|
||||||
|
if (!configMethod || configMethod === "USE" || configMethod === "ALL") {
|
||||||
|
return true
|
||||||
|
} else if (Array.isArray(configMethod)) {
|
||||||
|
return (
|
||||||
|
configMethod.includes(method) ||
|
||||||
|
configMethod.includes("ALL") ||
|
||||||
|
configMethod.includes("USE")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return method === configMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that looks though the global middlewares and returns the first
|
||||||
|
* complete match for the given path and method.
|
||||||
|
*
|
||||||
|
* @param path - The path to match
|
||||||
|
* @param method - The method to match
|
||||||
|
* @param routes - The routes to match against
|
||||||
|
* @returns The first complete match or undefined if no match is found
|
||||||
|
*/
|
||||||
|
function findMatch(
|
||||||
|
path: string,
|
||||||
|
method: RouteVerb,
|
||||||
|
routes: MiddlewareRoute[]
|
||||||
|
): MiddlewareRoute | undefined {
|
||||||
|
for (const route of routes) {
|
||||||
|
const { matcher, method: configMethod } = route
|
||||||
|
|
||||||
|
if (matchMethod(method, configMethod)) {
|
||||||
|
let isMatch = false
|
||||||
|
|
||||||
|
if (typeof matcher === "string") {
|
||||||
|
// Convert wildcard expressions to proper regex for matching entire path
|
||||||
|
// The '.*' will match any character sequence including '/'
|
||||||
|
const regex = new RegExp(`^${matcher.split("*").join(".*")}$`)
|
||||||
|
isMatch = regex.test(path)
|
||||||
|
} else if (matcher instanceof RegExp) {
|
||||||
|
// Ensure that the regex matches the entire path
|
||||||
|
const match = path.match(matcher)
|
||||||
|
isMatch = match !== null && match[0] === path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
return route // Return the first complete match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined // Return undefined if no complete match is found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of body parser middlewares that are applied on routes
|
||||||
|
* out-of-the-box.
|
||||||
|
*/
|
||||||
|
function getBodyParserMiddleware(sizeLimit?: string | number | undefined) {
|
||||||
|
return [
|
||||||
|
json({ limit: sizeLimit }),
|
||||||
|
text({ limit: sizeLimit }),
|
||||||
|
urlencoded({ limit: sizeLimit, extended: true }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export class RoutesLoader {
|
export class RoutesLoader {
|
||||||
protected routesMap = new Map<string, RouteDescriptor>()
|
protected routesMap = new Map<string, RouteDescriptor>()
|
||||||
protected globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
|
protected globalMiddlewaresDescriptor: GlobalMiddlewareDescriptor | undefined
|
||||||
|
|
||||||
protected app: Express
|
protected app: Express
|
||||||
|
protected router: Router
|
||||||
protected activityId?: string
|
protected activityId?: string
|
||||||
protected rootDir: string
|
protected rootDir: string
|
||||||
protected configModule: ConfigModule
|
protected configModule: ConfigModule
|
||||||
@@ -113,6 +188,7 @@ export class RoutesLoader {
|
|||||||
excludes?: RegExp[]
|
excludes?: RegExp[]
|
||||||
}) {
|
}) {
|
||||||
this.app = app
|
this.app = app
|
||||||
|
this.router = Router()
|
||||||
this.activityId = activityId
|
this.activityId = activityId
|
||||||
this.rootDir = rootDir
|
this.rootDir = rootDir
|
||||||
this.configModule = configModule
|
this.configModule = configModule
|
||||||
@@ -133,16 +209,16 @@ export class RoutesLoader {
|
|||||||
}: {
|
}: {
|
||||||
config?: MiddlewaresConfig
|
config?: MiddlewaresConfig
|
||||||
}): void {
|
}): void {
|
||||||
if (!config?.routes) {
|
if (!config?.routes && !config?.errorHandler) {
|
||||||
log({
|
log({
|
||||||
activityId: this.activityId,
|
activityId: this.activityId,
|
||||||
message: `No middleware routes found. Skipping middleware application.`,
|
message: `Empty middleware config. Skipping middleware application.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const route of config.routes) {
|
for (const route of config.routes ?? []) {
|
||||||
if (!route.matcher) {
|
if (!route.matcher) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Route is missing a \`matcher\` field. The 'matcher' field is required when applying middleware to this route.`
|
`Route is missing a \`matcher\` field. The 'matcher' field is required when applying middleware to this route.`
|
||||||
@@ -211,6 +287,7 @@ export class RoutesLoader {
|
|||||||
await promiseAll(
|
await promiseAll(
|
||||||
[...this.routesMap.values()].map(async (descriptor: RouteDescriptor) => {
|
[...this.routesMap.values()].map(async (descriptor: RouteDescriptor) => {
|
||||||
const absolutePath = descriptor.absolutePath
|
const absolutePath = descriptor.absolutePath
|
||||||
|
const route = descriptor.route
|
||||||
|
|
||||||
return await import(absolutePath).then((import_) => {
|
return await import(absolutePath).then((import_) => {
|
||||||
const map = this.routesMap
|
const map = this.routesMap
|
||||||
@@ -223,30 +300,41 @@ export class RoutesLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the developer has not exported the authenticate flag
|
* If the developer has not exported the
|
||||||
* we default to true.
|
* AUTHENTICATE flag we default to true.
|
||||||
*/
|
*/
|
||||||
const shouldRequireAuth =
|
const shouldRequireAuth =
|
||||||
import_[AUTHTHENTICATE] !== undefined
|
import_[AUTHTHENTICATE] !== undefined
|
||||||
? (import_[AUTHTHENTICATE] as boolean)
|
? (import_[AUTHTHENTICATE] as boolean)
|
||||||
: true
|
: true
|
||||||
|
|
||||||
if (
|
/**
|
||||||
shouldRequireAuth &&
|
* If the developer has not exported the
|
||||||
absolutePath.includes(join("api", "admin"))
|
* CORS flag we default to true.
|
||||||
) {
|
*/
|
||||||
config.shouldRequireAdminAuth = shouldRequireAuth
|
const shouldAddCors =
|
||||||
|
import_["CORS"] !== undefined ? (import_["CORS"] as boolean) : true
|
||||||
|
|
||||||
|
if (route.startsWith("/admin")) {
|
||||||
|
if (shouldAddCors) {
|
||||||
|
config.shouldAppendAdminCors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRequireAuth) {
|
||||||
|
config.shouldRequireAdminAuth = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (route.startsWith("/store")) {
|
||||||
shouldRequireAuth &&
|
|
||||||
absolutePath.includes(join("api", "store", "me"))
|
|
||||||
) {
|
|
||||||
config.shouldRequireCustomerAuth = shouldRequireAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
if (absolutePath.includes(join("api", "store"))) {
|
|
||||||
config.shouldAppendCustomer = true
|
config.shouldAppendCustomer = true
|
||||||
|
|
||||||
|
if (shouldAddCors) {
|
||||||
|
config.shouldAppendStoreCors = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRequireAuth && route.startsWith("/store/me")) {
|
||||||
|
config.shouldRequireCustomerAuth = shouldRequireAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = Object.keys(import_).filter((key) => {
|
const handlers = Object.keys(import_).filter((key) => {
|
||||||
@@ -446,7 +534,45 @@ export class RoutesLoader {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async registerRoutes(): Promise<void> {
|
/**
|
||||||
|
* Apply the most specific body parser middleware to the router
|
||||||
|
*/
|
||||||
|
applyBodyParserMiddleware(path: string, method: RouteVerb): void {
|
||||||
|
const middlewareDescriptor = this.globalMiddlewaresDescriptor
|
||||||
|
|
||||||
|
const mostSpecificConfig = findMatch(
|
||||||
|
path,
|
||||||
|
method,
|
||||||
|
middlewareDescriptor?.config?.routes ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!mostSpecificConfig || mostSpecificConfig?.bodyParser === undefined) {
|
||||||
|
this.router[method.toLowerCase()](path, ...getBodyParserMiddleware())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostSpecificConfig?.bodyParser) {
|
||||||
|
const sizeLimit = mostSpecificConfig?.bodyParser?.sizeLimit
|
||||||
|
|
||||||
|
this.router[method.toLowerCase()](
|
||||||
|
path,
|
||||||
|
...getBodyParserMiddleware(sizeLimit)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the route specific middlewares to the router,
|
||||||
|
* this includes the cors, authentication and
|
||||||
|
* body parsing. These are applied first to ensure
|
||||||
|
* that they are applied before any other middleware.
|
||||||
|
*/
|
||||||
|
applyRouteSpecificMiddlewares(): void {
|
||||||
const prioritizedRoutes = prioritize([...this.routesMap.values()])
|
const prioritizedRoutes = prioritize([...this.routesMap.values()])
|
||||||
|
|
||||||
for (const descriptor of prioritizedRoutes) {
|
for (const descriptor of prioritizedRoutes) {
|
||||||
@@ -456,18 +582,122 @@ export class RoutesLoader {
|
|||||||
|
|
||||||
const routes = descriptor.config.routes
|
const routes = descriptor.config.routes
|
||||||
|
|
||||||
if (descriptor.config.shouldAppendCustomer) {
|
/**
|
||||||
this.app.use(descriptor.route, authenticateCustomer())
|
* Apply default store and admin middlewares if
|
||||||
|
* not opted out of.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (descriptor.config.shouldAppendAdminCors) {
|
||||||
|
/**
|
||||||
|
* Apply the admin cors
|
||||||
|
*/
|
||||||
|
this.router.use(
|
||||||
|
descriptor.route,
|
||||||
|
cors({
|
||||||
|
origin: parseCorsOrigins(
|
||||||
|
this.configModule.projectConfig.admin_cors || ""
|
||||||
|
),
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptor.config.shouldRequireAdminAuth) {
|
if (descriptor.config.shouldAppendStoreCors) {
|
||||||
this.app.use(descriptor.route, authenticate())
|
/**
|
||||||
|
* Apply the store cors
|
||||||
|
*/
|
||||||
|
this.router.use(
|
||||||
|
descriptor.route,
|
||||||
|
cors({
|
||||||
|
origin: parseCorsOrigins(
|
||||||
|
this.configModule.projectConfig.store_cors || ""
|
||||||
|
),
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptor.config.shouldAppendCustomer) {
|
||||||
|
/**
|
||||||
|
* Add the customer to the request object
|
||||||
|
*/
|
||||||
|
this.router.use(descriptor.route, authenticateCustomer())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptor.config.shouldRequireCustomerAuth) {
|
if (descriptor.config.shouldRequireCustomerAuth) {
|
||||||
this.app.use(descriptor.route, requireCustomerAuthentication())
|
/**
|
||||||
|
* Require the customer to be authenticated
|
||||||
|
*/
|
||||||
|
this.router.use(descriptor.route, requireCustomerAuthentication())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (descriptor.config.shouldRequireAdminAuth) {
|
||||||
|
/**
|
||||||
|
* Require the admin to be authenticated
|
||||||
|
*/
|
||||||
|
this.router.use(descriptor.route, authenticate())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
/**
|
||||||
|
* Apply the body parser middleware if the route
|
||||||
|
* has not opted out of it.
|
||||||
|
*/
|
||||||
|
this.applyBodyParserMiddleware(descriptor.route, route.method!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the error handler middleware to the router
|
||||||
|
*/
|
||||||
|
applyErrorHandlerMiddleware(): void {
|
||||||
|
const middlewareDescriptor = this.globalMiddlewaresDescriptor
|
||||||
|
|
||||||
|
if (!middlewareDescriptor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorHandlerFn = middlewareDescriptor.config?.errorHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user has opted out of the error handler then return
|
||||||
|
*/
|
||||||
|
if (errorHandlerFn === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user has provided a custom error handler then use it
|
||||||
|
*/
|
||||||
|
if (errorHandlerFn) {
|
||||||
|
this.router.use(errorHandlerFn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user has not provided a custom error handler then use the
|
||||||
|
* default one.
|
||||||
|
*/
|
||||||
|
this.router.use(errorHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async registerRoutes(): Promise<void> {
|
||||||
|
const middlewareDescriptor = this.globalMiddlewaresDescriptor
|
||||||
|
|
||||||
|
const shouldWrapHandler = middlewareDescriptor?.config
|
||||||
|
? middlewareDescriptor.config.errorHandler !== false
|
||||||
|
: true
|
||||||
|
|
||||||
|
const prioritizedRoutes = prioritize([...this.routesMap.values()])
|
||||||
|
|
||||||
|
for (const descriptor of prioritizedRoutes) {
|
||||||
|
if (!descriptor.config?.routes?.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = descriptor.config.routes
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
log({
|
log({
|
||||||
activityId: this.activityId,
|
activityId: this.activityId,
|
||||||
@@ -475,7 +705,16 @@ export class RoutesLoader {
|
|||||||
descriptor.route
|
descriptor.route
|
||||||
}`,
|
}`,
|
||||||
})
|
})
|
||||||
this.app[route.method!.toLowerCase()](descriptor.route, route.handler)
|
|
||||||
|
/**
|
||||||
|
* If the user hasn't opted out of error handling then
|
||||||
|
* we wrap the handler in a try/catch block.
|
||||||
|
*/
|
||||||
|
const handler = shouldWrapHandler
|
||||||
|
? wrapHandler(route.handler as AsyncRouteHandler)
|
||||||
|
: route.handler
|
||||||
|
|
||||||
|
this.router[route.method!.toLowerCase()](descriptor.route, handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,51 +732,29 @@ export class RoutesLoader {
|
|||||||
|
|
||||||
const routes = descriptor.config.routes
|
const routes = descriptor.config.routes
|
||||||
|
|
||||||
for (const route of routes) {
|
/**
|
||||||
if (Array.isArray(route.method)) {
|
* We don't prioritize the middlewares to preserve the order
|
||||||
for (const method of route.method) {
|
* in which they are defined in the 'middlewares.ts'. This is to
|
||||||
log({
|
* maintain the same behavior as how middleware is applied
|
||||||
activityId: this.activityId,
|
* in Express.
|
||||||
message: `Registering middleware [${method}] - ${route.matcher}`,
|
*/
|
||||||
})
|
|
||||||
|
|
||||||
this.app[method.toLowerCase()](route.matcher, ...route.middlewares)
|
for (const route of routes) {
|
||||||
}
|
const methods = (
|
||||||
} else {
|
Array.isArray(route.method) ? route.method : [route.method]
|
||||||
|
).filter(Boolean) as MiddlewareVerb[]
|
||||||
|
|
||||||
|
for (const method of methods) {
|
||||||
log({
|
log({
|
||||||
activityId: this.activityId,
|
activityId: this.activityId,
|
||||||
message: `Registering middleware [${route.method}] - ${route.matcher}`,
|
message: `Registering middleware [${method}] - ${route.matcher}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app[route.method!.toLowerCase()](
|
this.router[method.toLowerCase()](route.matcher, ...route.middlewares)
|
||||||
route.matcher,
|
|
||||||
...route.middlewares
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyGlobalMiddlewares() {
|
|
||||||
if (this.routesMap.size > 0) {
|
|
||||||
this.app.use(json(), urlencoded({ extended: true }))
|
|
||||||
|
|
||||||
const adminCors = this.configModule.projectConfig.admin_cors || ""
|
|
||||||
this.app.use(
|
|
||||||
"/admin",
|
|
||||||
cors({
|
|
||||||
origin: parseCorsOrigins(adminCors),
|
|
||||||
credentials: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const storeCors = this.configModule.projectConfig.store_cors || ""
|
|
||||||
this.app.use(
|
|
||||||
"/store",
|
|
||||||
cors({ origin: parseCorsOrigins(storeCors), credentials: true })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
performance && performance.mark("file-base-routing-start" + this.rootDir)
|
performance && performance.mark("file-base-routing-start" + this.rootDir)
|
||||||
|
|
||||||
@@ -561,10 +778,20 @@ export class RoutesLoader {
|
|||||||
await this.createRoutesMap({ dirPath: this.rootDir })
|
await this.createRoutesMap({ dirPath: this.rootDir })
|
||||||
await this.createRoutesConfig()
|
await this.createRoutesConfig()
|
||||||
|
|
||||||
this.applyGlobalMiddlewares()
|
this.applyRouteSpecificMiddlewares()
|
||||||
|
|
||||||
await this.registerMiddlewares()
|
await this.registerMiddlewares()
|
||||||
await this.registerRoutes()
|
await this.registerRoutes()
|
||||||
|
|
||||||
|
this.applyErrorHandlerMiddleware()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the router to the app.
|
||||||
|
*
|
||||||
|
* This prevents middleware from a plugin from
|
||||||
|
* bleeding into the global middleware stack.
|
||||||
|
*/
|
||||||
|
this.app.use("/", this.router)
|
||||||
}
|
}
|
||||||
|
|
||||||
performance && performance.mark("file-base-routing-end" + this.rootDir)
|
performance && performance.mark("file-base-routing-end" + this.rootDir)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
MedusaNextFunction,
|
||||||
MedusaRequest,
|
MedusaRequest,
|
||||||
MedusaRequestHandler,
|
MedusaRequestHandler,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
@@ -18,12 +19,16 @@ export const HTTP_METHODS = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type RouteVerb = (typeof HTTP_METHODS)[number]
|
export type RouteVerb = (typeof HTTP_METHODS)[number]
|
||||||
type MiddlewareVerb = "USE" | "ALL" | RouteVerb
|
export type MiddlewareVerb = "USE" | "ALL" | RouteVerb
|
||||||
|
|
||||||
type RouteHandler = (
|
type SyncRouteHandler = (req: MedusaRequest, res: MedusaResponse) => void
|
||||||
|
|
||||||
|
export type AsyncRouteHandler = (
|
||||||
req: MedusaRequest,
|
req: MedusaRequest,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => Promise<void> | void
|
) => Promise<void>
|
||||||
|
|
||||||
|
type RouteHandler = SyncRouteHandler | AsyncRouteHandler
|
||||||
|
|
||||||
export type RouteImplementation = {
|
export type RouteImplementation = {
|
||||||
method?: RouteVerb
|
method?: RouteVerb
|
||||||
@@ -34,6 +39,8 @@ export type RouteConfig = {
|
|||||||
shouldRequireAdminAuth?: boolean
|
shouldRequireAdminAuth?: boolean
|
||||||
shouldRequireCustomerAuth?: boolean
|
shouldRequireCustomerAuth?: boolean
|
||||||
shouldAppendCustomer?: boolean
|
shouldAppendCustomer?: boolean
|
||||||
|
shouldAppendAdminCors?: boolean
|
||||||
|
shouldAppendStoreCors?: boolean
|
||||||
routes?: RouteImplementation[]
|
routes?: RouteImplementation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,13 +48,28 @@ export type MiddlewareFunction =
|
|||||||
| MedusaRequestHandler
|
| MedusaRequestHandler
|
||||||
| ((...args: any[]) => any)
|
| ((...args: any[]) => any)
|
||||||
|
|
||||||
|
export type MedusaErrorHandlerFunction = (
|
||||||
|
error: any,
|
||||||
|
req: MedusaRequest,
|
||||||
|
res: MedusaResponse,
|
||||||
|
next: MedusaNextFunction
|
||||||
|
) => Promise<void> | void
|
||||||
|
|
||||||
|
type ParserConfig =
|
||||||
|
| false
|
||||||
|
| {
|
||||||
|
sizeLimit?: string | number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
export type MiddlewareRoute = {
|
export type MiddlewareRoute = {
|
||||||
method?: MiddlewareVerb | MiddlewareVerb[]
|
method?: MiddlewareVerb | MiddlewareVerb[]
|
||||||
matcher: string | RegExp
|
matcher: string | RegExp
|
||||||
|
bodyParser?: ParserConfig
|
||||||
middlewares: MiddlewareFunction[]
|
middlewares: MiddlewareFunction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MiddlewaresConfig = {
|
export type MiddlewaresConfig = {
|
||||||
|
errorHandler?: false | MedusaErrorHandlerFunction
|
||||||
routes?: MiddlewareRoute[]
|
routes?: MiddlewareRoute[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Express } from "express"
|
import { Express } from "express"
|
||||||
import passport from "passport"
|
import passport from "passport"
|
||||||
import { Strategy as JWTStrategy, ExtractJwt } from "passport-jwt"
|
|
||||||
import { Strategy as LocalStrategy } from "passport-local"
|
|
||||||
import { Strategy as CustomStrategy } from "passport-custom"
|
import { Strategy as CustomStrategy } from "passport-custom"
|
||||||
|
import { ExtractJwt, Strategy as JWTStrategy } from "passport-jwt"
|
||||||
|
import { Strategy as LocalStrategy } from "passport-local"
|
||||||
import { AuthService } from "../services"
|
import { AuthService } from "../services"
|
||||||
import { ConfigModule, MedusaContainer } from "../types/global"
|
import { ConfigModule, MedusaContainer } from "../types/global"
|
||||||
|
|
||||||
@@ -47,32 +47,28 @@ export default async ({
|
|||||||
const { jwt_secret } = configModule.projectConfig
|
const { jwt_secret } = configModule.projectConfig
|
||||||
passport.use(
|
passport.use(
|
||||||
"admin-session",
|
"admin-session",
|
||||||
new CustomStrategy(
|
new CustomStrategy(async (req, done) => {
|
||||||
async (req, done) => {
|
// @ts-ignore
|
||||||
|
if (req.session?.user_id) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(req.session?.user_id) {
|
return done(null, { userId: req.session.user_id })
|
||||||
// @ts-ignore
|
|
||||||
return done(null, { userId: req.session.user_id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return done(null, false)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
return done(null, false)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
passport.use(
|
passport.use(
|
||||||
"store-session",
|
"store-session",
|
||||||
new CustomStrategy(
|
new CustomStrategy(async (req, done) => {
|
||||||
async (req, done) => {
|
// @ts-ignore
|
||||||
|
if (req.session?.customer_id) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(req.session?.customer_id) {
|
return done(null, { customer_id: req.session.customer_id })
|
||||||
// @ts-ignore
|
|
||||||
return done(null, { customer_id: req.session.customer_id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return done(null, false)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
return done(null, false)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Alternatively use API token to authenticate to the admin api
|
// Alternatively use API token to authenticate to the admin api
|
||||||
@@ -80,7 +76,7 @@ export default async ({
|
|||||||
"admin-api-token",
|
"admin-api-token",
|
||||||
new CustomStrategy(async (req, done) => {
|
new CustomStrategy(async (req, done) => {
|
||||||
// extract the token from the header
|
// extract the token from the header
|
||||||
const token = req.headers["x-medusa-access-token"];
|
const token = req.headers["x-medusa-access-token"]
|
||||||
|
|
||||||
// check if header exists and is string
|
// check if header exists and is string
|
||||||
// typescript will complain if we don't check for type
|
// typescript will complain if we don't check for type
|
||||||
@@ -88,7 +84,7 @@ export default async ({
|
|||||||
return done(null, false)
|
return done(null, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth = await authService.authenticateAPIToken(token);
|
const auth = await authService.authenticateAPIToken(token)
|
||||||
if (auth.success) {
|
if (auth.success) {
|
||||||
done(null, auth.user)
|
done(null, auth.user)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { promiseAll, SearchUtils, upperCaseFirst } from "@medusajs/utils"
|
import { promiseAll, SearchUtils, upperCaseFirst } from "@medusajs/utils"
|
||||||
import { aliasTo, asFunction, asValue, Lifetime } from "awilix"
|
import { aliasTo, asFunction, asValue, Lifetime } from "awilix"
|
||||||
|
import { Express } from "express"
|
||||||
|
import fs from "fs"
|
||||||
|
import { sync as existsSync } from "fs-exists-cached"
|
||||||
|
import glob from "glob"
|
||||||
|
import _ from "lodash"
|
||||||
|
import { createRequireFromPath } from "medusa-core-utils"
|
||||||
import { FileService, OauthService } from "medusa-interfaces"
|
import { FileService, OauthService } from "medusa-interfaces"
|
||||||
|
import { trackInstallation } from "medusa-telemetry"
|
||||||
|
import { EOL } from "os"
|
||||||
|
import path from "path"
|
||||||
|
import { EntitySchema } from "typeorm"
|
||||||
import {
|
import {
|
||||||
AbstractTaxService,
|
AbstractTaxService,
|
||||||
isBatchJobStrategy,
|
isBatchJobStrategy,
|
||||||
@@ -10,6 +20,7 @@ import {
|
|||||||
isPriceSelectionStrategy,
|
isPriceSelectionStrategy,
|
||||||
isTaxCalculationStrategy,
|
isTaxCalculationStrategy,
|
||||||
} from "../interfaces"
|
} from "../interfaces"
|
||||||
|
import { MiddlewareService } from "../services"
|
||||||
import {
|
import {
|
||||||
ClassConstructor,
|
ClassConstructor,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
@@ -20,24 +31,13 @@ import {
|
|||||||
formatRegistrationName,
|
formatRegistrationName,
|
||||||
formatRegistrationNameWithoutNamespace,
|
formatRegistrationNameWithoutNamespace,
|
||||||
} from "../utils/format-registration-name"
|
} from "../utils/format-registration-name"
|
||||||
|
import { getModelExtensionsMap } from "./helpers/get-model-extension-map"
|
||||||
import {
|
import {
|
||||||
registerAbstractFulfillmentServiceFromClass,
|
registerAbstractFulfillmentServiceFromClass,
|
||||||
registerFulfillmentServiceFromClass,
|
registerFulfillmentServiceFromClass,
|
||||||
registerPaymentProcessorFromClass,
|
registerPaymentProcessorFromClass,
|
||||||
registerPaymentServiceFromClass,
|
registerPaymentServiceFromClass,
|
||||||
} from "./helpers/plugins"
|
} from "./helpers/plugins"
|
||||||
|
|
||||||
import { Express } from "express"
|
|
||||||
import fs from "fs"
|
|
||||||
import { sync as existsSync } from "fs-exists-cached"
|
|
||||||
import glob from "glob"
|
|
||||||
import _ from "lodash"
|
|
||||||
import { createRequireFromPath } from "medusa-core-utils"
|
|
||||||
import { trackInstallation } from "medusa-telemetry"
|
|
||||||
import path from "path"
|
|
||||||
import { EntitySchema } from "typeorm"
|
|
||||||
import { MiddlewareService } from "../services"
|
|
||||||
import { getModelExtensionsMap } from "./helpers/get-model-extension-map"
|
|
||||||
import { RoutesLoader } from "./helpers/routing"
|
import { RoutesLoader } from "./helpers/routing"
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
|
||||||
@@ -325,7 +325,10 @@ function registerCoreRouters(
|
|||||||
const splat = descriptor.split("/")
|
const splat = descriptor.split("/")
|
||||||
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
|
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
|
||||||
const loaded = require(fn).default
|
const loaded = require(fn).default
|
||||||
middlewareService.addRouter(path, loaded())
|
|
||||||
|
if (loaded && typeof loaded === "function") {
|
||||||
|
middlewareService.addRouter(path, loaded())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
storeFiles.forEach((fn) => {
|
storeFiles.forEach((fn) => {
|
||||||
@@ -333,7 +336,10 @@ function registerCoreRouters(
|
|||||||
const splat = descriptor.split("/")
|
const splat = descriptor.split("/")
|
||||||
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
|
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
|
||||||
const loaded = require(fn).default
|
const loaded = require(fn).default
|
||||||
middlewareService.addRouter(path, loaded())
|
|
||||||
|
if (loaded && typeof loaded === "function") {
|
||||||
|
middlewareService.addRouter(path, loaded())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +364,7 @@ async function registerApi(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
/**
|
/**
|
||||||
* Register the plugin's api routes using the file based routing.
|
* Register the plugin's API routes using the file based routing.
|
||||||
*/
|
*/
|
||||||
await new RoutesLoader({
|
await new RoutesLoader({
|
||||||
app,
|
app,
|
||||||
@@ -366,7 +372,15 @@ async function registerApi(
|
|||||||
activityId: activityId,
|
activityId: activityId,
|
||||||
configModule: configmodule,
|
configModule: configmodule,
|
||||||
}).load()
|
}).load()
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`An error occurred while registering API Routes in ${projectName}${
|
||||||
|
err.stack ? EOL + err.stack : ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
/**
|
/**
|
||||||
* For backwards compatibility we also support loading routes from
|
* For backwards compatibility we also support loading routes from
|
||||||
* `/api/index` if the file exists.
|
* `/api/index` if the file exists.
|
||||||
@@ -385,8 +399,6 @@ async function registerApi(
|
|||||||
app.use("/", routes(rootDirectory, pluginDetails.options))
|
app.use("/", routes(rootDirectory, pluginDetails.options))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code !== "MODULE_NOT_FOUND") {
|
if (err.code !== "MODULE_NOT_FOUND") {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -397,9 +409,9 @@ async function registerApi(
|
|||||||
logger.warn(`${err.stack}`)
|
logger.warn(`${err.stack}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user