From bf4724b8e6c8eaf54900ca696d5b7520957ccd00 Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Sun, 19 May 2024 12:40:28 +0200 Subject: [PATCH] feat: Destroy session + introduce `http` config (#7336) --- integration-tests/api/__tests__/admin/auth.js | 60 ++- integration-tests/api/medusa-config.js | 135 +++--- .../customer/store/create-customer.spec.ts | 4 +- .../helpers/create-authenticated-customer.ts | 4 +- integration-tests/modules/medusa-config.js | 6 +- .../src/components/layout/shell/shell.tsx | 26 +- .../dashboard/src/hooks/api/auth.tsx | 7 + packages/core/js-sdk/src/auth/index.ts | 8 + packages/core/js-sdk/src/client.ts | 22 + .../core/types/src/common/config-module.ts | 452 ++++++++++-------- .../medusa/src/api-v2/admin/users/route.ts | 15 +- .../[scope]/[auth_provider]/callback/route.ts | 6 +- .../auth/[scope]/[auth_provider]/route.ts | 7 +- .../medusa/src/api-v2/auth/middlewares.ts | 5 + .../medusa/src/api-v2/auth/session/route.ts | 8 + packages/medusa/src/loaders/config.ts | 97 ++-- packages/medusa/src/loaders/express.ts | 8 +- .../routing/__fixtures__/mocks/index.ts | 10 +- .../routing/__fixtures__/server/index.js | 4 +- .../src/loaders/helpers/routing/index.ts | 14 +- packages/medusa/src/loaders/medusa-app.ts | 26 - packages/medusa/src/loaders/passport.ts | 6 +- .../medusa/src/subscribers/payment-webhook.ts | 6 +- .../medusa/src/utils/api/http-compression.ts | 14 +- .../middlewares/authenticate-middleware.ts | 12 +- .../auth/src/providers/email-password.ts | 2 + 26 files changed, 568 insertions(+), 396 deletions(-) diff --git a/integration-tests/api/__tests__/admin/auth.js b/integration-tests/api/__tests__/admin/auth.js index f3395835ff..775a3b27ae 100644 --- a/integration-tests/api/__tests__/admin/auth.js +++ b/integration-tests/api/__tests__/admin/auth.js @@ -12,7 +12,10 @@ const adminHeaders = { jest.setTimeout(30000) medusaIntegrationTestRunner({ - env: { MEDUSA_FF_MEDUSA_V2: true }, + force_modules_migration: true, + env: { + MEDUSA_FF_MEDUSA_V2: true, + }, testSuite: ({ dbConnection, getContainer, api }) => { let container @@ -58,21 +61,48 @@ medusaIntegrationTestRunner({ ) }) - // TODO: Remove in V2, as this is no longer supported - it("creates admin JWT token correctly", async () => { - breaking(async () => { - const response = await api - .post("/admin/auth/token", { - email: "admin@medusa.js", - password: "secret_password", - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.access_token).toEqual(expect.any(String)) + it("should test the entire authentication lifecycle", async () => { + // sign in + const response = await api.post("/auth/admin/emailpass", { + email: "admin@medusa.js", + password: "secret_password", }) + + expect(response.status).toEqual(200) + expect(response.data).toEqual({ token: expect.any(String) }) + + const headers = { + headers: { ["authorization"]: `Bearer ${response.data.token}` }, + } + + // convert token to session + const cookieRequest = await api.post("/auth/session", {}, headers) + expect(cookieRequest.status).toEqual(200) + + // extract cookie + const [cookie] = cookieRequest.headers["set-cookie"][0].split(";") + + const cookieHeader = { + headers: { Cookie: cookie }, + } + + // perform cookie authenticated request + const authedRequest = await api.get( + "/admin/products?limit=1", + cookieHeader + ) + expect(authedRequest.status).toEqual(200) + + // sign out + const signOutRequest = await api.delete("/auth/session", cookieHeader) + expect(signOutRequest.status).toEqual(200) + + // attempt to perform authenticated request + const unAuthedRequest = await api + .get("/admin/products?limit=1", cookieHeader) + .catch((e) => e) + + expect(unAuthedRequest.response.status).toEqual(401) }) }, }) diff --git a/integration-tests/api/medusa-config.js b/integration-tests/api/medusa-config.js index d5869f3cf1..f7ae5882e3 100644 --- a/integration-tests/api/medusa-config.js +++ b/integration-tests/api/medusa-config.js @@ -24,10 +24,12 @@ module.exports = { redis_url: redisUrl, database_url: DB_URL, database_type: "postgres", - jwt_secret: "test", - cookie_secret: "test", - http_compression: { - enabled: enableResponseCompression, + http: { + compression: { + enabled: enableResponseCompression, + }, + jwtSecret: "test", + cookieSecret: "test", }, }, featureFlags: { @@ -39,75 +41,70 @@ module.exports = { options: { ttl: cacheTTL }, }, workflows: true, - // We don't want to load the modules if v2 is not enabled, as they run data operations and migrations on load. - ...(enableMedusaV2 - ? { - [Modules.AUTH]: { - scope: "internal", - resources: "shared", - resolve: "@medusajs/auth", - options: { - providers: [ - { - name: "emailpass", - scopes: { - admin: {}, - store: {}, - }, - }, - ], + [Modules.AUTH]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/auth", + options: { + providers: [ + { + name: "emailpass", + scopes: { + admin: {}, + store: {}, }, }, - [Modules.USER]: { - scope: "internal", - resources: "shared", - resolve: "@medusajs/user", + ], + }, + }, + [Modules.USER]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/user", + options: { + jwt_secret: "test", + }, + }, + [Modules.CACHE]: { + resolve: "@medusajs/cache-inmemory", + options: { ttl: 0 }, // Cache disabled + }, + [Modules.STOCK_LOCATION]: { + resolve: "@medusajs/stock-location-next", + options: {}, + }, + [Modules.INVENTORY]: { + resolve: "@medusajs/inventory-next", + options: {}, + }, + [Modules.FILE]: { + resolve: "@medusajs/file", + options: { + providers: [ + { + resolve: "@medusajs/file-local-next", options: { - jwt_secret: "test", + config: { + local: {}, + }, }, }, - [Modules.CACHE]: { - resolve: "@medusajs/cache-inmemory", - options: { ttl: 0 }, // Cache disabled - }, - [Modules.STOCK_LOCATION]: { - resolve: "@medusajs/stock-location-next", - options: {}, - }, - [Modules.INVENTORY]: { - resolve: "@medusajs/inventory-next", - options: {}, - }, - [Modules.FILE]: { - resolve: "@medusajs/file", - options: { - providers: [ - { - resolve: "@medusajs/file-local-next", - options: { - config: { - local: {}, - }, - }, - }, - ], - }, - }, - [Modules.PRODUCT]: true, - [Modules.PRICING]: true, - [Modules.PROMOTION]: true, - [Modules.CUSTOMER]: true, - [Modules.SALES_CHANNEL]: true, - [Modules.CART]: true, - [Modules.WORKFLOW_ENGINE]: true, - [Modules.REGION]: true, - [Modules.API_KEY]: true, - [Modules.STORE]: true, - [Modules.TAX]: true, - [Modules.CURRENCY]: true, - [Modules.PAYMENT]: true, - [Modules.FULFILLMENT]: true, - } - : {}), + ], + }, + }, + [Modules.PRODUCT]: true, + [Modules.PRICING]: true, + [Modules.PROMOTION]: true, + [Modules.CUSTOMER]: true, + [Modules.SALES_CHANNEL]: true, + [Modules.CART]: true, + [Modules.WORKFLOW_ENGINE]: true, + [Modules.REGION]: true, + [Modules.API_KEY]: true, + [Modules.STORE]: true, + [Modules.TAX]: true, + [Modules.CURRENCY]: true, + [Modules.PAYMENT]: true, + [Modules.FULFILLMENT]: true, }, } diff --git a/integration-tests/modules/__tests__/customer/store/create-customer.spec.ts b/integration-tests/modules/__tests__/customer/store/create-customer.spec.ts index 398c160822..c6ac7d7d9d 100644 --- a/integration-tests/modules/__tests__/customer/store/create-customer.spec.ts +++ b/integration-tests/modules/__tests__/customer/store/create-customer.spec.ts @@ -34,7 +34,7 @@ medusaIntegrationTestRunner({ const authService: IAuthModuleService = appContainer.resolve( ModuleRegistrationName.AUTH ) - const { jwt_secret } = + const { http } = appContainer.resolve("configModule").projectConfig const authUser = await authService.create({ entity_id: "store_user", @@ -42,7 +42,7 @@ medusaIntegrationTestRunner({ scope: "store", }) - const token = jwt.sign(authUser, jwt_secret) + const token = jwt.sign(authUser, http.jwtSecret) const response = await api.post( `/store/customers`, diff --git a/integration-tests/modules/helpers/create-authenticated-customer.ts b/integration-tests/modules/helpers/create-authenticated-customer.ts index 3c355bf8e6..7864358af4 100644 --- a/integration-tests/modules/helpers/create-authenticated-customer.ts +++ b/integration-tests/modules/helpers/create-authenticated-customer.ts @@ -7,7 +7,7 @@ export const createAuthenticatedCustomer = async ( appContainer: MedusaContainer, customerData: Partial = {} ) => { - const { jwt_secret } = appContainer.resolve("configModule").projectConfig + const { http } = appContainer.resolve("configModule").projectConfig const authService = appContainer.resolve(ModuleRegistrationName.AUTH) const customerModuleService = appContainer.resolve( ModuleRegistrationName.CUSTOMER @@ -27,7 +27,7 @@ export const createAuthenticatedCustomer = async ( app_metadata: { customer_id: customer.id }, }) - const token = jwt.sign(authUser, jwt_secret) + const token = jwt.sign(authUser, http.jwtSecret) return { customer, authUser, jwt: token } } diff --git a/integration-tests/modules/medusa-config.js b/integration-tests/modules/medusa-config.js index 21f730f4dc..44ddd23426 100644 --- a/integration-tests/modules/medusa-config.js +++ b/integration-tests/modules/medusa-config.js @@ -38,8 +38,10 @@ module.exports = { projectConfig: { database_url: DB_URL, database_type: "postgres", - jwt_secret: "test", - cookie_secret: "test", + http: { + jwtSecret: "test", + cookieSecret: "test", + }, }, featureFlags: { medusa_v2: enableMedusaV2, diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx index cf65fffb5e..6f04e0ce1b 100644 --- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -19,6 +19,7 @@ import { UIMatch, useLocation, useMatches, + useNavigate, } from "react-router-dom" import { Skeleton } from "../../common/skeleton" @@ -27,6 +28,8 @@ import { useMe } from "../../../hooks/api/users" import { useSearch } from "../../../providers/search-provider" import { useSidebar } from "../../../providers/sidebar-provider" import { useTheme } from "../../../providers/theme-provider" +import { useLogout } from "../../../hooks/api/auth" +import { queryClient } from "../../../lib/medusa" export const Shell = ({ children }: PropsWithChildren) => { return ( @@ -200,20 +203,19 @@ const ThemeToggle = () => { } const Logout = () => { - // const navigate = useNavigate() - // const { mutateAsync: logoutMutation } = useAdminDeleteSession() + const navigate = useNavigate() + const { mutateAsync: logoutMutation } = useLogout() const handleLayout = async () => { - // await logoutMutation(undefined, { - // onSuccess: () => { - // /** - // * When the user logs out, we want to clear the query cache - // */ - // queryClient.clear() - // navigate("/login") - // }, - // }) - // noop + await logoutMutation(undefined, { + onSuccess: () => { + /** + * When the user logs out, we want to clear the query cache + */ + queryClient.clear() + navigate("/login") + }, + }) } return ( diff --git a/packages/admin-next/dashboard/src/hooks/api/auth.tsx b/packages/admin-next/dashboard/src/hooks/api/auth.tsx index 6be8ca81fb..b16c3a55fa 100644 --- a/packages/admin-next/dashboard/src/hooks/api/auth.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/auth.tsx @@ -14,3 +14,10 @@ export const useEmailPassLogin = ( ...options, }) } + +export const useLogout = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: () => sdk.auth.logout(), + ...options, + }) +} diff --git a/packages/core/js-sdk/src/auth/index.ts b/packages/core/js-sdk/src/auth/index.ts index 516f1900ab..9e775cb763 100644 --- a/packages/core/js-sdk/src/auth/index.ts +++ b/packages/core/js-sdk/src/auth/index.ts @@ -30,4 +30,12 @@ export class Auth { this.client.setToken(token) } } + + logout = async () => { + await this.client.fetch("/auth/session", { + method: "DELETE", + }) + + this.client.clearToken() + } } diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index 63f0ee4a21..ba7cb3aab9 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -109,6 +109,28 @@ export class Client { this.setToken_(token) } + clearToken() { + this.clearToken_() + } + + protected clearToken_() { + const { storageMethod, storageKey } = this.getTokenStorageInfo_() + switch (storageMethod) { + case "local": { + window.localStorage.removeItem(storageKey) + break + } + case "session": { + window.sessionStorage.removeItem(storageKey) + break + } + case "memory": { + this.token = "" + break + } + } + } + protected initClient(): ClientFetch { const defaultHeaders = new Headers({ "content-type": "application/json", diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index a903e9d360..ba3e85b9ff 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -67,7 +67,7 @@ type SessionOptions = { */ saveUninitialized?: boolean /** - * The secret to sign the session ID cookie. By default, the value of `cookie_secret` is used. + * The secret to sign the session ID cookie. By default, the value of `http.cookieSecret` is used. * Refer to [express-session’s documentation](https://www.npmjs.com/package/express-session#secret) for details. */ secret?: string @@ -111,193 +111,6 @@ export type HttpCompressionOptions = { * Essential configurations related to the Medusa backend, such as database and CORS configurations. */ export type ProjectConfigOptions = { - /** - * The Medusa backend’s API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - * - * `store_cors` is a string used to specify the accepted URLs or patterns for store API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. - * - * Every origin in that list must either be: - * - * 1. A URL. For example, `http://localhost:8000`. The URL must not end with a backslash; - * 2. Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that the backend tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`. - * - * @example - * Some example values of common use cases: - * - * ```bash - * # Allow different ports locally starting with 800 - * STORE_CORS=/http:\/\/localhost:800\d+$/ - * - * # Allow any origin ending with vercel.app. For example, storefront.vercel.app - * STORE_CORS=/vercel\.app$/ - * - * # Allow all HTTP requests - * STORE_CORS=/http:\/\/.+/ - * ``` - * - * Then, set the configuration in `medusa-config.js`: - * - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * store_cors: process.env.STORE_CORS, - * // ... - * }, - * // ... - * } - * ``` - * - * If you’re adding the value directly within `medusa-config.js`, make sure to add an extra escaping `/` for every backslash in the pattern. For example: - * - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * store_cors: "/vercel\\.app$/", - * // ... - * }, - * // ... - * } - * ``` - */ - store_cors?: string - /** - * The Medusa backend’s API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - * - * `admin_cors` is a string used to specify the accepted URLs or patterns for admin API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. - * - * Every origin in that list must either be: - * - * 1. A URL. For example, `http://localhost:7001`. The URL must not end with a backslash; - * 2. Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that the backend tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`. - * - * @example - * Some example values of common use cases: - * - * ```bash - * # Allow different ports locally starting with 700 - * ADMIN_CORS=/http:\/\/localhost:700\d+$/ - * - * # Allow any origin ending with vercel.app. For example, admin.vercel.app - * ADMIN_CORS=/vercel\.app$/ - * - * # Allow all HTTP requests - * ADMIN_CORS=/http:\/\/.+/ - * ``` - * - * Then, set the configuration in `medusa-config.js`: - * - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * admin_cors: process.env.ADMIN_CORS, - * // ... - * }, - * // ... - * } - * ``` - * - * If you’re adding the value directly within `medusa-config.js`, make sure to add an extra escaping `/` for every backslash in the pattern. For example: - * - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * admin_cors: "/http:\\/\\/localhost:700\\d+$/", - * // ... - * }, - * // ... - * } - * ``` - */ - admin_cors?: string - /** - * The Medusa backend’s API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - * - * `auth_cors` is a string used to specify the accepted URLs or patterns for API Routes starting with `/auth`. It can either be one accepted origin, or a comma-separated list of accepted origins. - * - * Every origin in that list must either be: - * - * 1. A URL. For example, `http://localhost:7001`. The URL must not end with a backslash; - * 2. Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that the backend tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`. - * - * @example - * Some example values of common use cases: - * - * ```bash - * # Allow different ports locally starting with 700 - * AUTH_CORS=/http:\/\/localhost:700\d+$/ - * - * # Allow any origin ending with vercel.app. For example, admin.vercel.app - * AUTH_CORS=/vercel\.app$/ - * - * # Allow all HTTP requests - * AUTH_CORS=/http:\/\/.+/ - * ``` - * - * Then, set the configuration in `medusa-config.js`: - * - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * auth_cors: process.env.AUTH_CORS, - * // ... - * }, - * // ... - * } - * ``` - * - * If you’re adding the value directly within `medusa-config.js`, make sure to add an extra escaping `/` for every backslash in the pattern. For example: - * - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * auth_cors: "/http:\\/\\/localhost:700\\d+$/", - * // ... - * }, - * // ... - * } - * ``` - */ - auth_cors?: string - /** - * A random string used to create cookie tokens. Although this configuration option is not required, it’s highly recommended to set it for better security. - * - * In a development environment, if this option is not set, the default secret is `supersecret` However, in production, if this configuration is not set, an error is thrown and - * the backend crashes. - * - * @example - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * cookie_secret: process.env.COOKIE_SECRET || - * "supersecret", - * // ... - * }, - * // ... - * } - * ``` - */ - cookie_secret?: string - - /** - * A random string used to create authentication tokens. Although this configuration option is not required, it’s highly recommended to set it for better security. - * - * In a development environment, if this option is not set the default secret is `supersecret` However, in production, if this configuration is not set an error, an - * error is thrown and the backend crashes. - * - * @example - * ```js title="medusa-config.js" - * module.exports = { - * projectConfig: { - * jwt_secret: process.env.JWT_SECRET || - * "supersecret", - * // ... - * }, - * // ... - * } - * ``` - */ - jwt_secret?: string - /** * The name of the database to connect to. If specified in `database_url`, then it’s not required to include it. * @@ -562,6 +375,7 @@ export type ProjectConfigOptions = { session_options?: SessionOptions /** + * @deprecated - use `http.compression` instead * Configure HTTP compression from the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. * However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative. * @@ -624,6 +438,268 @@ export type ProjectConfigOptions = { * ``` */ worker_mode?: "shared" | "worker" | "server" + + /** + * Configure the application's http-specific settings + * + * @example + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * cookieSecret: "some-super-secret", + * compression: { ... }, + * } + * // ... + * }, + * // ... + * } + * ``` + */ + http: { + /** + * A random string used to create authentication tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. + * + * In a development environment, if this option is not set the default secret is `supersecret` However, in production, if this configuration is not set an error, an + * error is thrown and the backend crashes. + * + * @example + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * cookieSecret: "supersecret" + * } + * }, + * // ... + * } + * ``` + */ + jwtSecret?: string + /** + * The expiration time for the JWT token. If not provided, the default value is `24h`. + * + * @example + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * jwtExpiresIn: "2d" + * } + * }, + * // ... + * } + * ``` + */ + jwtExpiresIn?: string + /** + * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. + * + * In a development environment, if this option is not set, the default secret is `supersecret` However, in production, if this configuration is not set, an error is thrown and + * the backend crashes. + * + * @example + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * cookieSecret: "supersecret" + * } + * // ... + * }, + * // ... + * } + * ``` + */ + cookieSecret?: string + /** + * The Medusa backend’s API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. + * + * `cors` is a string used to specify the accepted URLs or patterns for API Routes starting with `/auth`. It can either be one accepted origin, or a comma-separated list of accepted origins. + * + * Every origin in that list must either be: + * + * 1. A URL. For example, `http://localhost:7001`. The URL must not end with a backslash; + * 2. Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that the backend tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`. + * + * @example + * Some example values of common use cases: + * + * ```bash + * # Allow different ports locally starting with 700 + * AUTH_CORS=/http:\/\/localhost:700\d+$/ + * + * # Allow any origin ending with vercel.app. For example, admin.vercel.app + * AUTH_CORS=/vercel\.app$/ + * + * # Allow all HTTP requests + * AUTH_CORS=/http:\/\/.+/ + * ``` + * + * Then, set the configuration in `medusa-config.js`: + * + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * authCors: process.env.AUTH_CORS + * } + * // ... + * }, + * // ... + * } + * ``` + * + * If you’re adding the value directly within `medusa-config.js`, make sure to add an extra escaping `/` for every backslash in the pattern. For example: + * + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * authCors: "/http:\\/\\/localhost:700\\d+$/", + * } + * // ... + * }, + * // ... + * } + * ``` + */ + authCors: string + /** + * + * Configure HTTP compression from the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. + * However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative. + * + * Its value is an object that has the following properties: + * + * If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header `"x-no-compression": true`. + * + * @example + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * compression: { + * enabled: true, + * level: 6, + * memLevel: 8, + * threshold: 1024, + * } + * }, + * // ... + * }, + * // ... + * } + * ``` + */ + compression?: HttpCompressionOptions + /** + * The Medusa backend’s API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. + * + * `store_cors` is a string used to specify the accepted URLs or patterns for store API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. + * + * Every origin in that list must either be: + * + * 1. A URL. For example, `http://localhost:8000`. The URL must not end with a backslash; + * 2. Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that the backend tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`. + * + * @example + * Some example values of common use cases: + * + * ```bash + * # Allow different ports locally starting with 800 + * STORE_CORS=/http:\/\/localhost:800\d+$/ + * + * # Allow any origin ending with vercel.app. For example, storefront.vercel.app + * STORE_CORS=/vercel\.app$/ + * + * # Allow all HTTP requests + * STORE_CORS=/http:\/\/.+/ + * ``` + * + * Then, set the configuration in `medusa-config.js`: + * + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * storeCors: process.env.STORE_CORS, + * } + * // ... + * }, + * // ... + * } + * ``` + * + * If you’re adding the value directly within `medusa-config.js`, make sure to add an extra escaping `/` for every backslash in the pattern. For example: + * + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * storeCors: "/vercel\\.app$/", + * } + * // ... + * }, + * // ... + * } + * ``` + */ + storeCors: string + /** + * The Medusa backend’s API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. + * + * `admin_cors` is a string used to specify the accepted URLs or patterns for admin API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. + * + * Every origin in that list must either be: + * + * 1. A URL. For example, `http://localhost:7001`. The URL must not end with a backslash; + * 2. Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that the backend tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`. + * + * @example + * Some example values of common use cases: + * + * ```bash + * # Allow different ports locally starting with 700 + * ADMIN_CORS=/http:\/\/localhost:700\d+$/ + * + * # Allow any origin ending with vercel.app. For example, admin.vercel.app + * ADMIN_CORS=/vercel\.app$/ + * + * # Allow all HTTP requests + * ADMIN_CORS=/http:\/\/.+/ + * ``` + * + * Then, set the configuration in `medusa-config.js`: + * + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * adminCors: process.env.ADMIN_CORS, + * } + * // ... + * }, + * // ... + * } + * ``` + * + * If you’re adding the value directly within `medusa-config.js`, make sure to add an extra escaping `/` for every backslash in the pattern. For example: + * + * ```js title="medusa-config.js" + * module.exports = { + * projectConfig: { + * http: { + * adminCors: process.env.ADMIN_CORS, + * } + * // ... + * }, + * // ... + * } + * ``` + */ + adminCors: string + } } /** diff --git a/packages/medusa/src/api-v2/admin/users/route.ts b/packages/medusa/src/api-v2/admin/users/route.ts index 9033b6b4ed..abee7116c1 100644 --- a/packages/medusa/src/api-v2/admin/users/route.ts +++ b/packages/medusa/src/api-v2/admin/users/route.ts @@ -53,19 +53,28 @@ export const POST = async ( userData: req.validatedBody, authUserId: req.auth.auth_user_id, }, + throwOnError: false, + } + + const { errors } = await createUserAccountWorkflow(req.scope).run(input) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error } - const { result } = await createUserAccountWorkflow(req.scope).run(input) const user = await refetchUser( req.auth.auth_user_id, req.scope, req.remoteQueryConfig.fields ) - const { jwt_secret } = req.scope.resolve( + const { http } = req.scope.resolve( ContainerRegistrationKeys.CONFIG_MODULE ).projectConfig - const token = jwt.sign(user, jwt_secret) + + const token = jwt.sign(user, http.jwtSecret, { + expiresIn: http.jwtExpiresIn, + }) res.status(200).json({ user, token }) } diff --git a/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts index d3d635c631..1491c8cf20 100644 --- a/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts @@ -25,9 +25,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { success, error, authUser, successRedirectUrl } = authResult if (success) { - const { jwt_secret } = req.scope.resolve("configModule").projectConfig + const { http } = req.scope.resolve("configModule").projectConfig - const token = jwt.sign(authUser, jwt_secret) + const { jwtSecret, jwtExpiresIn } = http + + const token = jwt.sign(authUser, jwtSecret, { expiresIn: jwtExpiresIn }) if (successRedirectUrl) { const url = new URL(successRedirectUrl!) diff --git a/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts index 088f40f9fc..6a7266a192 100644 --- a/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts +++ b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts @@ -30,8 +30,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } if (success) { - const { jwt_secret } = req.scope.resolve("configModule").projectConfig - const token = jwt.sign(authUser, jwt_secret) + const { http } = req.scope.resolve("configModule").projectConfig + + const token = jwt.sign(authUser, http.jwtSecret, { + expiresIn: http.jwtExpiresIn, + }) return res.status(200).json({ token }) } diff --git a/packages/medusa/src/api-v2/auth/middlewares.ts b/packages/medusa/src/api-v2/auth/middlewares.ts index 3c67152a73..02c29acdfe 100644 --- a/packages/medusa/src/api-v2/auth/middlewares.ts +++ b/packages/medusa/src/api-v2/auth/middlewares.ts @@ -7,6 +7,11 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/auth/session", middlewares: [authenticate(/.*/, "bearer")], }, + { + method: ["DELETE"], + matcher: "/auth/session", + middlewares: [authenticate(/.*/, ["session"])], + }, { method: ["POST"], matcher: "/auth/:scope/:auth_provider/callback", diff --git a/packages/medusa/src/api-v2/auth/session/route.ts b/packages/medusa/src/api-v2/auth/session/route.ts index bd404031ab..d9d827cb48 100644 --- a/packages/medusa/src/api-v2/auth/session/route.ts +++ b/packages/medusa/src/api-v2/auth/session/route.ts @@ -11,3 +11,11 @@ export const POST = async ( res.status(200).json({ user: req.auth }) } + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + req.session.destroy() + res.json({ success: true }) +} diff --git a/packages/medusa/src/loaders/config.ts b/packages/medusa/src/loaders/config.ts index b1cf60e988..767f175098 100644 --- a/packages/medusa/src/loaders/config.ts +++ b/packages/medusa/src/loaders/config.ts @@ -1,6 +1,6 @@ +import { ConfigModule } from "@medusajs/types" import { getConfigFile, isDefined } from "medusa-core-utils" import logger from "./logger" -import { ConfigModule } from "@medusajs/types" const isProduction = ["production", "prod"].includes(process.env.NODE_ENV || "") @@ -18,47 +18,55 @@ export const handleConfigError = (error: Error): void => { process.exit(1) } -export default (rootDirectory: string): ConfigModule => { - const { configModule, error } = getConfigFile( - rootDirectory, - `medusa-config` - ) +const buildHttpConfig = (projectConfig: ConfigModule["projectConfig"]) => { + const http = projectConfig.http ?? {} - if (error) { - handleConfigError(error) + http.jwtExpiresIn = http?.jwtExpiresIn ?? "1d" + http.authCors = http.authCors ?? "" + http.storeCors = http.storeCors ?? "" + http.adminCors = http.adminCors ?? "" + + http.jwtSecret = http?.jwtSecret ?? process.env.JWT_SECRET + + if (!http.jwtSecret) { + errorHandler( + `[medusa-config] ⚠️ http.jwtSecret not found.${ + isProduction ? "" : "Using default 'supersecret'." + }` + ) + + http.jwtSecret = "supersecret" } - if (!configModule?.projectConfig?.redis_url) { + http.cookieSecret = + projectConfig.http?.cookieSecret ?? process.env.COOKIE_SECRET + + if (!http.cookieSecret) { + errorHandler( + `[medusa-config] ⚠️ http.cookieSecret not found.${ + isProduction ? "" : " Using default 'supersecret'." + }` + ) + + http.cookieSecret = "supersecret" + } + + return http +} + +const normalizeProjectConfig = ( + projectConfig: ConfigModule["projectConfig"] +) => { + if (!projectConfig?.redis_url) { console.log( `[medusa-config] ⚠️ redis_url not found. A fake redis instance will be used.` ) } - const jwt_secret = - configModule?.projectConfig?.jwt_secret ?? process.env.JWT_SECRET - if (!jwt_secret) { - errorHandler( - `[medusa-config] ⚠️ jwt_secret not found.${ - isProduction - ? "" - : " fallback to either cookie_secret or default 'supersecret'." - }` - ) - } + projectConfig.http = buildHttpConfig(projectConfig) - const cookie_secret = - configModule?.projectConfig?.cookie_secret ?? process.env.COOKIE_SECRET - if (!cookie_secret) { - errorHandler( - `[medusa-config] ⚠️ cookie_secret not found.${ - isProduction - ? "" - : " fallback to either cookie_secret or default 'supersecret'." - }` - ) - } + let worker_mode = projectConfig?.worker_mode - let worker_mode = configModule?.projectConfig?.worker_mode if (!isDefined(worker_mode)) { const env = process.env.MEDUSA_WORKER_MODE if (isDefined(env)) { @@ -71,12 +79,25 @@ export default (rootDirectory: string): ConfigModule => { } return { - projectConfig: { - jwt_secret: jwt_secret ?? "supersecret", - cookie_secret: cookie_secret ?? "supersecret", - ...configModule?.projectConfig, - worker_mode, - }, + ...projectConfig, + worker_mode, + } +} + +export default (rootDirectory: string): ConfigModule => { + const { configModule, error } = getConfigFile( + rootDirectory, + `medusa-config` + ) + + if (error) { + handleConfigError(error) + } + + const projectConfig = normalizeProjectConfig(configModule.projectConfig) + + return { + projectConfig, admin: configModule?.admin ?? {}, modules: configModule.modules ?? {}, featureFlags: configModule?.featureFlags ?? {}, diff --git a/packages/medusa/src/loaders/express.ts b/packages/medusa/src/loaders/express.ts index 8de3c14e01..3c72d80d1b 100644 --- a/packages/medusa/src/loaders/express.ts +++ b/packages/medusa/src/loaders/express.ts @@ -1,10 +1,10 @@ +import { ConfigModule } from "@medusajs/types" import createStore from "connect-redis" import cookieParser from "cookie-parser" import { Express } from "express" import session from "express-session" -import morgan from "morgan" import Redis from "ioredis" -import { ConfigModule } from "@medusajs/types" +import morgan from "morgan" type Options = { app: Express @@ -28,14 +28,14 @@ export default async ({ sameSite = "none" } - const { cookie_secret, session_options } = configModule.projectConfig + const { http, session_options } = configModule.projectConfig const sessionOpts = { name: session_options?.name ?? "connect.sid", resave: session_options?.resave ?? true, rolling: session_options?.rolling ?? false, saveUninitialized: session_options?.saveUninitialized ?? true, proxy: true, - secret: session_options?.secret ?? cookie_secret, + secret: session_options?.secret ?? http?.cookieSecret, cookie: { sameSite, secure, diff --git a/packages/medusa/src/loaders/helpers/routing/__fixtures__/mocks/index.ts b/packages/medusa/src/loaders/helpers/routing/__fixtures__/mocks/index.ts index 1b915b49f5..dd93d44728 100644 --- a/packages/medusa/src/loaders/helpers/routing/__fixtures__/mocks/index.ts +++ b/packages/medusa/src/loaders/helpers/routing/__fixtures__/mocks/index.ts @@ -4,11 +4,13 @@ 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", + http: { + storeCors: "http://localhost:8000", + adminCors: "http://localhost:7001", + jwtSecret: "supersecret", + cookieSecret: "superSecret", + }, }, featureFlags: {}, plugins: [], diff --git a/packages/medusa/src/loaders/helpers/routing/__fixtures__/server/index.js b/packages/medusa/src/loaders/helpers/routing/__fixtures__/server/index.js index 921c191277..de999ef069 100644 --- a/packages/medusa/src/loaders/helpers/routing/__fixtures__/server/index.js +++ b/packages/medusa/src/loaders/helpers/routing/__fixtures__/server/index.js @@ -124,7 +124,7 @@ export const createServer = async (rootDir) => { user_id: opts.adminSession.userId || opts.adminSession.jwt?.userId, domain: "admin", }, - config.projectConfig.jwt_secret + config.projectConfig.http.jwtSecret ) headers.Authorization = `Bearer ${token}` @@ -137,7 +137,7 @@ export const createServer = async (rootDir) => { opts.clientSession.jwt?.customer_id, domain: "store", }, - config.projectConfig.jwt_secret + config.projectConfig.http.jwtSecret ) headers.Authorization = `Bearer ${token}` diff --git a/packages/medusa/src/loaders/helpers/routing/index.ts b/packages/medusa/src/loaders/helpers/routing/index.ts index b44cef13a3..6ba15fd2fb 100644 --- a/packages/medusa/src/loaders/helpers/routing/index.ts +++ b/packages/medusa/src/loaders/helpers/routing/index.ts @@ -1,30 +1,30 @@ +import { ConfigModule } from "@medusajs/types" import { promiseAll, wrapHandler } from "@medusajs/utils" import cors from "cors" -import { type Express, json, Router, text, urlencoded } from "express" +import { Router, json, text, urlencoded, type Express } from "express" import { readdir } from "fs/promises" import { parseCorsOrigins } from "medusa-core-utils" import { extname, join, sep } from "path" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" import { authenticateCustomer, authenticateLegacy, errorHandler, requireCustomerAuthentication, } from "../../../utils/middlewares" -import { MedusaRequest, MedusaResponse } from "../../../types/routing" import logger from "../../logger" import { AsyncRouteHandler, GlobalMiddlewareDescriptor, HTTP_METHODS, MiddlewareRoute, - MiddlewaresConfig, MiddlewareVerb, + MiddlewaresConfig, ParserConfigArgs, RouteConfig, RouteDescriptor, RouteVerb, } from "./types" -import { ConfigModule } from "@medusajs/types" const log = ({ activityId, @@ -610,7 +610,7 @@ export class RoutesLoader { descriptor.route, cors({ origin: parseCorsOrigins( - this.configModule.projectConfig.admin_cors || "" + this.configModule.projectConfig.http.adminCors ), credentials: true, }) @@ -625,7 +625,7 @@ export class RoutesLoader { descriptor.route, cors({ origin: parseCorsOrigins( - this.configModule.projectConfig.auth_cors || "" + this.configModule.projectConfig.http.authCors ), credentials: true, }) @@ -640,7 +640,7 @@ export class RoutesLoader { descriptor.route, cors({ origin: parseCorsOrigins( - this.configModule.projectConfig.store_cors || "" + this.configModule.projectConfig.http.storeCors ), credentials: true, }) diff --git a/packages/medusa/src/loaders/medusa-app.ts b/packages/medusa/src/loaders/medusa-app.ts index 25a38608ab..d4798a86b1 100644 --- a/packages/medusa/src/loaders/medusa-app.ts +++ b/packages/medusa/src/loaders/medusa-app.ts @@ -3,9 +3,6 @@ import { MedusaAppMigrateDown, MedusaAppMigrateUp, MedusaAppOutput, - MedusaModule, - MODULE_PACKAGE_NAMES, - Modules, ModulesDefinition, } from "@medusajs/modules-sdk" import { @@ -188,29 +185,6 @@ export const loadMedusaApp = async ( injectedDependencies, }) - // TODO: Remove this and make it more dynamic on ensuring all modules are loaded. - const requiredModuleKeys = [Modules.PRODUCT, Modules.PRICING] - - const missingPackages: string[] = [] - - for (const requiredModuleKey of requiredModuleKeys) { - const isModuleInstalled = MedusaModule.isInstalled(requiredModuleKey) - - if (!isModuleInstalled) { - missingPackages.push( - MODULE_PACKAGE_NAMES[requiredModuleKey] || requiredModuleKey - ) - } - } - - if (missingPackages.length) { - throw new Error( - `Medusa requires the following packages/module registration: (${missingPackages.join( - ", " - )})` - ) - } - if (!config.registerInContainer) { return medusaApp } diff --git a/packages/medusa/src/loaders/passport.ts b/packages/medusa/src/loaders/passport.ts index ff27bca95f..1610d8156e 100644 --- a/packages/medusa/src/loaders/passport.ts +++ b/packages/medusa/src/loaders/passport.ts @@ -41,7 +41,7 @@ export default async ({ // After a user has authenticated a JWT will be placed on a cookie, all // calls will be authenticated based on the JWT - const { jwt_secret } = configModule.projectConfig + const { http } = configModule.projectConfig passport.use( "admin-session", new CustomStrategy(async (req, done) => { @@ -97,7 +97,7 @@ export default async ({ new JWTStrategy( { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: jwt_secret, + secretOrKey: http.jwtSecret, }, (token, done) => { if (token.domain !== "admin") { @@ -121,7 +121,7 @@ export default async ({ new JWTStrategy( { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: jwt_secret, + secretOrKey: http.jwtSecret, }, (token, done) => { if (token.domain !== "store") { diff --git a/packages/medusa/src/subscribers/payment-webhook.ts b/packages/medusa/src/subscribers/payment-webhook.ts index 8527529336..f96ed8c9f3 100644 --- a/packages/medusa/src/subscribers/payment-webhook.ts +++ b/packages/medusa/src/subscribers/payment-webhook.ts @@ -1,7 +1,7 @@ -import { PaymentWebhookEvents } from "@medusajs/utils" -import { IPaymentModuleService, ProviderWebhookPayload } from "@medusajs/types" -import { SubscriberArgs, SubscriberConfig } from "../types/subscribers" import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPaymentModuleService, ProviderWebhookPayload } from "@medusajs/types" +import { PaymentWebhookEvents } from "@medusajs/utils" +import { SubscriberArgs, SubscriberConfig } from "../types/subscribers" type SerializedBuffer = { data: ArrayBuffer diff --git a/packages/medusa/src/utils/api/http-compression.ts b/packages/medusa/src/utils/api/http-compression.ts index abd5d501ff..1926c981f6 100644 --- a/packages/medusa/src/utils/api/http-compression.ts +++ b/packages/medusa/src/utils/api/http-compression.ts @@ -1,13 +1,8 @@ -import { Request, Response, NextFunction } from "express" +import { HttpCompressionOptions, ProjectConfigOptions } from "@medusajs/types" import compression from "compression" -import { Logger } from "@medusajs/types" -import { - ProjectConfigOptions, - HttpCompressionOptions, -} from "@medusajs/types" +import { Request, Response } from "express" export function shouldCompressResponse(req: Request, res: Response) { - const logger: Logger = req.scope.resolve("logger") const { projectConfig } = req.scope.resolve("configModule") const { enabled } = compressionOptions(projectConfig) @@ -27,9 +22,10 @@ export function shouldCompressResponse(req: Request, res: Response) { export function compressionOptions( config: ProjectConfigOptions ): HttpCompressionOptions { - const responseCompressionOptions = config.http_compression ?? {} + const responseCompressionOptions = config.http.compression ?? {} - responseCompressionOptions.enabled = responseCompressionOptions.enabled ?? false + responseCompressionOptions.enabled = + responseCompressionOptions.enabled ?? false responseCompressionOptions.level = responseCompressionOptions.level ?? 6 responseCompressionOptions.memLevel = responseCompressionOptions.memLevel ?? 8 responseCompressionOptions.threshold = diff --git a/packages/medusa/src/utils/middlewares/authenticate-middleware.ts b/packages/medusa/src/utils/middlewares/authenticate-middleware.ts index 7d5b3d78d3..74e5b23474 100644 --- a/packages/medusa/src/utils/middlewares/authenticate-middleware.ts +++ b/packages/medusa/src/utils/middlewares/authenticate-middleware.ts @@ -1,5 +1,10 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { ApiKeyDTO, AuthUserDTO, IApiKeyModuleService } from "@medusajs/types" +import { + ApiKeyDTO, + AuthUserDTO, + ConfigModule, + IApiKeyModuleService, +} from "@medusajs/types" import { stringEqualsOrRegexMatch } from "@medusajs/utils" import { NextFunction, RequestHandler } from "express" import jwt, { JwtPayload } from "jsonwebtoken" @@ -55,10 +60,11 @@ export const authenticate = ( ) if (!authUser) { - const { jwt_secret } = req.scope.resolve("configModule").projectConfig + const { http } = + req.scope.resolve("configModule").projectConfig authUser = getAuthUserFromJwtToken( req.headers.authorization, - jwt_secret, + http.jwtSecret!, authTypes, authScope ) diff --git a/packages/modules/auth/src/providers/email-password.ts b/packages/modules/auth/src/providers/email-password.ts index 39a9c95a7b..c94a34e9e5 100644 --- a/packages/modules/auth/src/providers/email-password.ts +++ b/packages/modules/auth/src/providers/email-password.ts @@ -8,6 +8,8 @@ import { import { AuthUserService } from "@services" import Scrypt from "scrypt-kdf" +const EXPIRATION = "1d" + class EmailPasswordProvider extends AbstractAuthModuleProvider { public static PROVIDER = "emailpass" public static DISPLAY_NAME = "Email/Password Authentication"