Adds endpoints to manage customers (#54)
This commit is contained in:
@@ -3,10 +3,6 @@ export default async (req, res) => {
|
||||
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
|
||||
|
||||
37
packages/medusa/src/api/routes/store/auth/create-session.js
Normal file
37
packages/medusa/src/api/routes/store/auth/create-session.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import jwt from "jsonwebtoken"
|
||||
import { Validator } from "medusa-core-utils"
|
||||
import config from "../../../../config"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { body } = req
|
||||
const schema = Validator.object().keys({
|
||||
email: Validator.string().required(),
|
||||
password: Validator.string().required(),
|
||||
})
|
||||
const { value, error } = schema.validate(body)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const authService = req.scope.resolve("authService")
|
||||
const result = await authService.authenticateCustomer(
|
||||
value.email,
|
||||
value.password
|
||||
)
|
||||
if (!result.success) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
// Add JWT to cookie
|
||||
req.session.jwt = jwt.sign(
|
||||
{ customer_id: result.user._id },
|
||||
config.jwtSecret,
|
||||
{
|
||||
expiresIn: "30d",
|
||||
}
|
||||
)
|
||||
|
||||
res.json(result.customer)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
|
||||
|
||||
describe("POST /store/customers", () => {
|
||||
describe("successfully creates a customer", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/store/customers`, {
|
||||
payload: {
|
||||
email: "lebron@james.com",
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
password: "TheGame",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls CustomerService create", () => {
|
||||
expect(CustomerServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.create).toHaveBeenCalledWith({
|
||||
email: "lebron@james.com",
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
password: "TheGame",
|
||||
})
|
||||
})
|
||||
|
||||
it("returns customer decorated", () => {
|
||||
expect(subject.body.email).toEqual("lebron@james.com")
|
||||
expect(subject.body.decorated).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if missing field", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/store/customers`, {
|
||||
payload: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
password: "TheGame",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns product decorated", () => {
|
||||
expect(subject.body.name).toEqual("invalid_data")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
|
||||
|
||||
describe("POST /store/customers/password-token", () => {
|
||||
describe("successfully creates a customer", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/store/customers/password-token`, {
|
||||
payload: {
|
||||
email: "lebron@james.com",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls CustomerService retrieve", () => {
|
||||
expect(CustomerServiceMock.retrieveByEmail).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.retrieveByEmail).toHaveBeenCalledWith(
|
||||
"lebron@james.com"
|
||||
)
|
||||
})
|
||||
|
||||
it("calls CustomerService retrieve", () => {
|
||||
expect(
|
||||
CustomerServiceMock.generateResetPasswordToken
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
CustomerServiceMock.generateResetPasswordToken
|
||||
).toHaveBeenCalledWith(IdMap.getId("lebron"))
|
||||
})
|
||||
|
||||
it("returns customer decorated", () => {
|
||||
expect(subject.status).toEqual(204)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
|
||||
|
||||
describe("POST /store/customers/password-reset", () => {
|
||||
describe("successfully creates a customer", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/store/customers/password-reset`, {
|
||||
payload: {
|
||||
email: "lebron@james.com",
|
||||
token: jwt.sign({ customer_id: IdMap.getId("lebron") }, "1234"),
|
||||
password: "TheGame",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls CustomerService create", () => {
|
||||
expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{
|
||||
password: "TheGame",
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns customer decorated", () => {
|
||||
expect(subject.body.email).toEqual("lebron@james.com")
|
||||
expect(subject.body.decorated).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if id in webtoken not matching", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/store/customers/password-reset`, {
|
||||
payload: {
|
||||
email: "lebron@james.com",
|
||||
token: jwt.sign({ customer_id: IdMap.getId("not-lebron") }, "1234"),
|
||||
password: "TheGame",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("fails", () => {
|
||||
expect(subject.status).toEqual(401)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
|
||||
|
||||
describe("POST /store/customers/:id", () => {
|
||||
describe("successfully updates a customer", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/customers/${IdMap.getId("lebron")}`,
|
||||
{
|
||||
payload: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
},
|
||||
clientSession: {
|
||||
jwt: {
|
||||
customer_id: IdMap.getId("lebron"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls CustomerService create", () => {
|
||||
expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns product decorated", () => {
|
||||
expect(subject.body.first_name).toEqual("LeBron")
|
||||
expect(subject.body.decorated).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if not authenticated", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/customers/${IdMap.getId("customer1")}`,
|
||||
{
|
||||
payload: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
},
|
||||
clientSession: {
|
||||
jwt: {
|
||||
customer_id: IdMap.getId("lebron"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("status code 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { CustomerServiceMock } from "../../../../../services/__mocks__/customer"
|
||||
|
||||
describe("POST /store/customers/:id/password", () => {
|
||||
describe("successfully updates a customer", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/customers/${IdMap.getId("lebron")}/password`,
|
||||
{
|
||||
payload: {
|
||||
password: "NewPass",
|
||||
},
|
||||
clientSession: {
|
||||
jwt: {
|
||||
customer_id: IdMap.getId("lebron"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls CustomerService update", () => {
|
||||
expect(CustomerServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{
|
||||
password: "NewPass",
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns product decorated", () => {
|
||||
expect(subject.body.first_name).toEqual("LeBron")
|
||||
expect(subject.body.decorated).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if not authenticated", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/customers/${IdMap.getId("customer1")}/password`,
|
||||
{
|
||||
payload: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
},
|
||||
clientSession: {
|
||||
jwt: {
|
||||
customer_id: IdMap.getId("lebron"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("status code 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res, next, id) => {
|
||||
if (!(req.user && req.user.customer_id === id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"You must be logged in to update"
|
||||
)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
email: Validator.string()
|
||||
.email()
|
||||
.required(),
|
||||
first_name: Validator.string().required(),
|
||||
last_name: 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 customerService = req.scope.resolve("customerService")
|
||||
const customer = await customerService.create(value)
|
||||
|
||||
const data = await customerService.decorate(customer)
|
||||
res.status(201).json(data)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
31
packages/medusa/src/api/routes/store/customers/index.js
Normal file
31
packages/medusa/src/api/routes/store/customers/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/customers", route)
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-customer").default))
|
||||
route.post(
|
||||
"/password-reset",
|
||||
middlewares.wrap(require("./reset-password").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/password-token",
|
||||
middlewares.wrap(require("./reset-password-token").default)
|
||||
)
|
||||
|
||||
// Authenticated endpoints
|
||||
route.use(middlewares.authenticate())
|
||||
|
||||
route.param("id", middlewares.wrap(require("./authorize-customer").default))
|
||||
|
||||
route.post("/:id", middlewares.wrap(require("./update-customer").default))
|
||||
route.post(
|
||||
"/:id/password",
|
||||
middlewares.wrap(require("./update-password").default)
|
||||
)
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
email: Validator.string()
|
||||
.email()
|
||||
.required(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
const customer = await customerService.retrieveByEmail(value.email)
|
||||
|
||||
await customerService.generateResetPasswordToken(customer._id)
|
||||
|
||||
res.sendStatus(204)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
email: Validator.string()
|
||||
.email()
|
||||
.required(),
|
||||
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 customerService = req.scope.resolve("customerService")
|
||||
const customer = await customerService.retrieveByEmail(value.email)
|
||||
|
||||
const decodedToken = await jwt.verify(value.token, customer.password_hash)
|
||||
if (!decodedToken || decodedToken.customer_id !== customer._id) {
|
||||
res.status(401).send("Invalid or expired password reset token")
|
||||
}
|
||||
|
||||
await customerService.update(customer._id, { password: value.password })
|
||||
|
||||
const updated = await customerService.retrieve(customer._id)
|
||||
const data = await customerService.decorate(customer)
|
||||
res.status(200).json(data)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
first_name: Validator.string(),
|
||||
last_name: Validator.string(),
|
||||
password: Validator.string(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
await customerService.update(id, value)
|
||||
|
||||
const customer = await customerService.retrieve(id)
|
||||
const data = await customerService.decorate(customer)
|
||||
res.status(200).json(data)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { 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 customerService = req.scope.resolve("customerService")
|
||||
|
||||
await customerService.update(id, value)
|
||||
|
||||
const customer = await customerService.retrieve(id)
|
||||
const data = await customerService.decorate(customer)
|
||||
res.status(200).json(data)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Router } from "express"
|
||||
|
||||
import productRoutes from "./products"
|
||||
import cartRoutes from "./carts"
|
||||
import customerRoutes from "./customers"
|
||||
import shippingOptionRoutes from "./shipping-options"
|
||||
|
||||
const route = Router()
|
||||
@@ -8,6 +10,7 @@ const route = Router()
|
||||
export default app => {
|
||||
app.use("/store", route)
|
||||
|
||||
customerRoutes(route)
|
||||
productRoutes(route)
|
||||
cartRoutes(route)
|
||||
shippingOptionRoutes(route)
|
||||
|
||||
@@ -69,6 +69,16 @@ export async function request(method, url, opts = {}) {
|
||||
// console.log(sessions.util.decode(adminSessionOpts, opts.headers.Cookie))
|
||||
}
|
||||
if (opts.clientSession) {
|
||||
if (opts.clientSession.jwt) {
|
||||
opts.clientSession.jwt = jwt.sign(
|
||||
opts.clientSession.jwt,
|
||||
config.jwtSecret,
|
||||
{
|
||||
expiresIn: "30d",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
headers.Cookie +=
|
||||
clientSessionOpts.cookieName +
|
||||
"=" +
|
||||
|
||||
@@ -26,6 +26,9 @@ export const CustomerModelMock = {
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query.email === "oliver@medusa.com") {
|
||||
return Promise.resolve(customers.testCustomer)
|
||||
}
|
||||
if (query._id === IdMap.getId("testCustomer")) {
|
||||
return Promise.resolve(customers.testCustomer)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ class CustomerModel extends BaseModel {
|
||||
first_name: { type: String, required: true },
|
||||
last_name: { type: String, required: true },
|
||||
billing_address: { type: AddressSchema },
|
||||
password_hash: { type: String },
|
||||
has_account: { type: Boolean, default: false },
|
||||
orders: { type: [String], default: [] },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
}
|
||||
}
|
||||
|
||||
51
packages/medusa/src/services/__mocks__/customer.js
Normal file
51
packages/medusa/src/services/__mocks__/customer.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import bcrypt from "bcrypt"
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const CustomerServiceMock = {
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
update: jest.fn().mockImplementation((id, data) => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
decorate: jest.fn().mockImplementation(data => {
|
||||
let d = Object.assign({}, data)
|
||||
d.decorated = true
|
||||
return d
|
||||
}),
|
||||
generateResetPasswordToken: jest.fn().mockImplementation(id => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(id => {
|
||||
if (id === IdMap.getId("lebron")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("lebron"),
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
email: "lebron@james.com",
|
||||
password_hash: "1234",
|
||||
})
|
||||
}
|
||||
}),
|
||||
retrieveByEmail: jest.fn().mockImplementation(email => {
|
||||
if (email === "lebron@james.com") {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("lebron"),
|
||||
email,
|
||||
password_hash: "1234",
|
||||
})
|
||||
}
|
||||
if (email === "oliver@test.dk") {
|
||||
return bcrypt
|
||||
.hash("123456789", 10)
|
||||
.then(hash => ({ email, password_hash: hash }))
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return CustomerServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -1,5 +1,6 @@
|
||||
import AuthService from "../auth"
|
||||
import { users, UserServiceMock } from "../__mocks__/user"
|
||||
import { customers, CustomerServiceMock } from "../__mocks__/customer"
|
||||
|
||||
describe("AuthService", () => {
|
||||
describe("authenticate", () => {
|
||||
@@ -31,6 +32,35 @@ describe("AuthService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("authenticateCustomer", () => {
|
||||
let authService
|
||||
authService = new AuthService({ customerService: CustomerServiceMock })
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns success and user when passwords match", async () => {
|
||||
const result = await authService.authenticateCustomer(
|
||||
"oliver@test.dk",
|
||||
"123456789"
|
||||
)
|
||||
|
||||
expect(result.success).toEqual(true)
|
||||
expect(result.customer.email).toEqual("oliver@test.dk")
|
||||
})
|
||||
|
||||
it("returns failure when passwords don't match", async () => {
|
||||
const result = await authService.authenticateCustomer(
|
||||
"oliver@test.dk",
|
||||
"invalid-password"
|
||||
)
|
||||
|
||||
expect(result.success).toEqual(false)
|
||||
expect(result.error).toEqual("Invalid email or password")
|
||||
expect(result.customer).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("authenticateAPIToken", () => {
|
||||
let authService
|
||||
authService = new AuthService({ userService: UserServiceMock })
|
||||
|
||||
@@ -26,6 +26,28 @@ describe("CustomerService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveByEmail", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
result = await customerService.retrieveByEmail("oliver@medusa.com")
|
||||
})
|
||||
|
||||
it("calls customer model functions", () => {
|
||||
expect(CustomerModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerModelMock.findOne).toHaveBeenCalledWith({
|
||||
email: "oliver@medusa.com",
|
||||
})
|
||||
})
|
||||
|
||||
it("returns the customer", () => {
|
||||
expect(result).toEqual(customers.testCustomer)
|
||||
})
|
||||
})
|
||||
|
||||
describe("setMetadata", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
@@ -68,8 +90,8 @@ describe("CustomerService", () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls model layer create", () => {
|
||||
customerService.create({
|
||||
it("calls model layer create", async () => {
|
||||
await customerService.create({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
@@ -83,20 +105,38 @@ describe("CustomerService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("fails if email is in incorrect format", () => {
|
||||
try {
|
||||
it("calls model layer create", async () => {
|
||||
await customerService.create({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
password: "secretsauce",
|
||||
})
|
||||
|
||||
expect(CustomerModelMock.create).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.create).toBeCalledWith({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
has_account: true,
|
||||
password_hash: expect.stringMatching(
|
||||
/^\$2[aby]?\$[\d]+\$[./A-Za-z0-9]{53}$/
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
it("fails if email is in incorrect format", async () => {
|
||||
expect(
|
||||
customerService.create({
|
||||
email: "olivermedusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("The email is not valid")
|
||||
}
|
||||
).rejects.toThrow("The email is not valid")
|
||||
})
|
||||
|
||||
it("fails if billing address is in incorrect format", () => {
|
||||
try {
|
||||
expect(
|
||||
customerService.create({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
@@ -105,9 +145,7 @@ describe("CustomerService", () => {
|
||||
first_name: 1234,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("The address is not valid")
|
||||
}
|
||||
).rejects.toThrow("The address is not valid")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,12 +162,19 @@ describe("CustomerService", () => {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
first_name: "Olli",
|
||||
last_name: "Test",
|
||||
email: "oliver@medusa2.com",
|
||||
})
|
||||
|
||||
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testCustomer") },
|
||||
{ $set: { first_name: "Olli", last_name: "Test" } },
|
||||
{
|
||||
$set: {
|
||||
first_name: "Olli",
|
||||
last_name: "Test",
|
||||
email: "oliver@medusa2.com",
|
||||
},
|
||||
},
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
@@ -144,118 +189,62 @@ describe("CustomerService", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("fails if billing address updates are attempted", async () => {
|
||||
try {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
billing_address: {
|
||||
last_name: "nnonono",
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Use updateBillingAddress to update billing address"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateEmail", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates an email", async () => {
|
||||
await customerService.updateEmail(
|
||||
IdMap.getId("testCustomer"),
|
||||
"oliver@medusa2.com"
|
||||
)
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("testCustomer"),
|
||||
it("updates with billing address", async () => {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
first_name: "Olli",
|
||||
last_name: "Test",
|
||||
billing_address: {
|
||||
first_name: "Olli",
|
||||
last_name: "Juhl",
|
||||
address_1: "Laksegade",
|
||||
city: "Copenhagen",
|
||||
country_code: "DK",
|
||||
postal_code: "2100",
|
||||
},
|
||||
})
|
||||
|
||||
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testCustomer") },
|
||||
{
|
||||
$set: { email: "oliver@medusa2.com" },
|
||||
}
|
||||
$set: {
|
||||
first_name: "Olli",
|
||||
last_name: "Test",
|
||||
billing_address: {
|
||||
first_name: "Olli",
|
||||
last_name: "Juhl",
|
||||
address_1: "Laksegade",
|
||||
city: "Copenhagen",
|
||||
country_code: "DK",
|
||||
postal_code: "2100",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws on invalid email", async () => {
|
||||
try {
|
||||
await customerService.updateEmail(
|
||||
IdMap.getId("testCustomer"),
|
||||
"olivermedusa"
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("The email is not valid")
|
||||
}
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
describe("updateBillingAddress", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
it("updates with password", async () => {
|
||||
await customerService.update(IdMap.getId("testCustomer"), {
|
||||
password: "newpassword",
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates billing address", async () => {
|
||||
await customerService.updateBillingAddress(
|
||||
IdMap.getId("testCustomer"),
|
||||
{
|
||||
first_name: "Olli",
|
||||
last_name: "Juhl",
|
||||
address_1: "Laksegade",
|
||||
city: "Copenhagen",
|
||||
country_code: "DK",
|
||||
postal_code: "2100",
|
||||
}
|
||||
)
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("testCustomer"),
|
||||
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(CustomerModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testCustomer") },
|
||||
{
|
||||
$set: {
|
||||
has_account: true,
|
||||
password_hash: expect.stringMatching(
|
||||
/^\$2[aby]?\$[\d]+\$[./A-Za-z0-9]{53}$/
|
||||
),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
billing_address: {
|
||||
first_name: "Olli",
|
||||
last_name: "Juhl",
|
||||
address_1: "Laksegade",
|
||||
city: "Copenhagen",
|
||||
country_code: "DK",
|
||||
postal_code: "2100",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("throws on invalid address", async () => {
|
||||
try {
|
||||
await customerService.updateBillingAddress(
|
||||
IdMap.getId("testCustomer"),
|
||||
{
|
||||
first_name: 1234,
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("The address is not valid")
|
||||
}
|
||||
|
||||
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
},
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const customerService = new CustomerService({
|
||||
customerModel: CustomerModelMock,
|
||||
|
||||
@@ -6,10 +6,14 @@ import { BaseService } from "medusa-interfaces"
|
||||
* @implements BaseService
|
||||
*/
|
||||
class AuthService extends BaseService {
|
||||
constructor({ userService }) {
|
||||
constructor({ userService, customerService }) {
|
||||
super()
|
||||
|
||||
/** @private @const {UserService} */
|
||||
this.userService_ = userService
|
||||
|
||||
/** @private @const {CustomerService} */
|
||||
this.customerService_ = customerService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,6 +42,7 @@ class AuthService extends BaseService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a given user based on an email, password combination. Uses
|
||||
* bcrypt to match password with hashed value.
|
||||
@@ -70,6 +75,49 @@ class AuthService extends BaseService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a customer based on an email, password combination. Uses
|
||||
* bcrypt to match password with hashed value.
|
||||
* @param {string} email - the email of the user
|
||||
* @param {string} password - the password of the user
|
||||
* @return {{ success: (bool), user: (object | undefined) }}
|
||||
* success: whether authentication succeeded
|
||||
* user: the user document if authentication succeded
|
||||
* error: a string with the error message
|
||||
*/
|
||||
async authenticateCustomer(email, password) {
|
||||
try {
|
||||
const customer = await this.customerService_.retrieveByEmail(email)
|
||||
if (!customer.password_hash) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
|
||||
const passwordsMatch = await bcrypt.compare(
|
||||
password,
|
||||
customer.password_hash
|
||||
)
|
||||
if (passwordsMatch) {
|
||||
return {
|
||||
success: true,
|
||||
customer,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthService
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mongoose from "mongoose"
|
||||
import bcrypt from "bcrypt"
|
||||
import _ from "lodash"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
@@ -68,6 +69,36 @@ class CustomerService extends BaseService {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON Web token, that will be sent to a customer, that wishes to
|
||||
* reset password.
|
||||
* The token will be signed with the customer's current password hash as a
|
||||
* secret a long side a payload with userId and the expiry time for the token,
|
||||
* which is always 15 minutes.
|
||||
* @param {string} customerId - the customer to reset the password for
|
||||
* @returns {string} the generated JSON web token
|
||||
*/
|
||||
async generateResetPasswordToken(customerId) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
|
||||
if (!customer.has_account) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"You must have an account to reset the password. Create an account first"
|
||||
)
|
||||
}
|
||||
|
||||
const secret = customer.password_hash
|
||||
const expiry = Math.floor(Date.now() / 1000) + 60 * 15 // 15 minutes ahead
|
||||
const payload = { customer_id: customer._id, exp: expiry }
|
||||
const token = jwt.sign(payload, secret)
|
||||
|
||||
// TODO: Call event layer to ensure that there is an email service that
|
||||
// sends the token.
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
@@ -99,63 +130,54 @@ class CustomerService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a customer with email and billing address
|
||||
* (if provided) being validated.
|
||||
* Gets a customer by email.
|
||||
* @param {string} email - the email of the customer to get.
|
||||
* @return {Promise<Customer>} the customer document.
|
||||
*/
|
||||
async retrieveByEmail(email) {
|
||||
this.validateEmail_(email)
|
||||
const customer = await this.customerModel_.findOne({ email }).catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
|
||||
if (!customer) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Customer with email ${email} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return customer
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a customer from an email - customers can have accounts associated,
|
||||
* e.g. to login and view order history, etc. If a password is provided the
|
||||
* customer will automatically get an account, otherwise the customer is just
|
||||
* used to hold details of customers.
|
||||
* @param {object} customer - the customer to create
|
||||
* @return {Promise} the result of create
|
||||
*/
|
||||
create(customer) {
|
||||
const { email, billing_address } = customer
|
||||
async create(customer) {
|
||||
const { email, billing_address, password } = customer
|
||||
this.validateEmail_(email)
|
||||
|
||||
if (billing_address) {
|
||||
this.validateBillingAddress_(billing_address)
|
||||
}
|
||||
this.customerModel_.create(customer).catch(err => {
|
||||
|
||||
if (password) {
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
customer.password_hash = hashedPassword
|
||||
customer.has_account = true
|
||||
delete customer.password
|
||||
}
|
||||
|
||||
return this.customerModel_.create(customer).catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the email of a customer
|
||||
* @param {string} customerId - the id of the customer to update
|
||||
* @param {string} email - the email to add to customer
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async updateEmail(customerId, email) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
this.validateEmail_(email)
|
||||
|
||||
return this.customerModel_.updateOne(
|
||||
{
|
||||
_id: customer._id,
|
||||
},
|
||||
{
|
||||
$set: { email },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the billing address of a customer
|
||||
* @param {*} customerId - the customer to update address on
|
||||
* @param {*} address - the new address to replace the current one
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async updateBillingAddress(customerId, address) {
|
||||
const customer = await this.retrieve(customerId)
|
||||
|
||||
this.validateBillingAddress_(address)
|
||||
|
||||
return this.customerModel_.updateOne(
|
||||
{
|
||||
_id: customer._id,
|
||||
},
|
||||
{
|
||||
$set: { billing_address: address },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a customer. Metadata updates and address updates should
|
||||
* use dedicated methods, e.g. `setMetadata`, etc. The function
|
||||
@@ -175,11 +197,19 @@ class CustomerService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
if (update.email) {
|
||||
this.validateEmail_(update.email)
|
||||
}
|
||||
|
||||
if (update.billing_address) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Use updateBillingAddress to update billing address"
|
||||
)
|
||||
this.validateBillingAddress_(update.billing_address)
|
||||
}
|
||||
|
||||
if (update.password) {
|
||||
const hashedPassword = await bcrypt.hash(update.password, 10)
|
||||
update.password_hash = hashedPassword
|
||||
update.has_account = true
|
||||
delete update.password
|
||||
}
|
||||
|
||||
return this.customerModel_
|
||||
|
||||
@@ -243,7 +243,7 @@ class UserService extends BaseService {
|
||||
const user = await this.retrieve(userId)
|
||||
const secret = user.password_hash
|
||||
const expiry = Math.floor(Date.now() / 1000) + 60 * 15
|
||||
const payload = { userId: user._id, exp: expiry }
|
||||
const payload = { user_id: user._id, exp: expiry }
|
||||
const token = jwt.sign(payload, secret)
|
||||
return token
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user