From 0b23e7efb8c722001762069ed5fcabd00bc48001 Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Tue, 26 Mar 2024 21:02:56 +0100 Subject: [PATCH] feat: Support invites in CLI for V2 (#6798) **What** - Add invite support to cli for 2.0 - Allow email to be passed upon accepting an invite --- .../__tests__/invites/accept-invite.spec.ts | 39 +++++++++- .../__tests__/users/create-user.spec.ts | 33 -------- .../src/invite/workflows/accept-invite.ts | 10 +-- .../service-zone.spec.ts | 2 +- .../src/api-v2/admin/invites/accept/route.ts | 11 +-- .../src/api-v2/admin/invites/validators.ts | 12 +-- packages/medusa/src/commands/user.js | 75 +++++++++++-------- .../src/workflow/invite/accept-invite.ts | 1 + 8 files changed, 98 insertions(+), 85 deletions(-) delete mode 100644 integration-tests/modules/__tests__/users/create-user.spec.ts diff --git a/integration-tests/modules/__tests__/invites/accept-invite.spec.ts b/integration-tests/modules/__tests__/invites/accept-invite.spec.ts index 7eab2d8836..20294aa35c 100644 --- a/integration-tests/modules/__tests__/invites/accept-invite.spec.ts +++ b/integration-tests/modules/__tests__/invites/accept-invite.spec.ts @@ -1,7 +1,7 @@ -import { IUserModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAdminUser } from "../../../helpers/create-admin-user" +import { IUserModuleService } from "@medusajs/types" import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../helpers/create-admin-user" jest.setTimeout(50000) @@ -86,6 +86,41 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should accept an invite with email different from invite", async () => { + const invite = await userModuleService.createInvites({ + email: "potential_member@test.com", + }) + + const authResponse = await api.post(`/auth/admin/emailpass`, { + email: "some-email@test.com", + password: "supersecret", + }) + + expect(authResponse.status).toEqual(200) + const token = authResponse.data.token + + const acceptResponse = await api.post( + `/admin/invites/accept?token=${invite.token}`, + { + first_name: "John", + email: "some-email@test.com", + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + expect(acceptResponse.status).toEqual(200) + expect(acceptResponse.data.user).toEqual( + expect.objectContaining({ + email: "some-email@test.com", + first_name: "John", + }) + ) + }) }) }, }) diff --git a/integration-tests/modules/__tests__/users/create-user.spec.ts b/integration-tests/modules/__tests__/users/create-user.spec.ts deleted file mode 100644 index fb86248def..0000000000 --- a/integration-tests/modules/__tests__/users/create-user.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createAdminUser } from "../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -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("POST /admin/users", () => { - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, getContainer()) - }) - - it("create a user", async () => { - const body = { - email: "test_member@test.com", - } - - const response = await api.post(`/admin/users`, body, adminHeaders) - - expect(response.status).toEqual(200) - expect(response.data).toEqual({ - user: expect.objectContaining(body), - }) - }) - }) - }, -}) diff --git a/packages/core-flows/src/invite/workflows/accept-invite.ts b/packages/core-flows/src/invite/workflows/accept-invite.ts index 485eaa8c49..9a723faffc 100644 --- a/packages/core-flows/src/invite/workflows/accept-invite.ts +++ b/packages/core-flows/src/invite/workflows/accept-invite.ts @@ -1,14 +1,13 @@ -import { UserDTO } from "@medusajs/types" +import { InviteWorkflow, UserDTO } from "@medusajs/types" import { WorkflowData, createWorkflow, transform, } from "@medusajs/workflows-sdk" -import { createUsersStep } from "../../user" -import { validateTokenStep } from "../steps/validate-token" import { setAuthAppMetadataStep } from "../../auth" -import { InviteWorkflow } from "@medusajs/types" +import { createUsersStep } from "../../user" import { deleteInvitesStep } from "../steps" +import { validateTokenStep } from "../steps/validate-token" export const acceptInviteWorkflowId = "accept-invite-workflow" export const acceptInviteWorkflow = createWorkflow( @@ -16,7 +15,6 @@ export const acceptInviteWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowData => { - // validate token const invite = validateTokenStep(input.invite_token) const createUserInput = transform( @@ -25,7 +23,7 @@ export const acceptInviteWorkflow = createWorkflow( return [ { ...input.user, - email: invite.email, + email: input.user.email ?? invite.email, }, ] } diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts index 55a781cb9a..2af235e9f2 100644 --- a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts @@ -6,7 +6,7 @@ import { UpdateServiceZoneDTO, } from "@medusajs/types" import { GeoZoneType } from "@medusajs/utils" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" jest.setTimeout(100000) diff --git a/packages/medusa/src/api-v2/admin/invites/accept/route.ts b/packages/medusa/src/api-v2/admin/invites/accept/route.ts index de113a9a21..4cc9c8843f 100644 --- a/packages/medusa/src/api-v2/admin/invites/accept/route.ts +++ b/packages/medusa/src/api-v2/admin/invites/accept/route.ts @@ -1,11 +1,10 @@ +import { acceptInviteWorkflow } from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IUserModuleService, InviteWorkflow } from "@medusajs/types" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../types/routing" - -import { acceptInviteWorkflow } from "@medusajs/core-flows" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IUserModuleService, InviteWorkflow } from "@medusajs/types" import { AdminPostInvitesInviteAcceptReq } from "../validators" export const POST = async ( @@ -21,8 +20,6 @@ export const POST = async ( return } - const workflow = acceptInviteWorkflow(req.scope) - const input = { invite_token: req.filterableFields.token as string, auth_user_id: req.auth?.auth_user_id, @@ -31,7 +28,7 @@ export const POST = async ( let users try { - const { result } = await workflow.run({ input }) + const { result } = await acceptInviteWorkflow(req.scope).run({ input }) users = result } catch (e) { res.status(401).json({ message: "Unauthorized" }) diff --git a/packages/medusa/src/api-v2/admin/invites/validators.ts b/packages/medusa/src/api-v2/admin/invites/validators.ts index ddeae230ec..094dd4a4d0 100644 --- a/packages/medusa/src/api-v2/admin/invites/validators.ts +++ b/packages/medusa/src/api-v2/admin/invites/validators.ts @@ -77,11 +77,6 @@ export class AdminCreateInviteRequest { email: string } -/** - * Details of the use accepting the invite. - */ -export class AdminPostInvitesInviteAcceptUserReq {} - /** * @schema AdminPostInvitesInviteAcceptReq * type: object @@ -113,6 +108,13 @@ export class AdminPostInvitesInviteAcceptUserReq {} * format: password */ export class AdminPostInvitesInviteAcceptReq { + /** + * The invite's first name. + * If email is not passed, we default to using the email of the invite. + */ + @IsString() + @IsOptional() + email: string /** * The invite's first name. */ diff --git a/packages/medusa/src/commands/user.js b/packages/medusa/src/commands/user.js index a5039da207..ef9d630580 100644 --- a/packages/medusa/src/commands/user.js +++ b/packages/medusa/src/commands/user.js @@ -11,25 +11,38 @@ import featureFlagLoader from "../loaders/feature-flags" import configModuleLoader from "../loaders/config" import { MedusaV2Flag } from "@medusajs/utils" -// TEMP: Only supporting emailpass -const createV2User = async ({ email, password }, { container }) => { - const authService = container.resolve(ModuleRegistrationName.AUTH) +const useV2Command = async ( + { email, password, isInvite, provider = "emailpass" }, + { container } +) => { const userService = container.resolve(ModuleRegistrationName.USER) - const user = await userService.create({ email }) - const { authUser } = await authService.authenticate("emailpass", { - body: { - email, - password, - }, - authScope: "admin", - }) + const authService = container.resolve(ModuleRegistrationName.AUTH) - await authService.update({ - id: authUser.id, - app_metadata: { - user_id: user.id, - }, - }) + if (isInvite) { + // The invite flow only works with the V2 version of packages/admin-next/dashboard, so enable the V2 feature flag in admin before using this command + const invite = await userService.createInvites({ email }) + + Logger.info(` + Invite token: ${invite.token} + Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite.token}`) + } else { + const user = await userService.create({ email }) + + const { authUser } = await authService.authenticate(provider, { + body: { + email, + password, + }, + authScope: "admin", + }) + + await authService.update({ + id: authUser.id, + app_metadata: { + user_id: user.id, + }, + }) + } } export default async function ({ @@ -48,21 +61,21 @@ export default async function ({ expressApp: app, }) - if (invite) { - const inviteService = container.resolve("inviteService") - await inviteService.create(email, "admin") - const invite = await inviteService.list({ - user_email: email, - }) - Logger.info(` - Invite token: ${invite[0].token} - Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite[0].token}`) - } else { - const configModule = configModuleLoader(directory) - const featureFlagRouter = featureFlagLoader(configModule) + const configModule = configModuleLoader(directory) + const featureFlagRouter = featureFlagLoader(configModule) - if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - await createV2User({ email, password }, { container }) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + await useV2Command({ email, password, isInvite: invite }, { container }) + } else { + if (invite) { + const inviteService = container.resolve("inviteService") + await inviteService.create(email, "admin") + const invite = await inviteService.list({ + user_email: email, + }) + Logger.info(` + Invite token: ${invite[0].token} + Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite[0].token}`) } else { const userService = container.resolve("userService") await userService.create({ id, email }, password) diff --git a/packages/types/src/workflow/invite/accept-invite.ts b/packages/types/src/workflow/invite/accept-invite.ts index fdda032c4f..103d9e3dba 100644 --- a/packages/types/src/workflow/invite/accept-invite.ts +++ b/packages/types/src/workflow/invite/accept-invite.ts @@ -2,6 +2,7 @@ export interface AcceptInviteWorkflowInputDTO { invite_token: string auth_user_id: string user: { + email?: string first_name?: string | null last_name?: string | null avatar_url?: string | null