diff --git a/integration-tests/helpers/create-admin-user.ts b/integration-tests/helpers/create-admin-user.ts index dbd9608b7e..36810c3b9b 100644 --- a/integration-tests/helpers/create-admin-user.ts +++ b/integration-tests/helpers/create-admin-user.ts @@ -1,6 +1,7 @@ import { IAuthModuleService, IUserModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/utils" import jwt from "jsonwebtoken" +import Scrypt from "scrypt-kdf" import { getContainer } from "../environment-helpers/use-container" export const adminHeaders = { @@ -26,13 +27,16 @@ export const createAdminUser = async ( email: "admin@medusa.js", }) + const hashConfig = { logN: 15, r: 8, p: 1 } + const passwordHash = await Scrypt.kdf("somepassword", hashConfig) + const authIdentity = await authModule.createAuthIdentities({ provider_identities: [ { provider: "emailpass", entity_id: "admin@medusa.js", provider_metadata: { - password: "somepassword", + password: passwordHash.toString("base64"), }, }, ], @@ -55,5 +59,5 @@ export const createAdminUser = async ( adminHeaders.headers["authorization"] = `Bearer ${token}` - return { user } + return { user, authIdentity } } diff --git a/integration-tests/http/__tests__/auth/admin/auth.spec.ts b/integration-tests/http/__tests__/auth/admin/auth.spec.ts index 1774b88a19..43bca91115 100644 --- a/integration-tests/http/__tests__/auth/admin/auth.spec.ts +++ b/integration-tests/http/__tests__/auth/admin/auth.spec.ts @@ -1,6 +1,6 @@ import { generateResetPasswordTokenWorkflow } from "@medusajs/core-flows" -import { medusaIntegrationTestRunner } from "medusa-test-utils" import jwt from "jsonwebtoken" +import { medusaIntegrationTestRunner } from "medusa-test-utils" import { adminHeaders, createAdminUser, diff --git a/integration-tests/http/__tests__/customer/admin/customer.spec.ts b/integration-tests/http/__tests__/customer/admin/customer.spec.ts index 0dd5227685..8fb02867f4 100644 --- a/integration-tests/http/__tests__/customer/admin/customer.spec.ts +++ b/integration-tests/http/__tests__/customer/admin/customer.spec.ts @@ -1,3 +1,6 @@ +import { IAuthModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import jwt from "jsonwebtoken" import { medusaIntegrationTestRunner } from "medusa-test-utils" import { adminHeaders, @@ -13,9 +16,10 @@ medusaIntegrationTestRunner({ let customer3 let customer4 let customer5 + let container beforeEach(async () => { - const appContainer = getContainer() - await createAdminUser(dbConnection, adminHeaders, appContainer) + container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) customer1 = ( await api.post( @@ -392,5 +396,63 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("DELETE /admin/customers/:id", () => { + it("should delete a customer and update auth identity", async () => { + const registeredCustomerToken = ( + await api.post("/auth/customer/emailpass/register", { + email: "test@email.com", + password: "password", + }) + ).data.token + + const customer = ( + await api.post( + "/store/customers", + { + email: "test@email.com", + }, + { + headers: { + Authorization: `Bearer ${registeredCustomerToken}`, + }, + } + ) + ).data.customer + + const response = await api.delete( + `/admin/customers/${customer.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data).toEqual( + expect.objectContaining({ + id: customer.id, + deleted: true, + object: "customer", + }) + ) + + const { auth_identity_id } = jwt.decode(registeredCustomerToken) + + const authModule: IAuthModuleService = container.resolve( + ModuleRegistrationName.AUTH + ) + + const authIdentity = await authModule.retrieveAuthIdentity( + auth_identity_id + ) + + expect(authIdentity).toEqual( + expect.objectContaining({ + id: authIdentity.id, + app_metadata: expect.not.objectContaining({ + customer_id: expect.any(String), + }), + }) + ) + }) + }) }, }) diff --git a/integration-tests/http/__tests__/user/admin/user.spec.ts b/integration-tests/http/__tests__/user/admin/user.spec.ts index 2c07ad680b..b5eb5a18d6 100644 --- a/integration-tests/http/__tests__/user/admin/user.spec.ts +++ b/integration-tests/http/__tests__/user/admin/user.spec.ts @@ -1,3 +1,5 @@ +import { IAuthModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" import { adminHeaders, @@ -8,17 +10,18 @@ jest.setTimeout(30000) medusaIntegrationTestRunner({ testSuite: ({ dbConnection, getContainer, api }) => { - let user + let user, container, authIdentity beforeEach(async () => { - const container = getContainer() - const { user: adminUser } = await createAdminUser( + container = getContainer() + const { user: adminUser, authIdentity: authId } = await createAdminUser( dbConnection, adminHeaders, container ) user = adminUser + authIdentity = authId }) describe("GET /admin/users/:id", () => { @@ -102,20 +105,76 @@ medusaIntegrationTestRunner({ }) describe("DELETE /admin/users", () => { - it("Deletes a user", async () => { - const userId = "member-user" - + it("Deletes a user and updates associated auth identity", async () => { const response = await api.delete( - `/admin/users/${userId}`, + `/admin/users/${user.id}`, adminHeaders ) expect(response.status).toEqual(200) expect(response.data).toEqual({ - id: userId, + id: user.id, object: "user", deleted: true, }) + + const authModule: IAuthModuleService = container.resolve( + ModuleRegistrationName.AUTH + ) + + const updatedAuthIdentity = await authModule.retrieveAuthIdentity( + authIdentity.id + ) + + // Ensure the auth identity has been updated to not contain the user's id + expect(updatedAuthIdentity).toEqual( + expect.objectContaining({ + id: authIdentity.id, + app_metadata: expect.not.objectContaining({ + user_id: user.id, + }), + }) + ) + + // Authentication should still succeed + const authenticateToken = ( + await api.post(`/auth/user/emailpass`, { + email: user.email, + password: "somepassword", + }) + ).data.token + + expect(authenticateToken).toEqual(expect.any(String)) + + // However, it should not be possible to access routes any longer + const meResponse = await api + .get(`/admin/users/me`, { + headers: { + authorization: `Bearer ${authenticateToken}`, + }, + }) + .catch((e) => e) + + expect(meResponse.response.status).toEqual(401) + }) + + it("throws if you attempt to delete another user", async () => { + const userModule = container.resolve(ModuleRegistrationName.USER) + + const userTwo = await userModule.createUsers({ + email: "test@test.com", + password: "test", + role: "member", + }) + + const error = await api + .delete(`/admin/users/${userTwo.id}`, adminHeaders) + .catch((e) => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.message).toEqual( + "You are not allowed to delete other users" + ) }) // TODO: Migrate when analytics config is implemented in 2.0 diff --git a/integration-tests/modules/__tests__/users/delete-user.spec.ts b/integration-tests/modules/__tests__/users/delete-user.spec.ts deleted file mode 100644 index 83c74d01ce..0000000000 --- a/integration-tests/modules/__tests__/users/delete-user.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IUserModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { createAdminUser } from "../../../helpers/create-admin-user" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("DELETE /admin/users/:id", () => { - let appContainer - let userModuleService: IUserModuleService - - beforeAll(async () => { - appContainer = getContainer() - userModuleService = appContainer.resolve(ModuleRegistrationName.USER) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should delete a single user", async () => { - const user = await userModuleService.createUsers({ - email: "member@test.com", - }) - - const response = await api.delete( - `/admin/users/${user.id}`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data).toEqual({ - id: user.id, - object: "user", - deleted: true, - }) - - const { response: deletedResponse } = await api - .get(`/admin/users/${user.id}`, adminHeaders) - .catch((e) => e) - - expect(deletedResponse.status).toEqual(404) - expect(deletedResponse.data.type).toEqual("not_found") - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/users/list-users.spec.ts b/integration-tests/modules/__tests__/users/list-users.spec.ts deleted file mode 100644 index aaafa7706f..0000000000 --- a/integration-tests/modules/__tests__/users/list-users.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { IUserModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { createAdminUser } from "../../../helpers/create-admin-user" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("GET /admin/users", () => { - let appContainer - let userModuleService: IUserModuleService - - beforeAll(async () => { - appContainer = getContainer() - userModuleService = appContainer.resolve(ModuleRegistrationName.USER) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should list users", async () => { - await userModuleService.createUsers([ - { - email: "member@test.com", - }, - ]) - - const response = await api.get(`/admin/users`, adminHeaders) - - expect(response.status).toEqual(200) - expect(response.data).toEqual({ - users: expect.arrayContaining([ - expect.objectContaining({ - email: "admin@medusa.js", - }), - expect.objectContaining({ email: "member@test.com" }), - ]), - count: 2, - offset: 0, - limit: 50, - }) - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/users/retrieve-user.spec.ts b/integration-tests/modules/__tests__/users/retrieve-user.spec.ts deleted file mode 100644 index 6d911ea5df..0000000000 --- a/integration-tests/modules/__tests__/users/retrieve-user.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IUserModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { createAdminUser } from "../../../helpers/create-admin-user" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("GET /admin/users/:id", () => { - let appContainer - let userModuleService: IUserModuleService - - beforeAll(async () => { - appContainer = getContainer() - userModuleService = appContainer.resolve(ModuleRegistrationName.USER) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should retrieve a single user", async () => { - const user = await userModuleService.createUsers({ - email: "member@test.com", - }) - - const response = await api.get(`/admin/users/${user.id}`, adminHeaders) - - expect(response.status).toEqual(200) - expect(response.data.user).toEqual( - expect.objectContaining({ email: "member@test.com" }) - ) - }) - }) - }, -}) diff --git a/packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts b/packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts index 3bd25be381..d853385b4f 100644 --- a/packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts +++ b/packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts @@ -6,7 +6,7 @@ import { isDefined, ModuleRegistrationName } from "@medusajs/utils" export type SetAuthAppMetadataStepInput = { authIdentityId: string actorType: string - value: string + value: string | null // null means delete the key } export const setAuthAppMetadataStepId = "set-auth-app-metadata" @@ -24,10 +24,13 @@ export const setAuthAppMetadataStep = createStep( const authIdentity = await service.retrieveAuthIdentity(data.authIdentityId) const appMetadata = authIdentity.app_metadata || {} - if (isDefined(appMetadata[key])) { + + // If the value is null, we are deleting the association with an actor + if (isDefined(appMetadata[key]) && data.value !== null) { throw new Error(`Key ${key} already exists in app metadata`) } + const oldValue = appMetadata[key] appMetadata[key] = data.value await service.updateAuthIdentities({ @@ -38,14 +41,16 @@ export const setAuthAppMetadataStep = createStep( return new StepResponse(authIdentity, { id: authIdentity.id, key: key, + value: data.value, + oldValue, }) }, - async (idAndKey, { container }) => { - if (!idAndKey) { + async (idAndKeyAndValue, { container }) => { + if (!idAndKeyAndValue) { return } - const { id, key } = idAndKey + const { id, key, oldValue, value } = idAndKeyAndValue const service = container.resolve( ModuleRegistrationName.AUTH @@ -54,7 +59,11 @@ export const setAuthAppMetadataStep = createStep( const authIdentity = await service.retrieveAuthIdentity(id) const appMetadata = authIdentity.app_metadata || {} - if (isDefined(appMetadata[key])) { + + // If the value is null, we WERE deleting the association with an actor, so we need to restore it + if (value === null) { + appMetadata[key] = oldValue + } else { delete appMetadata[key] } diff --git a/packages/core/core-flows/src/customer/workflows/index.ts b/packages/core/core-flows/src/customer/workflows/index.ts index 97c7505c37..35fbedce24 100644 --- a/packages/core/core-flows/src/customer/workflows/index.ts +++ b/packages/core/core-flows/src/customer/workflows/index.ts @@ -1,7 +1,9 @@ -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 "./create-customer-account" +export * from "./create-customers" export * from "./delete-addresses" +export * from "./delete-customers" +export * from "./remove-customer-account" +export * from "./update-addresses" +export * from "./update-customers" + diff --git a/packages/core/core-flows/src/customer/workflows/remove-customer-account.ts b/packages/core/core-flows/src/customer/workflows/remove-customer-account.ts new file mode 100644 index 0000000000..830ab20b64 --- /dev/null +++ b/packages/core/core-flows/src/customer/workflows/remove-customer-account.ts @@ -0,0 +1,79 @@ +import { MedusaError } from "@medusajs/utils" +import { + WorkflowData, + WorkflowResponse, + createWorkflow, + transform, + when, +} from "@medusajs/workflows-sdk" +import { setAuthAppMetadataStep } from "../../auth" +import { useRemoteQueryStep } from "../../common" +import { deleteCustomersWorkflow } from "./delete-customers" + +export type RemoveCustomerAccountWorkflowInput = { + customerId: string +} +export const removeCustomerAccountWorkflowId = "remove-customer-account" +/** + * This workflow deletes a user and remove the association in the auth identity. + */ +export const removeCustomerAccountWorkflow = createWorkflow( + removeCustomerAccountWorkflowId, + ( + input: WorkflowData + ): WorkflowResponse => { + const customers = useRemoteQueryStep({ + entry_point: "customer", + fields: ["id", "has_account"], + variables: { + id: input.customerId, + }, + }).config({ name: "get-customer" }) + + deleteCustomersWorkflow.runAsStep({ + input: { + ids: [input.customerId], + }, + }) + + when({ customers }, ({ customers }) => { + return !!customers[0]?.has_account + }).then(() => { + const authIdentities = useRemoteQueryStep({ + entry_point: "auth_identity", + fields: ["id"], + variables: { + filters: { + app_metadata: { + customer_id: input.customerId, + }, + }, + }, + }) + + const authIdentity = transform( + { authIdentities }, + ({ authIdentities }) => { + const authIdentity = authIdentities[0] + + if (!authIdentity) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Auth identity not found" + ) + } + + return authIdentity + } + ) + + setAuthAppMetadataStep({ + authIdentityId: authIdentity.id, + actorType: "customer", + value: null, + }) + }) + + return new WorkflowResponse(input.customerId) + } +) diff --git a/packages/core/core-flows/src/user/workflows/index.ts b/packages/core/core-flows/src/user/workflows/index.ts index 5dd813b7a9..1856a7688a 100644 --- a/packages/core/core-flows/src/user/workflows/index.ts +++ b/packages/core/core-flows/src/user/workflows/index.ts @@ -1,5 +1,6 @@ export * from "./create-user-account" export * from "./create-users" export * from "./delete-users" +export * from "./remove-user-account" export * from "./update-users" diff --git a/packages/core/core-flows/src/user/workflows/remove-user-account.ts b/packages/core/core-flows/src/user/workflows/remove-user-account.ts new file mode 100644 index 0000000000..0052943876 --- /dev/null +++ b/packages/core/core-flows/src/user/workflows/remove-user-account.ts @@ -0,0 +1,61 @@ +import { + WorkflowData, + WorkflowResponse, + createWorkflow, + transform, + when, +} from "@medusajs/workflows-sdk" +import { setAuthAppMetadataStep } from "../../auth" +import { useRemoteQueryStep } from "../../common" +import { deleteUsersWorkflow } from "./delete-users" + +export type RemoveUserAccountWorkflowInput = { + userId: string +} +export const removeUserAccountWorkflowId = "remove-user-account" +/** + * This workflow deletes a user and remove the association in the auth identity. + */ +export const removeUserAccountWorkflow = createWorkflow( + removeUserAccountWorkflowId, + ( + input: WorkflowData + ): WorkflowResponse => { + deleteUsersWorkflow.runAsStep({ + input: { + ids: [input.userId], + }, + }) + + const authIdentities = useRemoteQueryStep({ + entry_point: "auth_identity", + fields: ["id"], + variables: { + filters: { + app_metadata: { + user_id: input.userId, + }, + }, + }, + }) + + const authIdentity = transform( + { authIdentities, input }, + ({ authIdentities }) => { + return authIdentities[0] + } + ) + + when({ authIdentity }, ({ authIdentity }) => { + return !!authIdentity + }).then(() => { + setAuthAppMetadataStep({ + authIdentityId: authIdentity.id, + actorType: "user", + value: null, + }) + }) + + return new WorkflowResponse(input.userId) + } +) diff --git a/packages/medusa/src/api/admin/customers/[id]/route.ts b/packages/medusa/src/api/admin/customers/[id]/route.ts index dddf084a21..f2c675c843 100644 --- a/packages/medusa/src/api/admin/customers/[id]/route.ts +++ b/packages/medusa/src/api/admin/customers/[id]/route.ts @@ -1,5 +1,5 @@ import { - deleteCustomersWorkflow, + removeCustomerAccountWorkflow, updateCustomersWorkflow, } from "@medusajs/core-flows" import { AdditionalData, HttpTypes } from "@medusajs/types" @@ -68,10 +68,11 @@ export const DELETE = async ( res: MedusaResponse ) => { const id = req.params.id - const deleteCustomers = deleteCustomersWorkflow(req.scope) - await deleteCustomers.run({ - input: { ids: [id] }, + await removeCustomerAccountWorkflow(req.scope).run({ + input: { + customerId: id, + }, }) res.status(200).json({ diff --git a/packages/medusa/src/api/admin/users/[id]/route.ts b/packages/medusa/src/api/admin/users/[id]/route.ts index fb579930f8..c6ee0ccd08 100644 --- a/packages/medusa/src/api/admin/users/[id]/route.ts +++ b/packages/medusa/src/api/admin/users/[id]/route.ts @@ -1,4 +1,7 @@ -import { deleteUsersWorkflow, updateUsersWorkflow } from "@medusajs/core-flows" +import { + removeUserAccountWorkflow, + updateUsersWorkflow, +} from "@medusajs/core-flows" import { HttpTypes, UpdateUserDTO } from "@medusajs/types" import { AuthenticatedMedusaRequest, @@ -10,8 +13,8 @@ import { MedusaError, remoteQueryObjectFromString, } from "@medusajs/utils" -import { AdminUpdateUserType } from "../validators" import { refetchUser } from "../helpers" +import { AdminUpdateUserType } from "../validators" // Get user export const GET = async ( @@ -71,10 +74,19 @@ export const DELETE = async ( res: MedusaResponse ) => { const { id } = req.params - const workflow = deleteUsersWorkflow(req.scope) + const { actor_id } = req.auth_context + + if (actor_id !== id) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "You are not allowed to delete other users" + ) + } + + const workflow = removeUserAccountWorkflow(req.scope) await workflow.run({ - input: { ids: [id] }, + input: { userId: id }, }) res.status(200).json({