From bf43896d1942d352efcf8b900f4a31fef0fe215d Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Fri, 17 Sep 2021 08:27:46 +0200 Subject: [PATCH] fix: customer endpoints shouldn't use customer id already provided through authentication (#402) * Updated customers/:id to customers/me - untested * fix: integration +unit tests * docs: fix oas docs Co-authored-by: ColdMeekly <20516479+ColdMeekly@users.noreply.github.com> --- .../api/__tests__/store/customer.js | 148 +++++++++--------- .../customers/__tests__/update-customer.js | 111 +++++-------- .../store/customers/authorize-customer.js | 12 -- .../routes/store/customers/create-address.js | 2 +- .../routes/store/customers/delete-address.js | 3 +- .../routes/store/customers/get-customer.js | 6 +- .../store/customers/get-payment-methods.js | 8 +- .../src/api/routes/store/customers/index.js | 15 +- .../api/routes/store/customers/list-orders.js | 8 +- .../store/customers/reset-password-token.js | 8 +- .../routes/store/customers/reset-password.js | 9 +- .../routes/store/customers/update-address.js | 6 +- .../routes/store/customers/update-customer.js | 6 +- 13 files changed, 139 insertions(+), 203 deletions(-) delete mode 100644 packages/medusa/src/api/routes/store/customers/authorize-customer.js diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index 5b08322bdd..52189ca8c0 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -1,67 +1,67 @@ -const path = require("path"); -const { Address, Customer } = require("@medusajs/medusa"); +const path = require("path") +const { Address, Customer } = require("@medusajs/medusa") -const setupServer = require("../../../helpers/setup-server"); -const { useApi } = require("../../../helpers/use-api"); -const { initDb, useDb } = require("../../../helpers/use-db"); +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +const { initDb, useDb } = require("../../../helpers/use-db") -const customerSeeder = require("../../helpers/customer-seeder"); +const customerSeeder = require("../../helpers/customer-seeder") -jest.setTimeout(30000); +jest.setTimeout(30000) describe("/store/customers", () => { - let medusaProcess; - let dbConnection; + let medusaProcess + let dbConnection const doAfterEach = async () => { - const db = useDb(); - await db.teardown(); - }; + const db = useDb() + await db.teardown() + } beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")); - dbConnection = await initDb({ cwd }); - medusaProcess = await setupServer({ cwd }); - }); + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) afterAll(async () => { - const db = useDb(); - await db.shutdown(); - medusaProcess.kill(); - }); + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) describe("POST /store/customers", () => { beforeEach(async () => { - const manager = dbConnection.manager; + const manager = dbConnection.manager await manager.insert(Customer, { id: "test_customer", first_name: "John", last_name: "Deere", email: "john@deere.com", has_account: true, - }); - }); + }) + }) afterEach(async () => { - await doAfterEach(); - }); + await doAfterEach() + }) it("creates a customer", async () => { - const api = useApi(); + const api = useApi() const response = await api.post("/store/customers", { first_name: "James", last_name: "Bond", email: "james@bond.com", password: "test", - }); + }) - expect(response.status).toEqual(200); - expect(response.data.customer).not.toHaveProperty("password_hash"); - }); + expect(response.status).toEqual(200) + expect(response.data.customer).not.toHaveProperty("password_hash") + }) it("responds 409 on duplicate", async () => { - const api = useApi(); + const api = useApi() const response = await api .post("/store/customers", { @@ -70,15 +70,15 @@ describe("/store/customers", () => { email: "john@deere.com", password: "test", }) - .catch((err) => err.response); + .catch((err) => err.response) - expect(response.status).toEqual(402); - }); - }); + expect(response.status).toEqual(402) + }) + }) - describe("POST /store/customers/:id", () => { + describe("POST /store/customers/me", () => { beforeEach(async () => { - const manager = dbConnection.manager; + const manager = dbConnection.manager await manager.insert(Address, { id: "addr_test", first_name: "String", @@ -88,7 +88,7 @@ describe("/store/customers", () => { postal_code: "1236", province: "ca", country_code: "us", - }); + }) await manager.insert(Customer, { id: "test_customer", @@ -98,26 +98,26 @@ describe("/store/customers", () => { password_hash: "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" has_account: true, - }); - }); + }) + }) afterEach(async () => { - await doAfterEach(); - }); + await doAfterEach() + }) it("updates a customer", async () => { - const api = useApi(); + const api = useApi() const authResponse = await api.post("/store/auth", { email: "john@deere.com", password: "test", - }); + }) - const customerId = authResponse.data.customer.id; - const [authCookie] = authResponse.headers["set-cookie"][0].split(";"); + const customerId = authResponse.data.customer.id + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") const response = await api.post( - `/store/customers/${customerId}`, + `/store/customers/me`, { password: "test", metadata: { key: "value" }, @@ -127,30 +127,30 @@ describe("/store/customers", () => { Cookie: authCookie, }, } - ); + ) - expect(response.status).toEqual(200); - expect(response.data.customer).not.toHaveProperty("password_hash"); + expect(response.status).toEqual(200) + expect(response.data.customer).not.toHaveProperty("password_hash") expect(response.data.customer).toEqual( expect.objectContaining({ metadata: { key: "value" }, }) - ); - }); + ) + }) it("updates customer billing address", async () => { - const api = useApi(); + const api = useApi() const authResponse = await api.post("/store/auth", { email: "john@deere.com", password: "test", - }); + }) - const customerId = authResponse.data.customer.id; - const [authCookie] = authResponse.headers["set-cookie"][0].split(";"); + const customerId = authResponse.data.customer.id + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") const response = await api.post( - `/store/customers/${customerId}`, + `/store/customers/me`, { billing_address: { first_name: "test", @@ -167,10 +167,10 @@ describe("/store/customers", () => { Cookie: authCookie, }, } - ); + ) - expect(response.status).toEqual(200); - expect(response.data.customer).not.toHaveProperty("password_hash"); + expect(response.status).toEqual(200) + expect(response.data.customer).not.toHaveProperty("password_hash") expect(response.data.customer.billing_address).toEqual( expect.objectContaining({ first_name: "test", @@ -181,22 +181,22 @@ describe("/store/customers", () => { province: "ca", country_code: "us", }) - ); - }); + ) + }) it("updates customer billing address with string", async () => { - const api = useApi(); + const api = useApi() const authResponse = await api.post("/store/auth", { email: "john@deere.com", password: "test", - }); + }) - const customerId = authResponse.data.customer.id; - const [authCookie] = authResponse.headers["set-cookie"][0].split(";"); + const customerId = authResponse.data.customer.id + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") const response = await api.post( - `/store/customers/${customerId}`, + `/store/customers/me`, { billing_address: "addr_test", }, @@ -205,10 +205,10 @@ describe("/store/customers", () => { Cookie: authCookie, }, } - ); + ) - expect(response.status).toEqual(200); - expect(response.data.customer).not.toHaveProperty("password_hash"); + expect(response.status).toEqual(200) + expect(response.data.customer).not.toHaveProperty("password_hash") expect(response.data.customer.billing_address).toEqual( expect.objectContaining({ first_name: "String", @@ -219,7 +219,7 @@ describe("/store/customers", () => { province: "ca", country_code: "us", }) - ); - }); - }); -}); + ) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js b/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js index baab0d5261..a1a9c7925e 100644 --- a/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js +++ b/packages/medusa/src/api/routes/store/customers/__tests__/update-customer.js @@ -7,21 +7,17 @@ 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", + subject = await request("POST", `/store/customers/me`, { + payload: { + first_name: "LeBron", + last_name: "James", + }, + clientSession: { + jwt: { + customer_id: IdMap.getId("lebron"), }, - clientSession: { - jwt: { - customer_id: IdMap.getId("lebron"), - }, - }, - } - ) + }, + }) }) afterAll(() => { @@ -59,20 +55,16 @@ describe("POST /store/customers/:id", () => { describe("successfully updates a customer with billing address id", () => { let subject beforeAll(async () => { - subject = await request( - "POST", - `/store/customers/${IdMap.getId("lebron")}`, - { - payload: { - billing_address: "test", + subject = await request("POST", `/store/customers/me`, { + payload: { + billing_address: "test", + }, + clientSession: { + jwt: { + customer_id: IdMap.getId("lebron"), }, - clientSession: { - jwt: { - customer_id: IdMap.getId("lebron"), - }, - }, - } - ) + }, + }) }) afterAll(() => { @@ -97,28 +89,24 @@ describe("POST /store/customers/:id", () => { describe("successfully updates a customer with billing address object", () => { let subject beforeAll(async () => { - subject = await request( - "POST", - `/store/customers/${IdMap.getId("lebron")}`, - { - payload: { - billing_address: { - first_name: "Olli", - last_name: "Juhl", - address_1: "Laksegade", - city: "Copenhagen", - country_code: "dk", - postal_code: "2100", - phone: "+1 (222) 333 4444", - }, + subject = await request("POST", `/store/customers/me`, { + payload: { + billing_address: { + first_name: "Olli", + last_name: "Juhl", + address_1: "Laksegade", + city: "Copenhagen", + country_code: "dk", + postal_code: "2100", + phone: "+1 (222) 333 4444", }, - clientSession: { - jwt: { - customer_id: IdMap.getId("lebron"), - }, + }, + clientSession: { + jwt: { + customer_id: IdMap.getId("lebron"), }, - } - ) + }, + }) }) afterAll(() => { @@ -147,33 +135,4 @@ describe("POST /store/customers/:id", () => { expect(subject.status).toEqual(200) }) }) - - 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) - }) - }) }) diff --git a/packages/medusa/src/api/routes/store/customers/authorize-customer.js b/packages/medusa/src/api/routes/store/customers/authorize-customer.js deleted file mode 100644 index c789c84bed..0000000000 --- a/packages/medusa/src/api/routes/store/customers/authorize-customer.js +++ /dev/null @@ -1,12 +0,0 @@ -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() - } -} diff --git a/packages/medusa/src/api/routes/store/customers/create-address.js b/packages/medusa/src/api/routes/store/customers/create-address.js index 390f07e1a2..2ecf2bbd31 100644 --- a/packages/medusa/src/api/routes/store/customers/create-address.js +++ b/packages/medusa/src/api/routes/store/customers/create-address.js @@ -30,7 +30,7 @@ import { defaultRelations, defaultFields } from "./" * $ref: "#/components/schemas/customer" */ export default async (req, res) => { - const { id } = req.params + const id = req.user.customer_id const schema = Validator.object().keys({ address: Validator.address().required(), diff --git a/packages/medusa/src/api/routes/store/customers/delete-address.js b/packages/medusa/src/api/routes/store/customers/delete-address.js index 4829da15ac..a47ed0edc4 100644 --- a/packages/medusa/src/api/routes/store/customers/delete-address.js +++ b/packages/medusa/src/api/routes/store/customers/delete-address.js @@ -21,7 +21,8 @@ import { defaultRelations, defaultFields } from "./" * $ref: "#/components/schemas/customer" */ export default async (req, res) => { - const { id, address_id } = req.params + const id = req.user.customer_id + const { address_id } = req.params const customerService = req.scope.resolve("customerService") try { diff --git a/packages/medusa/src/api/routes/store/customers/get-customer.js b/packages/medusa/src/api/routes/store/customers/get-customer.js index 53db7b0e5e..48cc2bcf8a 100644 --- a/packages/medusa/src/api/routes/store/customers/get-customer.js +++ b/packages/medusa/src/api/routes/store/customers/get-customer.js @@ -1,12 +1,10 @@ import { defaultRelations, defaultFields } from "./" /** - * @oas [get] /customers/{id} + * @oas [get] /customers/me * operationId: GetCustomersCustomer * summary: Retrieves a Customer * description: "Retrieves a Customer - the Customer must be logged in to retrieve their details." - * parameters: - * - (path) id=* {string} The id of the Customer. * tags: * - Customer * responses: @@ -20,7 +18,7 @@ import { defaultRelations, defaultFields } from "./" * $ref: "#/components/schemas/customer" */ export default async (req, res) => { - const { id } = req.params + const id = req.user.customer_id try { const customerService = req.scope.resolve("customerService") const customer = await customerService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/store/customers/get-payment-methods.js b/packages/medusa/src/api/routes/store/customers/get-payment-methods.js index 73cb812c30..5d84696916 100644 --- a/packages/medusa/src/api/routes/store/customers/get-payment-methods.js +++ b/packages/medusa/src/api/routes/store/customers/get-payment-methods.js @@ -1,5 +1,5 @@ /** - * @oas [get] /customers/{id}/payment-methods + * @oas [get] /customers/me/payment-methods * operationId: GetCustomersCustomerPaymentMethods * summary: Retrieve saved payment methods * description: "Retrieves a list of a Customer's saved payment methods. Payment methods are saved with Payment Providers and it is their responsibility to fetch saved methods." @@ -26,7 +26,7 @@ * description: The data needed for the Payment Provider to use the saved payment method. */ export default async (req, res) => { - const { id } = req.params + const id = req.user.customer_id try { const storeService = req.scope.resolve("storeService") const paymentProviderService = req.scope.resolve("paymentProviderService") @@ -37,11 +37,11 @@ export default async (req, res) => { const store = await storeService.retrieve(["payment_providers"]) const methods = await Promise.all( - store.payment_providers.map(async next => { + store.payment_providers.map(async (next) => { const provider = paymentProviderService.retrieveProvider(next) const pMethods = await provider.retrieveSavedMethods(customer) - return pMethods.map(m => ({ + return pMethods.map((m) => ({ provider_id: next, data: m, })) diff --git a/packages/medusa/src/api/routes/store/customers/index.js b/packages/medusa/src/api/routes/store/customers/index.js index 79938d1ac2..ab578c7585 100644 --- a/packages/medusa/src/api/routes/store/customers/index.js +++ b/packages/medusa/src/api/routes/store/customers/index.js @@ -7,7 +7,6 @@ export default (app, container) => { const middlewareService = container.resolve("middlewareService") app.use("/customers", route) - route.param("id", middlewares.wrap(require("./authorize-customer").default)) // Inject plugin routes const routers = middlewareService.getRouters("store/customers") @@ -30,28 +29,28 @@ export default (app, container) => { // Authenticated endpoints route.use(middlewares.authenticate()) - route.get("/:id", middlewares.wrap(require("./get-customer").default)) - route.post("/:id", middlewares.wrap(require("./update-customer").default)) + route.get("/me", middlewares.wrap(require("./get-customer").default)) + route.post("/me", middlewares.wrap(require("./update-customer").default)) - route.get("/:id/orders", middlewares.wrap(require("./list-orders").default)) + route.get("/me/orders", middlewares.wrap(require("./list-orders").default)) route.post( - "/:id/addresses", + "/me/addresses", middlewares.wrap(require("./create-address").default) ) route.post( - "/:id/addresses/:address_id", + "/me/addresses/:address_id", middlewares.wrap(require("./update-address").default) ) route.delete( - "/:id/addresses/:address_id", + "/me/addresses/:address_id", middlewares.wrap(require("./delete-address").default) ) route.get( - "/:id/payment-methods", + "/me/payment-methods", middlewares.wrap(require("./get-payment-methods").default) ) diff --git a/packages/medusa/src/api/routes/store/customers/list-orders.js b/packages/medusa/src/api/routes/store/customers/list-orders.js index 47491805f4..30cb695fd7 100644 --- a/packages/medusa/src/api/routes/store/customers/list-orders.js +++ b/packages/medusa/src/api/routes/store/customers/list-orders.js @@ -7,7 +7,7 @@ import { } from "../orders" /** - * @oas [get] /customers/{id}/orders + * @oas [get] /customers/me/orders * operationId: GetCustomersCustomerOrders * summary: Retrieve Customer Orders * description: "Retrieves a list of a Customer's Orders." @@ -28,7 +28,7 @@ import { * $ref: "#/components/schemas/order" */ export default async (req, res) => { - const { id } = req.params + const id = req.user.customer_id try { const orderService = req.scope.resolve("orderService") @@ -42,13 +42,13 @@ export default async (req, res) => { let includeFields = [] if ("fields" in req.query) { includeFields = req.query.fields.split(",") - includeFields = includeFields.filter(f => allowedFields.includes(f)) + includeFields = includeFields.filter((f) => allowedFields.includes(f)) } let expandFields = [] if ("expand" in req.query) { expandFields = req.query.expand.split(",") - expandFields = expandFields.filter(f => allowedRelations.includes(f)) + expandFields = expandFields.filter((f) => allowedRelations.includes(f)) } const listConfig = { diff --git a/packages/medusa/src/api/routes/store/customers/reset-password-token.js b/packages/medusa/src/api/routes/store/customers/reset-password-token.js index 6898fd8a47..3fe343b70b 100644 --- a/packages/medusa/src/api/routes/store/customers/reset-password-token.js +++ b/packages/medusa/src/api/routes/store/customers/reset-password-token.js @@ -1,12 +1,10 @@ import { MedusaError, Validator } from "medusa-core-utils" /** - * @oas [post] /customers/{id}/password-token + * @oas [post] /customers/password-token * operationId: PostCustomersCustomerPasswordToken * summary: Creates a reset password token * description: "Creates a reset password token to be used in a subsequent /reset-password request. The password token should be sent out of band e.g. via email and will not be returned." - * parameters: - * - (path) id=* {string} The id of the Customer. * tags: * - Customer * responses: @@ -15,9 +13,7 @@ import { MedusaError, Validator } from "medusa-core-utils" */ export default async (req, res) => { const schema = Validator.object().keys({ - email: Validator.string() - .email() - .required(), + email: Validator.string().email().required(), }) const { value, error } = schema.validate(req.body) diff --git a/packages/medusa/src/api/routes/store/customers/reset-password.js b/packages/medusa/src/api/routes/store/customers/reset-password.js index 4c0f09045c..4242e0e8f5 100644 --- a/packages/medusa/src/api/routes/store/customers/reset-password.js +++ b/packages/medusa/src/api/routes/store/customers/reset-password.js @@ -2,12 +2,11 @@ import { MedusaError, Validator } from "medusa-core-utils" import jwt from "jsonwebtoken" /** - * @oas [post] /customers/{id}/reset-password - * operationId: PostCustomersCustomerResetPassword + * @oas [post] /customers/reset-password + * operationId: PostCustomersResetPassword * summary: Resets Customer password * description: "Resets a Customer's password using a password token created by a previous /password-token request." * parameters: - * - (path) id=* {string} The id of the Customer. * - (body) email=* {string} The Customer's email. * - (body) token=* {string} The password token created by a /password-token request. * - (body) password=* {string} The new password to set for the Customer. @@ -25,9 +24,7 @@ import jwt from "jsonwebtoken" */ export default async (req, res) => { const schema = Validator.object().keys({ - email: Validator.string() - .email() - .required(), + email: Validator.string().email().required(), token: Validator.string().required(), password: Validator.string().required(), }) diff --git a/packages/medusa/src/api/routes/store/customers/update-address.js b/packages/medusa/src/api/routes/store/customers/update-address.js index 6c495451a2..d0c835416d 100644 --- a/packages/medusa/src/api/routes/store/customers/update-address.js +++ b/packages/medusa/src/api/routes/store/customers/update-address.js @@ -2,12 +2,11 @@ import { Validator, MedusaError } from "medusa-core-utils" import { defaultRelations, defaultFields } from "./" /** - * @oas [post] /customers/{id}/addresses/{address_id} + * @oas [post] /customers/me/addresses/{address_id} * operationId: PostCustomersCustomerAddressesAddress * summary: "Update a Shipping Address" * description: "Updates a Customer's saved Shipping Address." * parameters: - * - (path) id=* {String} The Customer id. * - (path) address_id=* {String} The id of the Address to update. * requestBody: * content: @@ -31,7 +30,8 @@ import { defaultRelations, defaultFields } from "./" * $ref: "#/components/schemas/customer" */ export default async (req, res) => { - const { id, address_id } = req.params + const id = req.user.customer_id + const { address_id } = req.params const schema = Validator.object().keys({ address: Validator.address().required(), diff --git a/packages/medusa/src/api/routes/store/customers/update-customer.js b/packages/medusa/src/api/routes/store/customers/update-customer.js index 6101f4fcaa..a3635f9ec5 100644 --- a/packages/medusa/src/api/routes/store/customers/update-customer.js +++ b/packages/medusa/src/api/routes/store/customers/update-customer.js @@ -2,12 +2,10 @@ import { Validator, MedusaError } from "medusa-core-utils" import { defaultRelations, defaultFields } from "./" /** - * @oas [post] /customers/{id} + * @oas [post] /customers/me * operationId: PostCustomersCustomer * summary: Update Customer details * description: "Updates a Customer's saved details." - * parameters: - * - (path) id=* {string} The id of the Customer. * requestBody: * content: * application/json: @@ -45,7 +43,7 @@ import { defaultRelations, defaultFields } from "./" * $ref: "#/components/schemas/customer" */ export default async (req, res) => { - const { id } = req.params + const id = req.user.customer_id const schema = Validator.object().keys({ billing_address: Validator.address().optional(),