Adds endpoints to manage customers (#54)

This commit is contained in:
Sebastian Rindom
2020-05-07 13:31:38 +02:00
committed by GitHub
parent 516bc7675d
commit 6a78df1ecd
24 changed files with 858 additions and 170 deletions

View File

@@ -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

View 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)
}

View File

@@ -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")
})
})
})

View File

@@ -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)
})
})
})

View File

@@ -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)
})
})
})

View File

@@ -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)
})
})
})

View File

@@ -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)
})
})
})

View File

@@ -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()
}
}

View File

@@ -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
}
}

View 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
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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 +
"=" +

View File

@@ -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)
}

View File

@@ -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: {} },
}
}

View 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

View File

@@ -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 })

View File

@@ -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,

View File

@@ -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

View File

@@ -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_

View File

@@ -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
}