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