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
This commit is contained in:
Oli Juhl authored and GitHub committed 2024-03-26 20:02:56 +00:00
1 parent 16860cc883
commit 0b23e7efb8
8 files changed
+77 -64

No files matched your search

@@ -1,7 +1,7 @@
import { IUserModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk" 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 { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../helpers/create-admin-user"
jest.setTimeout(50000) 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",
})
)
})
}) })
}, },
}) })
@@ -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),
})
})
})
},
})
@@ -1,14 +1,13 @@
import { UserDTO } from "@medusajs/types" import { InviteWorkflow, UserDTO } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
createWorkflow, createWorkflow,
transform, transform,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { createUsersStep } from "../../user"
import { validateTokenStep } from "../steps/validate-token"
import { setAuthAppMetadataStep } from "../../auth" import { setAuthAppMetadataStep } from "../../auth"
import { InviteWorkflow } from "@medusajs/types" import { createUsersStep } from "../../user"
import { deleteInvitesStep } from "../steps" import { deleteInvitesStep } from "../steps"
import { validateTokenStep } from "../steps/validate-token"
export const acceptInviteWorkflowId = "accept-invite-workflow" export const acceptInviteWorkflowId = "accept-invite-workflow"
export const acceptInviteWorkflow = createWorkflow( export const acceptInviteWorkflow = createWorkflow(
@@ -16,7 +15,6 @@ export const acceptInviteWorkflow = createWorkflow(
( (
input: WorkflowData<InviteWorkflow.AcceptInviteWorkflowInputDTO> input: WorkflowData<InviteWorkflow.AcceptInviteWorkflowInputDTO>
): WorkflowData<UserDTO[]> => { ): WorkflowData<UserDTO[]> => {
// validate token
const invite = validateTokenStep(input.invite_token) const invite = validateTokenStep(input.invite_token)
const createUserInput = transform( const createUserInput = transform(
@@ -25,7 +23,7 @@ export const acceptInviteWorkflow = createWorkflow(
return [ return [
{ {
...input.user, ...input.user,
email: invite.email, email: input.user.email ?? invite.email,
}, },
] ]
} }
@@ -6,7 +6,7 @@ import {
UpdateServiceZoneDTO, UpdateServiceZoneDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { GeoZoneType } from "@medusajs/utils" import { GeoZoneType } from "@medusajs/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -1,11 +1,10 @@
import { acceptInviteWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IUserModuleService, InviteWorkflow } from "@medusajs/types"
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse, MedusaResponse,
} from "../../../../types/routing" } 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" import { AdminPostInvitesInviteAcceptReq } from "../validators"
export const POST = async ( export const POST = async (
@@ -21,8 +20,6 @@ export const POST = async (
return return
} }
const workflow = acceptInviteWorkflow(req.scope)
const input = { const input = {
invite_token: req.filterableFields.token as string, invite_token: req.filterableFields.token as string,
auth_user_id: req.auth?.auth_user_id, auth_user_id: req.auth?.auth_user_id,
@@ -31,7 +28,7 @@ export const POST = async (
let users let users
try { try {
const { result } = await workflow.run({ input }) const { result } = await acceptInviteWorkflow(req.scope).run({ input })
users = result users = result
} catch (e) { } catch (e) {
res.status(401).json({ message: "Unauthorized" }) res.status(401).json({ message: "Unauthorized" })
@@ -77,11 +77,6 @@ export class AdminCreateInviteRequest {
email: string email: string
} }
/**
* Details of the use accepting the invite.
*/
export class AdminPostInvitesInviteAcceptUserReq {}
/** /**
* @schema AdminPostInvitesInviteAcceptReq * @schema AdminPostInvitesInviteAcceptReq
* type: object * type: object
@@ -113,6 +108,13 @@ export class AdminPostInvitesInviteAcceptUserReq {}
* format: password * format: password
*/ */
export class AdminPostInvitesInviteAcceptReq { 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. * The invite's first name.
*/ */
+23 -10
View File
@@ -11,12 +11,24 @@ import featureFlagLoader from "../loaders/feature-flags"
import configModuleLoader from "../loaders/config" import configModuleLoader from "../loaders/config"
import { MedusaV2Flag } from "@medusajs/utils" import { MedusaV2Flag } from "@medusajs/utils"
// TEMP: Only supporting emailpass const useV2Command = async (
const createV2User = async ({ email, password }, { container }) => { { email, password, isInvite, provider = "emailpass" },
const authService = container.resolve(ModuleRegistrationName.AUTH) { container }
) => {
const userService = container.resolve(ModuleRegistrationName.USER) const userService = container.resolve(ModuleRegistrationName.USER)
const authService = container.resolve(ModuleRegistrationName.AUTH)
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 user = await userService.create({ email })
const { authUser } = await authService.authenticate("emailpass", {
const { authUser } = await authService.authenticate(provider, {
body: { body: {
email, email,
password, password,
@@ -31,6 +43,7 @@ const createV2User = async ({ email, password }, { container }) => {
}, },
}) })
} }
}
export default async function ({ export default async function ({
directory, directory,
@@ -48,6 +61,12 @@ export default async function ({
expressApp: app, expressApp: app,
}) })
const configModule = configModuleLoader(directory)
const featureFlagRouter = featureFlagLoader(configModule)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
await useV2Command({ email, password, isInvite: invite }, { container })
} else {
if (invite) { if (invite) {
const inviteService = container.resolve("inviteService") const inviteService = container.resolve("inviteService")
await inviteService.create(email, "admin") await inviteService.create(email, "admin")
@@ -57,12 +76,6 @@ export default async function ({
Logger.info(` Logger.info(`
Invite token: ${invite[0].token} Invite token: ${invite[0].token}
Open the invite in Medusa Admin at: [your-admin-url]/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)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
await createV2User({ email, password }, { container })
} else { } else {
const userService = container.resolve("userService") const userService = container.resolve("userService")
await userService.create({ id, email }, password) await userService.create({ id, email }, password)
@@ -2,6 +2,7 @@ export interface AcceptInviteWorkflowInputDTO {
invite_token: string invite_token: string
auth_user_id: string auth_user_id: string
user: { user: {
email?: string
first_name?: string | null first_name?: string | null
last_name?: string | null last_name?: string | null
avatar_url?: string | null avatar_url?: string | null