diff --git a/.changeset/perfect-geckos-hunt.md b/.changeset/perfect-geckos-hunt.md new file mode 100644 index 0000000000..9ca3823a80 --- /dev/null +++ b/.changeset/perfect-geckos-hunt.md @@ -0,0 +1,7 @@ +--- +"medusa-react": patch +"@medusajs/medusa": patch +"@medusajs/utils": patch +--- + +feat(medusa-react,medusa,utils): fix login for medusa v2 admin next dashboard diff --git a/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts b/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts new file mode 100644 index 0000000000..e0518d1e0e --- /dev/null +++ b/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts @@ -0,0 +1,135 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IAuthModuleService, ICustomerModuleService } from "@medusajs/types" +import path from "path" +import Scrypt from "scrypt-kdf" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("POST /auth/emailpass", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + const password = "supersecret" + const email = "test@test.com" + + it("should return a token on successful login", async () => { + const passwordHash = ( + await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 }) + ).toString("base64") + const authService: IAuthModuleService = appContainer.resolve( + ModuleRegistrationName.AUTH + ) + + await authService.create({ + provider: "emailpass", + entity_id: email, + scope: "admin", + provider_metadata: { + password: passwordHash, + }, + }) + + const api = useApi() as any + const response = await api + .post(`/auth/admin/emailpass`, { + email: email, + password: password, + }) + .catch((e) => e) + + expect(response.status).toEqual(200) + expect(response.data).toEqual( + expect.objectContaining({ + token: expect.any(String), + }) + ) + }) + + it("should throw an error upon incorrect password", async () => { + const passwordHash = ( + await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 }) + ).toString("base64") + const authService: IAuthModuleService = appContainer.resolve( + ModuleRegistrationName.AUTH + ) + + await authService.create({ + provider: "emailpass", + entity_id: email, + scope: "admin", + provider_metadata: { + password: passwordHash, + }, + }) + + const api = useApi() as any + const error = await api + .post(`/auth/admin/emailpass`, { + email: email, + password: "incorrect-password", + }) + .catch((e) => e) + + expect(error.response.status).toEqual(401) + expect(error.response.data).toEqual({ + type: "unauthorized", + message: "Invalid email or password", + }) + }) + + it.skip("should throw an error upon logging in with a non existing auth user", async () => { + const passwordHash = ( + await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 }) + ).toString("base64") + + const api = useApi() as any + const error = await api + .post(`/auth/admin/emailpass`, { + email: "should-not-exist", + password: "should-not-exist", + }) + .catch((e) => e) + + // TODO: This is creating a user with a scope of admin. The client consuming the auth service + // should reject this if its not being created by an admin user + expect(error.response.status).toEqual(401) + expect(error.response.data).toEqual({ + type: "unauthorized", + message: "Invalid email or password", + }) + }) +}) diff --git a/integration-tests/modules/__tests__/users/get-me.spec.ts b/integration-tests/modules/__tests__/users/get-me.spec.ts new file mode 100644 index 0000000000..e3ac8530a9 --- /dev/null +++ b/integration-tests/modules/__tests__/users/get-me.spec.ts @@ -0,0 +1,51 @@ +import { initDb, useDb } from "../../../environment-helpers/use-db" + +import { AxiosInstance } from "axios" +import path from "path" +import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../environment-helpers/use-api" +import { createAdminUser } from "../../helpers/create-admin-user" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("POST /admin/users/me", () => { + let dbConnection + let shutdownServer + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("gets the current user", async () => { + const api = useApi()! as AxiosInstance + + const response = await api.get(`/admin/users/me`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data).toEqual({ + user: expect.objectContaining({ id: "admin_user" }), + }) + }) +}) diff --git a/integration-tests/modules/medusa-config.js b/integration-tests/modules/medusa-config.js index e0d5289d41..0aa1798f22 100644 --- a/integration-tests/modules/medusa-config.js +++ b/integration-tests/modules/medusa-config.js @@ -5,6 +5,7 @@ const DB_PASSWORD = process.env.DB_PASSWORD const DB_NAME = process.env.DB_TEMP_NAME const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}` process.env.POSTGRES_URL = DB_URL +process.env.LOG_LEVEL = "error" const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true" diff --git a/packages/admin/package.json b/packages/admin/package.json index 9faf03af40..2340019de1 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -35,7 +35,7 @@ "@rollup/plugin-replace": "5.0.2", "@rollup/plugin-virtual": "^3.0.1", "commander": "^10.0.0", - "dotenv": "16.3.1", + "dotenv": "16.4.5", "esbuild": "0.17.18", "express": "4.18.2", "fs-extra": "11.1.0", diff --git a/packages/auth/package.json b/packages/auth/package.json index 4516db7edd..3b2a9dd550 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "16.3.1", + "dotenv": "16.4.5", "jsonwebtoken": "^9.0.2", "knex": "2.4.2", "scrypt-kdf": "^2.0.1", diff --git a/packages/medusa/src/api-v2/admin/users/me/route.ts b/packages/medusa/src/api-v2/admin/users/me/route.ts new file mode 100644 index 0000000000..7839785091 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/users/me/route.ts @@ -0,0 +1,38 @@ +import { + ContainerRegistrationKeys, + MedusaError, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.auth.app_metadata.user_id + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + if (!id) { + throw new MedusaError(MedusaError.Types.NOT_FOUND, `User ID not found`) + } + + const query = remoteQueryObjectFromString({ + entryPoint: "user", + variables: { id }, + fields: req.retrieveConfig.select as string[], + }) + + const [user] = await remoteQuery(query) + + if (!user) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `User with id: ${id} was not found` + ) + } + + res.status(200).json({ user }) +} diff --git a/packages/medusa/src/api-v2/admin/users/middlewares.ts b/packages/medusa/src/api-v2/admin/users/middlewares.ts index a70015471f..4694622926 100644 --- a/packages/medusa/src/api-v2/admin/users/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/users/middlewares.ts @@ -1,12 +1,12 @@ import * as QueryConfig from "./query-config" +import { transformBody, transformQuery } from "../../../api/middlewares" import { AdminCreateUserRequest, AdminGetUsersParams, AdminGetUsersUserParams, AdminUpdateUserRequest, } from "./validators" -import { transformBody, transformQuery } from "../../../api/middlewares" import { MiddlewareRoute } from "../../../types/middlewares" import { authenticate } from "../../../utils/authenticate-middleware" @@ -39,6 +39,16 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["GET"], + matcher: "/admin/users/me", + middlewares: [ + transformQuery( + AdminGetUsersUserParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, { method: ["POST"], matcher: "/admin/users/:id", diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts index 23f3ff87bd..5261b5494a 100644 --- a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts +++ b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts @@ -1,7 +1,7 @@ -import jwt from "jsonwebtoken" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" import { MedusaError } from "@medusajs/utils" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import jwt from "jsonwebtoken" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { @@ -23,6 +23,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const authResult = await service.authenticate(authProvider, authData) const { success, error, authUser, location } = authResult + if (location) { res.redirect(location) return @@ -30,7 +31,6 @@ 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) return res.status(200).json({ token }) diff --git a/packages/medusa/src/loaders/api.ts b/packages/medusa/src/loaders/api.ts index 478d6c2f47..3f8f1b56a7 100644 --- a/packages/medusa/src/loaders/api.ts +++ b/packages/medusa/src/loaders/api.ts @@ -1,12 +1,12 @@ -import path from "path" import { FeatureFlagUtils, FlagRouter } from "@medusajs/utils" import { AwilixContainer } from "awilix" import bodyParser from "body-parser" import { Express } from "express" +import path from "path" import qs from "qs" -import { RoutesLoader } from "./helpers/routing" import routes from "../api" import { ConfigModule } from "../types/global" +import { RoutesLoader } from "./helpers/routing" type Options = { app: Express diff --git a/packages/medusa/src/loaders/helpers/routing/index.ts b/packages/medusa/src/loaders/helpers/routing/index.ts index ab8d31749f..dee0a94c18 100644 --- a/packages/medusa/src/loaders/helpers/routing/index.ts +++ b/packages/medusa/src/loaders/helpers/routing/index.ts @@ -307,6 +307,7 @@ export class RoutesLoader { shouldRequireAdminAuth: false, shouldRequireCustomerAuth: false, shouldAppendCustomer: false, + shouldAppendAuthCors: false, } /** @@ -343,6 +344,10 @@ export class RoutesLoader { } } + if (route.startsWith("/auth") && shouldAddCors) { + config.shouldAppendAuthCors = true + } + if (shouldRequireAuth && route.startsWith("/store/me")) { config.shouldRequireCustomerAuth = shouldRequireAuth } @@ -612,6 +617,21 @@ export class RoutesLoader { ) } + if (descriptor.config.shouldAppendAuthCors) { + /** + * Apply the auth cors + */ + this.router.use( + descriptor.route, + cors({ + origin: parseCorsOrigins( + this.configModule.projectConfig.auth_cors || "" + ), + credentials: true, + }) + ) + } + if (descriptor.config.shouldAppendStoreCors) { /** * Apply the store cors diff --git a/packages/medusa/src/loaders/helpers/routing/types.ts b/packages/medusa/src/loaders/helpers/routing/types.ts index 63d691d1bc..5871e386f6 100644 --- a/packages/medusa/src/loaders/helpers/routing/types.ts +++ b/packages/medusa/src/loaders/helpers/routing/types.ts @@ -41,6 +41,7 @@ export type RouteConfig = { shouldAppendCustomer?: boolean shouldAppendAdminCors?: boolean shouldAppendStoreCors?: boolean + shouldAppendAuthCors?: boolean routes?: RouteImplementation[] } diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 91331f436a..e96bf6198e 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -3,13 +3,9 @@ import { ModulesDefinition, } from "@medusajs/modules-sdk" import { MODULE_RESOURCE_TYPE } from "@medusajs/types" -import { Express, NextFunction, Request, Response } from "express" - -import databaseLoader, { dataSource } from "./database" -import pluginsLoader, { registerPluginModels } from "./plugins" - import { ContainerRegistrationKeys, isString } from "@medusajs/utils" import { asValue } from "awilix" +import { Express, NextFunction, Request, Response } from "express" import { createMedusaContainer } from "medusa-core-utils" import { track } from "medusa-telemetry" import { EOL } from "os" @@ -19,6 +15,7 @@ import { v4 } from "uuid" import { MedusaContainer } from "../types/global" import apiLoader from "./api" import loadConfig from "./config" +import databaseLoader, { dataSource } from "./database" import defaultsLoader from "./defaults" import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" @@ -27,6 +24,7 @@ import loadMedusaApp, { mergeDefaultModules } from "./medusa-app" import modelsLoader from "./models" import passportLoader from "./passport" import pgConnectionLoader from "./pg-connection" +import pluginsLoader, { registerPluginModels } from "./plugins" import redisLoader from "./redis" import repositoriesLoader from "./repositories" import searchIndexLoader from "./search-index" diff --git a/packages/medusa/src/utils/authenticate-middleware.ts b/packages/medusa/src/utils/authenticate-middleware.ts index 5f068da96e..112afb856d 100644 --- a/packages/medusa/src/utils/authenticate-middleware.ts +++ b/packages/medusa/src/utils/authenticate-middleware.ts @@ -1,16 +1,13 @@ -import { AuthUserDTO } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ApiKeyDTO, AuthUserDTO, IApiKeyModuleService } from "@medusajs/types" +import { stringEqualsOrRegexMatch } from "@medusajs/utils" +import { NextFunction, RequestHandler } from "express" +import jwt, { JwtPayload } from "jsonwebtoken" import { AuthenticatedMedusaRequest, MedusaRequest, MedusaResponse, } from "../types/routing" -import { NextFunction, RequestHandler } from "express" -import jwt, { JwtPayload } from "jsonwebtoken" - -import { stringEqualsOrRegexMatch } from "@medusajs/utils" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IApiKeyModuleService } from "@medusajs/types" -import { ApiKeyDTO } from "@medusajs/types" const SESSION_AUTH = "session" const BEARER_AUTH = "bearer" @@ -68,6 +65,7 @@ export const authenticate = ( } const isMedusaScope = isAdminScope(authScope) || isStoreScope(authScope) + const isRegistered = !isMedusaScope || (authUser?.app_metadata?.user_id && @@ -85,6 +83,7 @@ export const authenticate = ( app_metadata: authUser.app_metadata, scope: authUser.scope, } + return next() } diff --git a/packages/pricing/package.json b/packages/pricing/package.json index a880e65e61..1753abcb36 100644 --- a/packages/pricing/package.json +++ b/packages/pricing/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.4.5", "knex": "2.4.2" } } diff --git a/packages/product/package.json b/packages/product/package.json index 3a002e1c64..bffddac60e 100644 --- a/packages/product/package.json +++ b/packages/product/package.json @@ -57,7 +57,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.4.5", "knex": "2.4.2", "lodash": "^4.17.21" } diff --git a/packages/promotion/package.json b/packages/promotion/package.json index 80f1246d4a..2be5a3d652 100644 --- a/packages/promotion/package.json +++ b/packages/promotion/package.json @@ -50,12 +50,12 @@ "dependencies": { "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", - "@medusajs/utils": "^1.11.2", + "@medusajs/utils": "1.11.6", "@mikro-orm/core": "5.9.7", "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.3.1", "knex": "2.4.2" } } diff --git a/packages/types/src/common/config-module.ts b/packages/types/src/common/config-module.ts index f900d8af11..29a4903f10 100644 --- a/packages/types/src/common/config-module.ts +++ b/packages/types/src/common/config-module.ts @@ -1,10 +1,11 @@ -import { RedisOptions } from "ioredis" -import { LoggerOptions } from "typeorm" import { ExternalModuleDeclaration, InternalModuleDeclaration, } from "../modules-sdk" +import { LoggerOptions } from "typeorm" +import { RedisOptions } from "ioredis" + /** * @interface * @@ -174,6 +175,7 @@ export type ProjectConfigOptions = { * ``` */ admin_cors?: string + 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. * diff --git a/packages/user/package.json b/packages/user/package.json index 8067caddaa..d18beae587 100644 --- a/packages/user/package.json +++ b/packages/user/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "16.3.1", + "dotenv": "16.4.5", "jsonwebtoken": "^9.0.2", "knex": "2.4.2" } diff --git a/yarn.lock b/yarn.lock index 6f50670c7c..ffb42f6d9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7918,7 +7918,7 @@ __metadata: "@rollup/plugin-virtual": ^3.0.1 "@types/express": ^4.17.13 commander: ^10.0.0 - dotenv: 16.3.1 + dotenv: 16.4.5 esbuild: 0.17.18 express: 4.18.2 fs-extra: 11.1.0 @@ -7975,7 +7975,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: 16.3.1 + dotenv: 16.4.5 jest: ^29.6.3 jsonwebtoken: ^9.0.2 knex: 2.4.2 @@ -8675,7 +8675,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.4.5 jest: ^29.6.3 knex: 2.4.2 medusa-test-utils: ^1.1.41 @@ -8702,7 +8702,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.4.5 faker: ^6.6.6 jest: ^29.6.3 knex: 2.4.2 @@ -8725,14 +8725,14 @@ __metadata: dependencies: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 - "@medusajs/utils": ^1.11.2 + "@medusajs/utils": 1.11.6 "@mikro-orm/cli": 5.9.7 "@mikro-orm/core": 5.9.7 "@mikro-orm/migrations": 5.9.7 "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.3.1 jest: ^29.6.3 knex: 2.4.2 medusa-test-utils: ^1.1.40 @@ -9014,7 +9014,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: 16.3.1 + dotenv: 16.4.5 jest: ^29.6.3 jsonwebtoken: ^9.0.2 knex: 2.4.2 @@ -9029,7 +9029,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/utils@^1.1.41, @medusajs/utils@^1.10.5, @medusajs/utils@^1.11.1, @medusajs/utils@^1.11.2, @medusajs/utils@^1.11.3, @medusajs/utils@^1.11.4, @medusajs/utils@^1.11.5, @medusajs/utils@^1.11.6, @medusajs/utils@^1.9.4, @medusajs/utils@workspace:^, @medusajs/utils@workspace:packages/utils": +"@medusajs/utils@1.11.6, @medusajs/utils@^1.1.41, @medusajs/utils@^1.10.5, @medusajs/utils@^1.11.1, @medusajs/utils@^1.11.2, @medusajs/utils@^1.11.3, @medusajs/utils@^1.11.4, @medusajs/utils@^1.11.5, @medusajs/utils@^1.11.6, @medusajs/utils@^1.9.4, @medusajs/utils@workspace:^, @medusajs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@medusajs/utils@workspace:packages/utils" dependencies: @@ -25802,6 +25802,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f + languageName: node + linkType: hard + "dotenv@npm:^7.0.0": version: 7.0.0 resolution: "dotenv@npm:7.0.0"