From 7903a15e0f3bb277c8513199a4d50123e1b28f5d Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 31 Jan 2024 12:58:29 +0100 Subject: [PATCH] feat(customer): Add create and retrieve customer from store side (#6267) **What** - GET /store/customers/me - POST /store/customers - Workflow for customer account creation - Authentication middleware on customer routes --- .../plugins/__tests__/cart/store/get-cart.ts | 2 + .../admin/create-customer-group.ts | 2 + .../admin/delete-customer-group.ts | 2 + .../admin/list-customer-groups.spec.ts | 2 + .../admin/retrieve-customer-group.ts | 2 + .../admin/update-customer-group.ts | 2 + .../admin/create-customer-addresses.ts | 2 + .../customer/admin/create-customer.ts | 2 + .../admin/delete-customer-address.spec.ts | 2 + .../customer/admin/delete-customer.ts | 2 + .../customer/admin/list-customer-addresses.ts | 2 + .../customer/admin/list-customers.spec.ts | 2 + .../admin/update-customer-address.spec.ts | 2 + .../customer/admin/update-customer.ts | 2 + .../customer/store/create-customer.spec.ts | 76 ++++++++++++++++++ .../__tests__/customer/store/get-me.spec.ts | 78 +++++++++++++++++++ integration-tests/plugins/medusa-config.js | 8 ++ integration-tests/plugins/package.json | 1 + .../src/migrations/Migration20240122041959.ts | 28 ++++--- packages/core-flows/src/auth/index.ts | 1 + packages/core-flows/src/auth/steps/index.ts | 1 + .../src/auth/steps/set-auth-app-metadata.ts | 59 ++++++++++++++ .../src/customer/steps/delete-customers.ts | 6 +- .../workflows/create-customer-account.ts | 31 ++++++++ .../customer/workflows/delete-customers.ts | 4 +- .../src/customer/workflows/index.ts | 1 + packages/medusa/src/api-v2/middlewares.ts | 2 + .../src/api-v2/store/customers/me/route.ts | 15 ++++ .../src/api-v2/store/customers/middlewares.ts | 28 +++++++ .../api-v2/store/customers/query-config.ts | 24 ++++++ .../src/api-v2/store/customers/route.ts | 14 ++++ .../src/api-v2/store/customers/validators.ts | 29 +++++++ yarn.lock | 3 +- 33 files changed, 421 insertions(+), 16 deletions(-) create mode 100644 integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts create mode 100644 integration-tests/plugins/__tests__/customer/store/get-me.spec.ts create mode 100644 packages/core-flows/src/auth/index.ts create mode 100644 packages/core-flows/src/auth/steps/index.ts create mode 100644 packages/core-flows/src/auth/steps/set-auth-app-metadata.ts create mode 100644 packages/core-flows/src/customer/workflows/create-customer-account.ts create mode 100644 packages/medusa/src/api-v2/store/customers/me/route.ts create mode 100644 packages/medusa/src/api-v2/store/customers/middlewares.ts create mode 100644 packages/medusa/src/api-v2/store/customers/query-config.ts create mode 100644 packages/medusa/src/api-v2/store/customers/route.ts create mode 100644 packages/medusa/src/api-v2/store/customers/validators.ts diff --git a/integration-tests/plugins/__tests__/cart/store/get-cart.ts b/integration-tests/plugins/__tests__/cart/store/get-cart.ts index 0ce5e7870e..8d2b64409c 100644 --- a/integration-tests/plugins/__tests__/cart/store/get-cart.ts +++ b/integration-tests/plugins/__tests__/cart/store/get-cart.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } describe("GET /store/:id", () => { diff --git a/integration-tests/plugins/__tests__/customer-group/admin/create-customer-group.ts b/integration-tests/plugins/__tests__/customer-group/admin/create-customer-group.ts index b987f255c8..0c4fd5de01 100644 --- a/integration-tests/plugins/__tests__/customer-group/admin/create-customer-group.ts +++ b/integration-tests/plugins/__tests__/customer-group/admin/create-customer-group.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer-group/admin/delete-customer-group.ts b/integration-tests/plugins/__tests__/customer-group/admin/delete-customer-group.ts index 88189e4402..106cf0272d 100644 --- a/integration-tests/plugins/__tests__/customer-group/admin/delete-customer-group.ts +++ b/integration-tests/plugins/__tests__/customer-group/admin/delete-customer-group.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer-group/admin/list-customer-groups.spec.ts b/integration-tests/plugins/__tests__/customer-group/admin/list-customer-groups.spec.ts index 732f26da34..2790bc17a9 100644 --- a/integration-tests/plugins/__tests__/customer-group/admin/list-customer-groups.spec.ts +++ b/integration-tests/plugins/__tests__/customer-group/admin/list-customer-groups.spec.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer-group/admin/retrieve-customer-group.ts b/integration-tests/plugins/__tests__/customer-group/admin/retrieve-customer-group.ts index f2ad1a6634..17bfa8493d 100644 --- a/integration-tests/plugins/__tests__/customer-group/admin/retrieve-customer-group.ts +++ b/integration-tests/plugins/__tests__/customer-group/admin/retrieve-customer-group.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer-group/admin/update-customer-group.ts b/integration-tests/plugins/__tests__/customer-group/admin/update-customer-group.ts index 25d94f0b0b..64d28fccca 100644 --- a/integration-tests/plugins/__tests__/customer-group/admin/update-customer-group.ts +++ b/integration-tests/plugins/__tests__/customer-group/admin/update-customer-group.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts index c860f237c5..5aed39eeaa 100644 --- a/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts +++ b/integration-tests/plugins/__tests__/customer/admin/create-customer-addresses.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/create-customer.ts b/integration-tests/plugins/__tests__/customer/admin/create-customer.ts index 1a11e54234..a62b88615f 100644 --- a/integration-tests/plugins/__tests__/customer/admin/create-customer.ts +++ b/integration-tests/plugins/__tests__/customer/admin/create-customer.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts b/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts index a604b2b021..946deec07b 100644 --- a/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts +++ b/integration-tests/plugins/__tests__/customer/admin/delete-customer-address.spec.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/delete-customer.ts b/integration-tests/plugins/__tests__/customer/admin/delete-customer.ts index 0d1c032c14..77192b966e 100644 --- a/integration-tests/plugins/__tests__/customer/admin/delete-customer.ts +++ b/integration-tests/plugins/__tests__/customer/admin/delete-customer.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts index b87ac4e592..842e6f9fcd 100644 --- a/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts +++ b/integration-tests/plugins/__tests__/customer/admin/list-customer-addresses.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts b/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts index a5c0c47d54..a1e9aac30f 100644 --- a/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts +++ b/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts b/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts index 4ce6fac25d..10e5d3b1d9 100644 --- a/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts +++ b/integration-tests/plugins/__tests__/customer/admin/update-customer-address.spec.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/admin/update-customer.ts b/integration-tests/plugins/__tests__/customer/admin/update-customer.ts index 92b8c9da32..4aeda967bf 100644 --- a/integration-tests/plugins/__tests__/customer/admin/update-customer.ts +++ b/integration-tests/plugins/__tests__/customer/admin/update-customer.ts @@ -7,6 +7,8 @@ import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import adminSeeder from "../../../../helpers/admin-seeder" +jest.setTimeout(50000) + const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" }, diff --git a/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts b/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts new file mode 100644 index 0000000000..6b986b0bc0 --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts @@ -0,0 +1,76 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService, IAuthModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("POST /store/customers", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create a customer", async () => { + const authService: IAuthModuleService = appContainer.resolve( + ModuleRegistrationName.AUTH + ) + const authUser = await authService.createAuthUser({ + entity_id: "store_user", + provider_id: "test", + }) + const jwt = await authService.generateJwtToken(authUser.id, "store") + + const api = useApi() as any + const response = await api.post( + `/store/customers`, + { + first_name: "John", + last_name: "Doe", + email: "john@me.com", + }, + { headers: { authorization: `Bearer ${jwt}` } } + ) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "John", + last_name: "Doe", + email: "john@me.com", + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/customer/store/get-me.spec.ts b/integration-tests/plugins/__tests__/customer/store/get-me.spec.ts new file mode 100644 index 0000000000..24dc30af87 --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/store/get-me.spec.ts @@ -0,0 +1,78 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService, IAuthModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("GET /store/customers", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should retrieve auth user's customer", async () => { + const customer = await customerModuleService.create({ + first_name: "John", + last_name: "Doe", + email: "john@me.com", + }) + + const authService: IAuthModuleService = appContainer.resolve( + ModuleRegistrationName.AUTH + ) + const authUser = await authService.createAuthUser({ + entity_id: "store_user", + provider_id: "test", + app_metadata: { customer_id: customer.id }, + }) + + const jwt = await authService.generateJwtToken(authUser.id, "store") + + const api = useApi() as any + const response = await api.get(`/store/customers/me`, { + headers: { authorization: `Bearer ${jwt}` }, + }) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "John", + last_name: "Doe", + email: "john@me.com", + }) + ) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index 41dffcbef9..5d4193b4a4 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -42,6 +42,14 @@ module.exports = { }, }, modules: { + [Modules.AUTH]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/auth", + options: { + jwt_secret: "test", + }, + }, [Modules.STOCK_LOCATION]: { scope: "internal", resources: "shared", diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 74a9ec209c..17c355023e 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -9,6 +9,7 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { + "@medusajs/auth": "workspace:*", "@medusajs/cache-inmemory": "workspace:*", "@medusajs/customer": "workspace:^", "@medusajs/event-bus-local": "workspace:*", diff --git a/packages/auth/src/migrations/Migration20240122041959.ts b/packages/auth/src/migrations/Migration20240122041959.ts index d23f27a107..15f1526572 100644 --- a/packages/auth/src/migrations/Migration20240122041959.ts +++ b/packages/auth/src/migrations/Migration20240122041959.ts @@ -1,22 +1,30 @@ -import { Migration } from '@mikro-orm/migrations'; +import { Migration } from "@mikro-orm/migrations" export class Migration20240122041959 extends Migration { - async up(): Promise { - this.addSql('create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + this.addSql( + 'create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));' + ) - this.addSql('create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); - this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); + this.addSql( + 'create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));' + ) + this.addSql( + 'alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");' + ) - this.addSql('alter table "auth_user" add constraint if not exists "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); + this.addSql( + 'alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;' + ) } async down(): Promise { - this.addSql('alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";'); + this.addSql( + 'alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";' + ) - this.addSql('drop table if exists "auth_provider" cascade;'); + this.addSql('drop table if exists "auth_provider" cascade;') - this.addSql('drop table if exists "auth_user" cascade;'); + this.addSql('drop table if exists "auth_user" cascade;') } - } diff --git a/packages/core-flows/src/auth/index.ts b/packages/core-flows/src/auth/index.ts new file mode 100644 index 0000000000..c1f49c23fa --- /dev/null +++ b/packages/core-flows/src/auth/index.ts @@ -0,0 +1 @@ +export * from "./steps" diff --git a/packages/core-flows/src/auth/steps/index.ts b/packages/core-flows/src/auth/steps/index.ts new file mode 100644 index 0000000000..5c0c85f9a0 --- /dev/null +++ b/packages/core-flows/src/auth/steps/index.ts @@ -0,0 +1 @@ +export * from "./set-auth-app-metadata" diff --git a/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts b/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts new file mode 100644 index 0000000000..7b5d9809d4 --- /dev/null +++ b/packages/core-flows/src/auth/steps/set-auth-app-metadata.ts @@ -0,0 +1,59 @@ +import { StepResponse, createStep } from "@medusajs/workflows-sdk" +import { IAuthModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { isDefined } from "@medusajs/utils" + +type StepInput = { + authUserId: string + key: string + value: string +} + +export const setAuthAppMetadataStepId = "set-auth-app-metadata" +export const setAuthAppMetadataStep = createStep( + setAuthAppMetadataStepId, + async (data: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.AUTH + ) + + const authUser = await service.retrieveAuthUser(data.authUserId) + + const appMetadata = authUser.app_metadata || {} + if (isDefined(appMetadata[data.key])) { + throw new Error(`Key ${data.key} already exists in app metadata`) + } + + appMetadata[data.key] = data.value + + await service.updateAuthUser({ + id: authUser.id, + app_metadata: appMetadata, + }) + + return new StepResponse(authUser, { id: authUser.id, key: data.key }) + }, + async (idAndKey, { container }) => { + if (!idAndKey) { + return + } + + const { id, key } = idAndKey + + const service = container.resolve( + ModuleRegistrationName.AUTH + ) + + const authUser = await service.retrieveAuthUser(id) + + const appMetadata = authUser.app_metadata || {} + if (isDefined(appMetadata[key])) { + delete appMetadata[key] + } + + await service.updateAuthUser({ + id: authUser.id, + app_metadata: appMetadata, + }) + } +) diff --git a/packages/core-flows/src/customer/steps/delete-customers.ts b/packages/core-flows/src/customer/steps/delete-customers.ts index 35b6947c73..c2917d26c8 100644 --- a/packages/core-flows/src/customer/steps/delete-customers.ts +++ b/packages/core-flows/src/customer/steps/delete-customers.ts @@ -4,9 +4,9 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" type DeleteCustomerStepInput = string[] -export const deleteCustomerStepId = "delete-customer" -export const deleteCustomerStep = createStep( - deleteCustomerStepId, +export const deleteCustomersStepId = "delete-customers" +export const deleteCustomersStep = createStep( + deleteCustomersStepId, async (ids: DeleteCustomerStepInput, { container }) => { const service = container.resolve( ModuleRegistrationName.CUSTOMER diff --git a/packages/core-flows/src/customer/workflows/create-customer-account.ts b/packages/core-flows/src/customer/workflows/create-customer-account.ts new file mode 100644 index 0000000000..7dd8007ce1 --- /dev/null +++ b/packages/core-flows/src/customer/workflows/create-customer-account.ts @@ -0,0 +1,31 @@ +import { CreateCustomerDTO, CustomerDTO } from "@medusajs/types" +import { createWorkflow, WorkflowData } from "@medusajs/workflows-sdk" +import { createCustomersStep } from "../steps" +import { setAuthAppMetadataStep } from "../../auth/steps" +import { transform } from "@medusajs/workflows-sdk" + +type WorkflowInput = { + authUserId: string + customersData: CreateCustomerDTO +} + +export const createCustomerAccountWorkflowId = "create-customer-account" +export const createCustomerAccountWorkflow = createWorkflow( + createCustomerAccountWorkflowId, + (input: WorkflowData): WorkflowData => { + const customers = createCustomersStep([input.customersData]) + + const customer = transform( + customers, + (customers: CustomerDTO[]) => customers[0] + ) + + setAuthAppMetadataStep({ + authUserId: input.authUserId, + key: "customer_id", + value: customer.id, + }) + + return customer + } +) diff --git a/packages/core-flows/src/customer/workflows/delete-customers.ts b/packages/core-flows/src/customer/workflows/delete-customers.ts index 7603d4daec..2af349498d 100644 --- a/packages/core-flows/src/customer/workflows/delete-customers.ts +++ b/packages/core-flows/src/customer/workflows/delete-customers.ts @@ -1,5 +1,5 @@ import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { deleteCustomerStep } from "../steps" +import { deleteCustomersStep } from "../steps" type WorkflowInput = { ids: string[] } @@ -7,6 +7,6 @@ export const deleteCustomersWorkflowId = "delete-customers" export const deleteCustomersWorkflow = createWorkflow( deleteCustomersWorkflowId, (input: WorkflowData): WorkflowData => { - return deleteCustomerStep(input.ids) + return deleteCustomersStep(input.ids) } ) diff --git a/packages/core-flows/src/customer/workflows/index.ts b/packages/core-flows/src/customer/workflows/index.ts index cf2fe8e9b0..97c7505c37 100644 --- a/packages/core-flows/src/customer/workflows/index.ts +++ b/packages/core-flows/src/customer/workflows/index.ts @@ -1,6 +1,7 @@ export * from "./create-customers" export * from "./update-customers" export * from "./delete-customers" +export * from "./create-customer-account" export * from "./create-addresses" export * from "./update-addresses" export * from "./delete-addresses" diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 34fabd658f..40b80fa5ee 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,6 +1,7 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" +import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares" import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" @@ -11,6 +12,7 @@ export const config: MiddlewaresConfig = { ...adminCustomerRoutesMiddlewares, ...adminPromotionRoutesMiddlewares, ...adminCampaignRoutesMiddlewares, + ...storeCustomerRoutesMiddlewares, ...storeCartRoutesMiddlewares, ], } diff --git a/packages/medusa/src/api-v2/store/customers/me/route.ts b/packages/medusa/src/api-v2/store/customers/me/route.ts new file mode 100644 index 0000000000..22a9746d97 --- /dev/null +++ b/packages/medusa/src/api-v2/store/customers/me/route.ts @@ -0,0 +1,15 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.auth_user!.app_metadata.customer_id + + const customerModule = req.scope.resolve(ModuleRegistrationName.CUSTOMER) + + const customer = await customerModule.retrieve(id, { + select: req.retrieveConfig.select, + relations: req.retrieveConfig.relations, + }) + + res.json({ customer }) +} diff --git a/packages/medusa/src/api-v2/store/customers/middlewares.ts b/packages/medusa/src/api-v2/store/customers/middlewares.ts new file mode 100644 index 0000000000..b42ff3a60a --- /dev/null +++ b/packages/medusa/src/api-v2/store/customers/middlewares.ts @@ -0,0 +1,28 @@ +import { transformBody, transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { StorePostCustomersReq, StoreGetCustomersMeParams } from "./validators" +import authenticate from "../../../utils/authenticate-middleware" +import * as QueryConfig from "./query-config" + +export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: "ALL", + matcher: "/store/customers*", + middlewares: [authenticate("store", ["session", "bearer"])], + }, + { + method: ["POST"], + matcher: "/store/customers", + middlewares: [transformBody(StorePostCustomersReq)], + }, + { + method: ["GET"], + matcher: "/store/customers/me", + middlewares: [ + transformQuery( + StoreGetCustomersMeParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/store/customers/query-config.ts b/packages/medusa/src/api-v2/store/customers/query-config.ts new file mode 100644 index 0000000000..8a1828b5e7 --- /dev/null +++ b/packages/medusa/src/api-v2/store/customers/query-config.ts @@ -0,0 +1,24 @@ +import { CustomerDTO } from "@medusajs/types" + +export const defaultStoreCustomersRelations = [] +export const allowedStoreCustomersRelations = ["addresses", "groups"] +export const defaultStoreCustomersFields: (keyof CustomerDTO)[] = [ + "id", + "email", + "company_name", + "first_name", + "last_name", + "phone", + "metadata", + "created_by", + "deleted_at", + "created_at", + "updated_at", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultStoreCustomersFields, + defaultRelations: defaultStoreCustomersRelations, + allowedRelations: allowedStoreCustomersRelations, + isList: false, +} diff --git a/packages/medusa/src/api-v2/store/customers/route.ts b/packages/medusa/src/api-v2/store/customers/route.ts new file mode 100644 index 0000000000..b7600fc55d --- /dev/null +++ b/packages/medusa/src/api-v2/store/customers/route.ts @@ -0,0 +1,14 @@ +import { MedusaRequest, MedusaResponse } from "../../../types/routing" +import { createCustomerAccountWorkflow } from "@medusajs/core-flows" +import { CreateCustomerDTO } from "@medusajs/types" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const createCustomers = createCustomerAccountWorkflow(req.scope) + const customersData = req.validatedBody as CreateCustomerDTO + + const { result } = await createCustomers.run({ + input: { customersData, authUserId: req.auth_user!.id }, + }) + + res.status(200).json({ customer: result }) +} diff --git a/packages/medusa/src/api-v2/store/customers/validators.ts b/packages/medusa/src/api-v2/store/customers/validators.ts new file mode 100644 index 0000000000..c32d2634b5 --- /dev/null +++ b/packages/medusa/src/api-v2/store/customers/validators.ts @@ -0,0 +1,29 @@ +import { IsEmail, IsObject, IsOptional, IsString } from "class-validator" +import { FindParams } from "../../../types/common" + +export class StoreGetCustomersMeParams extends FindParams {} + +export class StorePostCustomersReq { + @IsString() + @IsOptional() + first_name: string + + @IsString() + @IsOptional() + last_name: string + + @IsEmail() + email: string + + @IsString() + @IsOptional() + phone?: string + + @IsString() + @IsOptional() + company_name?: string + + @IsObject() + @IsOptional() + metadata?: Record +} diff --git a/yarn.lock b/yarn.lock index 7a53985a90..5775225d01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7889,7 +7889,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/auth@workspace:packages/auth": +"@medusajs/auth@workspace:*, @medusajs/auth@workspace:packages/auth": version: 0.0.0-use.local resolution: "@medusajs/auth@workspace:packages/auth" dependencies: @@ -31377,6 +31377,7 @@ __metadata: "@babel/cli": ^7.12.10 "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 + "@medusajs/auth": "workspace:*" "@medusajs/cache-inmemory": "workspace:*" "@medusajs/customer": "workspace:^" "@medusajs/event-bus-local": "workspace:*"