diff --git a/packages/medusa/src/api/index.js b/packages/medusa/src/api/index.js index ccfa33175d..97cdd0d316 100644 --- a/packages/medusa/src/api/index.js +++ b/packages/medusa/src/api/index.js @@ -1,14 +1,12 @@ import { Router } from "express" import admin from "./routes/admin" import store from "./routes/store" -import users from "./routes/users" import errorHandler from "./middlewares/error-handler" // guaranteed to get dependencies export default () => { const app = Router() - users(app) admin(app) store(app) diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index 479bd74a7a..5f1a970994 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -2,6 +2,7 @@ import { Router } from "express" import middlewares from "../../middlewares" import authRoutes from "./auth" import productRoutes from "./products" +import userRoutes from "./users" import productVariantRoutes from "./product-variants" import regionRoutes from "./regions" import shippingOptionRoutes from "./shipping-options" @@ -20,6 +21,7 @@ export default app => { route.use(middlewares.authenticate()) productRoutes(route) + userRoutes(route) regionRoutes(route) shippingOptionRoutes(route) shippingProfileRoutes(route) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/create-user.js b/packages/medusa/src/api/routes/admin/users/__tests__/create-user.js new file mode 100644 index 0000000000..080d3ab6dc --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/create-user.js @@ -0,0 +1,71 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("POST /admin/users", () => { + describe("successfully creates a user", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", "/admin/users", { + payload: { + email: "oliver@test.dk", + password: "123456789", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls UserService create", () => { + expect(UserServiceMock.create).toHaveBeenCalledTimes(1) + expect(UserServiceMock.create).toHaveBeenCalledWith( + { + email: "oliver@test.dk", + }, + "123456789" + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the user", () => { + expect(subject.body._id).toEqual(IdMap.getId("test-user")) + }) + }) + + describe("handles failed create operation", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", "/admin/users", { + payload: { + email: "olivertest.dk", + password: "123456789", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("returns 400 on invalid email", () => { + expect(subject.status).toEqual(400) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/delete-user.js b/packages/medusa/src/api/routes/admin/users/__tests__/delete-user.js new file mode 100644 index 0000000000..691a1900a2 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/delete-user.js @@ -0,0 +1,46 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("DELETE /admin/users/:id", () => { + describe("successfully deletes a user", () => { + let subject + + beforeAll(async () => { + subject = await request( + "DELETE", + `/admin/users/${IdMap.getId("delete-user")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls UserService create", () => { + expect(UserServiceMock.delete).toHaveBeenCalledTimes(1) + expect(UserServiceMock.delete).toHaveBeenCalledWith( + IdMap.getId("delete-user") + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns correct delete data", () => { + expect(subject.body).toEqual({ + id: IdMap.getId("delete-user"), + object: "user", + deleted: true, + }) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/get-user.js b/packages/medusa/src/api/routes/admin/users/__tests__/get-user.js new file mode 100644 index 0000000000..0ebdee2b25 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/get-user.js @@ -0,0 +1,42 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("GET /admin/users/:id", () => { + describe("successfully gets a user", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/admin/users/${IdMap.getId("test-user")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls UserService retrieve", () => { + expect(UserServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(UserServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("test-user") + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns the user", () => { + expect(subject.body._id).toEqual(IdMap.getId("test-user")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/list-users.js b/packages/medusa/src/api/routes/admin/users/__tests__/list-users.js new file mode 100644 index 0000000000..61dd5b1aec --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/list-users.js @@ -0,0 +1,28 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("GET /admin/users", () => { + describe("successful retrieval", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/admin/users`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service retrieve", () => { + expect(UserServiceMock.list).toHaveBeenCalledTimes(1) + expect(UserServiceMock.list).toHaveBeenCalledWith({}) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/reset-password-token.js b/packages/medusa/src/api/routes/admin/users/__tests__/reset-password-token.js new file mode 100644 index 0000000000..cdef1b12f4 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/reset-password-token.js @@ -0,0 +1,40 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("POST /admin/users/:id/reset-password-token", () => { + describe("successfully generates reset password token", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/users/${IdMap.getId("test-user")}/reset-password-token`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls UserService generateResetPasswordToken", () => { + expect(UserServiceMock.generateResetPasswordToken).toHaveBeenCalledTimes( + 1 + ) + expect(UserServiceMock.generateResetPasswordToken).toHaveBeenCalledWith( + IdMap.getId("test-user") + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/reset-password.js b/packages/medusa/src/api/routes/admin/users/__tests__/reset-password.js new file mode 100644 index 0000000000..332e1c8fc5 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/reset-password.js @@ -0,0 +1,56 @@ +import { IdMap } from "medusa-test-utils" +import jwt from "jsonwebtoken" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("POST /admin/users/:id/reset-password", () => { + describe("successfully resets password", () => { + let subject + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("calls UserService setPassword", async () => { + const exp = Math.floor(Date.now() / 1000) + 60 * 15 + const token = jwt.sign( + { + userId: "test-user-id", + name: "Oliver Juhl", + email: "oliver@test.dk", + exp, + }, + "123456789hash" + ) + + subject = await request( + "POST", + `/admin/users/test-user-id/reset-password`, + { + payload: { + token, + password: "new-password", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + expect(UserServiceMock.setPassword).toHaveBeenCalledTimes(1) + expect(UserServiceMock.setPassword).toHaveBeenCalledWith( + "test-user-id", + "new-password" + ) + }) + + it("returns updated user", () => { + expect(subject.body._id).toEqual("test-user-id") + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/set-password.js b/packages/medusa/src/api/routes/admin/users/__tests__/set-password.js new file mode 100644 index 0000000000..22c7cb8d8b --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/set-password.js @@ -0,0 +1,46 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("POST /admin/users/:id/set-password", () => { + describe("successfully sets password", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/users/${IdMap.getId("test-user")}/set-password`, + { + payload: { + password: "987654321", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls UserService setPassword", () => { + expect(UserServiceMock.setPassword).toHaveBeenCalledTimes(1) + expect(UserServiceMock.setPassword).toHaveBeenCalledWith( + IdMap.getId("test-user"), + "987654321" + ) + }) + + it("returns the user", () => { + expect(subject.body._id).toEqual(IdMap.getId("test-user")) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/__tests__/update-user.js b/packages/medusa/src/api/routes/admin/users/__tests__/update-user.js new file mode 100644 index 0000000000..ac58dd4317 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/__tests__/update-user.js @@ -0,0 +1,40 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { UserServiceMock } from "../../../../../services/__mocks__/user" + +describe("POST /admin/users/:id", () => { + describe("successful update", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/users/${IdMap.getId("test-user")}`, + { + payload: { + name: "Oliver Juhl", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service method", () => { + expect(UserServiceMock.update).toHaveBeenCalledTimes(1) + expect(UserServiceMock.update).toHaveBeenCalledWith( + IdMap.getId("test-user"), + { + name: "Oliver Juhl", + } + ) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/users/create-user.js b/packages/medusa/src/api/routes/admin/users/create-user.js new file mode 100644 index 0000000000..a980d8d49a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/create-user.js @@ -0,0 +1,28 @@ +import { Validator, MedusaError } from "medusa-core-utils" +import _ from "lodash" + +export default async (req, res) => { + const schema = Validator.object().keys({ + email: Validator.string() + .email() + .required(), + name: Validator.string().optional(), + password: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const userService = req.scope.resolve("userService") + const data = _.pick(value, ["email", "name"]) + + const user = await userService.create(data, value.password) + + res.status(200).json(user) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/users/delete-user.js b/packages/medusa/src/api/routes/admin/users/delete-user.js new file mode 100644 index 0000000000..05d83125cb --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/delete-user.js @@ -0,0 +1,12 @@ +export default async (req, res) => { + const { user_id } = req.params + + const userService = req.scope.resolve("userService") + await userService.delete(user_id) + + res.status(200).send({ + id: user_id, + object: "user", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/routes/admin/users/get-user.js b/packages/medusa/src/api/routes/admin/users/get-user.js new file mode 100644 index 0000000000..34c8ba1df9 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/get-user.js @@ -0,0 +1,12 @@ +export default async (req, res) => { + const { user_id } = req.params + + try { + const userService = req.scope.resolve("userService") + + const user = await userService.retrieve(user_id) + res.json(user) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/users/index.js b/packages/medusa/src/api/routes/admin/users/index.js new file mode 100644 index 0000000000..9e8b036175 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/index.js @@ -0,0 +1,31 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/users", route) + + route.get("/", middlewares.wrap(require("./list-users").default)) + route.get("/:user_id", middlewares.wrap(require("./get-user").default)) + + route.post("/", middlewares.wrap(require("./create-user").default)) + route.post("/:user_id", middlewares.wrap(require("./update-user").default)) + route.post( + "/:user_id/set-password", + middlewares.wrap(require("./set-password").default) + ) + route.post( + "/:user_id/reset-password-token", + middlewares.wrap(require("./reset-password-token").default) + ) + + route.post( + "/:user_id/reset-password", + middlewares.wrap(require("./reset-password").default) + ) + + route.delete("/:user_id", middlewares.wrap(require("./delete-user").default)) + + return app +} diff --git a/packages/medusa/src/api/routes/admin/users/list-users.js b/packages/medusa/src/api/routes/admin/users/list-users.js new file mode 100644 index 0000000000..2292481060 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/list-users.js @@ -0,0 +1,10 @@ +export default async (req, res) => { + try { + const userService = req.scope.resolve("userService") + const users = await userService.list({}) + + res.status(200).json(users) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/users/reset-password-token.js b/packages/medusa/src/api/routes/admin/users/reset-password-token.js new file mode 100644 index 0000000000..d477503837 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/reset-password-token.js @@ -0,0 +1,14 @@ +export default async (req, res) => { + const { user_id } = req.params + try { + const userService = req.scope.resolve("userService") + const token = await userService.generateResetPasswordToken(user_id) + if (!token) { + res.sendStatus(404) + return + } + res.json(token) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/users/reset-password.js b/packages/medusa/src/api/routes/admin/users/reset-password.js new file mode 100644 index 0000000000..a0e599a85a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/reset-password.js @@ -0,0 +1,33 @@ +import { MedusaError, Validator } from "medusa-core-utils" +import jwt from "jsonwebtoken" + +export default async (req, res) => { + const { user_id } = req.params + const schema = Validator.object().keys({ + token: Validator.string().required(), + password: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const userService = req.scope.resolve("userService") + let user = await userService.retrieve(user_id) + + const decodedToken = await jwt.verify(value.token, user.password_hash) + if (!decodedToken) { + res.status(401).send("Invalid or expired password reset token") + } + + await userService.setPassword(user._id, value.password) + + const updatedUser = await userService.retrieve(user._id) + + res.status(200).json(updatedUser) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/users/set-password.js b/packages/medusa/src/api/routes/admin/users/set-password.js new file mode 100644 index 0000000000..6986ff30f7 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/set-password.js @@ -0,0 +1,24 @@ +import { Validator, MedusaError } from "medusa-core-utils" + +export default async (req, res) => { + const { user_id } = req.params + const schema = Validator.object().keys({ + password: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const userService = req.scope.resolve("userService") + await userService.setPassword(user_id, value.password) + + const newUser = await userService.retrieve(user_id) + + res.json(newUser) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/users/update-user.js b/packages/medusa/src/api/routes/admin/users/update-user.js new file mode 100644 index 0000000000..aab92ecf7d --- /dev/null +++ b/packages/medusa/src/api/routes/admin/users/update-user.js @@ -0,0 +1,26 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const { user_id } = req.params + const schema = Validator.object().keys({ + name: Validator.string().optional(), + api_token: Validator.string().optional(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const userService = req.scope.resolve("userService") + + await userService.update(user_id, value) + const user = await userService.retrieve(user_id) + + res.status(200).json(user) + } catch (err) { + console.log(err) + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/index.js b/packages/medusa/src/api/routes/store/index.js index 12a82ef22f..57515b0b4c 100644 --- a/packages/medusa/src/api/routes/store/index.js +++ b/packages/medusa/src/api/routes/store/index.js @@ -2,7 +2,6 @@ import { Router } from "express" import productRoutes from "./products" import cartRoutes from "./carts" import shippingOptionRoutes from "./shipping-options" -import middlewares from "../../middlewares" const route = Router() diff --git a/packages/medusa/src/api/routes/users/index.js b/packages/medusa/src/api/routes/users/index.js deleted file mode 100644 index 41012cb896..0000000000 --- a/packages/medusa/src/api/routes/users/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Router } from "express" -import middlewares from "../../middlewares" - -const route = Router() - -export default app => { - return app -} diff --git a/packages/medusa/src/models/__mocks__/user.js b/packages/medusa/src/models/__mocks__/user.js index 826aa6e6d4..25995dbee1 100644 --- a/packages/medusa/src/models/__mocks__/user.js +++ b/packages/medusa/src/models/__mocks__/user.js @@ -4,12 +4,12 @@ export const users = { testUser: { _id: IdMap.getId("test-user"), email: "oliver@medusa.test", - passwordHash: "123456789", + password_hash: "123456789", }, permissionUser: { _id: IdMap.getId("permissions-user"), email: "oliver@medusa.com", - passwordHash: "123456789", + password_hash: "123456789", }, } diff --git a/packages/medusa/src/models/user.js b/packages/medusa/src/models/user.js index 12cf6d28c9..41d43504c6 100644 --- a/packages/medusa/src/models/user.js +++ b/packages/medusa/src/models/user.js @@ -10,7 +10,8 @@ class UserModel extends BaseModel { static schema = { email: { type: String, required: true }, name: { type: String }, - password_hash: { type: String }, + password_hash: { type: String, required: true }, + api_token: { type: String }, metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, } } diff --git a/packages/medusa/src/services/__mocks__/user.js b/packages/medusa/src/services/__mocks__/user.js index eebce34d44..be214c6355 100644 --- a/packages/medusa/src/services/__mocks__/user.js +++ b/packages/medusa/src/services/__mocks__/user.js @@ -1,14 +1,64 @@ import bcrypt from "bcrypt" +import { IdMap } from "medusa-test-utils" +import _ from "lodash" export const users = { - user1: { + testUser: { + _id: IdMap.getId("test-user"), email: "oliver@test.dk", - password_hash: "123456789", - api_token: "123456789", + password_hash: "hashed123456789", + }, + jwtUser: { + _id: "test-user-id", + email: "oliver@test.dk", + password_hash: "123456789hash", + }, + deleteUser: { + _id: IdMap.getId("delete-user"), + email: "oliver@deletetest.dk", + password_hash: "hashed123456789", }, } export const UserServiceMock = { + create: jest.fn().mockImplementation(data => { + if (data.email === "oliver@test.dk") { + return Promise.resolve(users.testUser) + } + return Promise.resolve(undefined) + }), + update: jest.fn().mockReturnValue(Promise.resolve()), + list: jest.fn().mockReturnValue(Promise.resolve([])), + delete: jest.fn().mockImplementation(data => { + if (data === IdMap.getId("delete-user")) { + return Promise.resolve({ + id: IdMap.getId("delete-user"), + object: "user", + deleted: true, + }) + } + return Promise.resolve(undefined) + }), + retrieve: jest.fn().mockImplementation(userId => { + if (userId === IdMap.getId("test-user")) { + return Promise.resolve(users.testUser) + } + // used for jwt token tests + if (userId === "test-user-id") { + return Promise.resolve(users.jwtUser) + } + return Promise.resolve(undefined) + }), + setPassword: jest.fn().mockImplementation(userId => { + return Promise.resolve() + }), + decorate: jest.fn().mockImplementation((user, fields) => { + user.decorated = true + return user + }), + generateResetPasswordToken: jest + .fn() + .mockReturnValue(Promise.resolve("JSONWEBTOKEN")), retrieveByApiToken: jest.fn().mockImplementation(token => { if (token === "123456789") { return Promise.resolve(users.user1) diff --git a/packages/medusa/src/services/__tests__/auth.js b/packages/medusa/src/services/__tests__/auth.js index 02f6cbd88b..71d0174f6d 100644 --- a/packages/medusa/src/services/__tests__/auth.js +++ b/packages/medusa/src/services/__tests__/auth.js @@ -1,4 +1,3 @@ -import bcrypt from "bcrypt" import AuthService from "../auth" import { users, UserServiceMock } from "../__mocks__/user" diff --git a/packages/medusa/src/services/__tests__/user.js b/packages/medusa/src/services/__tests__/user.js index e1576a7e52..c68991639d 100644 --- a/packages/medusa/src/services/__tests__/user.js +++ b/packages/medusa/src/services/__tests__/user.js @@ -16,7 +16,7 @@ describe("UserService", () => { result = await userService.retrieve(IdMap.getId("test-user")) }) - it("calls cart model functions", () => { + it("calls user model functions", () => { expect(UserModelMock.findOne).toHaveBeenCalledTimes(1) expect(UserModelMock.findOne).toHaveBeenCalledWith({ _id: IdMap.getId("test-user"), @@ -28,6 +28,60 @@ describe("UserService", () => { }) }) + describe("create", () => { + let result + + const userService = new UserService({ + userModel: UserModelMock, + }) + + beforeAll(async () => { + jest.clearAllMocks() + }) + + it("calls user model functions", async () => { + result = await userService.create( + { + email: "oliver@test.dk", + name: "Oliver", + password_hash: "hashedpassword", + }, + "password" + ) + expect(UserModelMock.create).toHaveBeenCalledTimes(1) + expect(UserModelMock.create).toHaveBeenCalledWith({ + email: "oliver@test.dk", + name: "Oliver", + password_hash: expect.stringMatching( + /^\$2[aby]?\$[\d]+\$[./A-Za-z0-9]{53}$/ + ), + }) + }) + }) + + describe("update", () => { + const userService = new UserService({ + userModel: UserModelMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("calls updateOne with correct params", async () => { + await userService.update(IdMap.getId("test-user"), { + name: "new name", + }) + + expect(UserModelMock.updateOne).toBeCalledTimes(1) + expect(UserModelMock.updateOne).toBeCalledWith( + { _id: IdMap.getId("test-user") }, + { $set: { name: "new name" } }, + { runValidators: true } + ) + }) + }) + describe("setMetadata", () => { const userService = new UserService({ userModel: UserModelMock, diff --git a/packages/medusa/src/services/user.js b/packages/medusa/src/services/user.js index db004b3731..e61a712431 100644 --- a/packages/medusa/src/services/user.js +++ b/packages/medusa/src/services/user.js @@ -135,14 +135,56 @@ class UserService extends BaseService { * @param {object} user - the user to create * @return {Promise} the result of create */ - create(user) { + async create(user, password) { const validatedEmail = this.validateEmail_(user.email) + const hashedPassword = await bcrypt.hash(password, 10) user.email = validatedEmail + user.password_hash = hashedPassword return this.userModel_.create(user).catch(err => { throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) }) } + /** + * Updates a user. + * @param {object} user - the user to create + * @return {Promise} the result of create + */ + async update(userId, update) { + const validatedId = this.validateId_(userId) + + if (update.password_hash) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Use dedicated methods, `setPassword`, `generateResetPasswordToken` for password operations" + ) + } + + if (update.email) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "You are not allowed to update email" + ) + } + + if (update.metadata) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Use setMetadata to update metadata fields" + ) + } + + return this.userModel_ + .updateOne( + { _id: validatedId }, + { $set: update }, + { runValidators: true } + ) + .catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } + /** * Deletes a user from a given user id. * @param {string} userId - the id of the user to delete. Must be @@ -177,7 +219,7 @@ class UserService extends BaseService { const hashedPassword = await bcrypt.hash(password, 10) if (!hashedPassword) { throw new MedusaError( - MedusaError.Types.INVALID_DATA, + MedusaError.Types.DB_ERROR, `An error occured while hashing password` ) } @@ -199,7 +241,7 @@ class UserService extends BaseService { */ async generateResetPasswordToken(userId) { const user = await this.retrieve(userId) - const secret = user.passwordHash + const secret = user.password_hash const expiry = Math.floor(Date.now() / 1000) + 60 * 15 const payload = { userId: user._id, exp: expiry } const token = jwt.sign(payload, secret)