fix: Update auth app_metadata when deleting users + customers (#9041)

* wip

* more work

* working on stuff

* more

* fix test

* remove incorrect test

* fix test

* fix: Only allow deletion of yourself

* remove redundant tests
This commit is contained in:
Oli Juhl
2024-09-10 19:58:16 +02:00
committed by GitHub
parent e9e0267aa8
commit 4bf42f7889
14 changed files with 322 additions and 183 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<IAuthModuleService>(
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]
}

View File

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

View File

@@ -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<RemoveCustomerAccountWorkflowInput>
): WorkflowResponse<string> => {
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)
}
)

View File

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

View File

@@ -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<RemoveUserAccountWorkflowInput>
): WorkflowResponse<string> => {
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)
}
)

View File

@@ -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<HttpTypes.AdminCustomerDeleteResponse>
) => {
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({

View File

@@ -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<HttpTypes.AdminUserDeleteResponse>
) => {
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({