feat: Refresh invite (#6469)
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
import { IAuthModuleService, IUserModuleService } from "@medusajs/types"
|
||||||
|
import { initDb, useDb } from "../../../environment-helpers/use-db"
|
||||||
|
|
||||||
|
import { AxiosInstance } from "axios"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { createAdminUser } from "../../helpers/create-admin-user"
|
||||||
|
import { getContainer } from "../../../environment-helpers/use-container"
|
||||||
|
import path from "path"
|
||||||
|
import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app"
|
||||||
|
import { useApi } from "../../../environment-helpers/use-api"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
|
const adminHeaders = {
|
||||||
|
headers: { "x-medusa-access-token": "test_token" },
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("POST /admin/invites/:id/resend", () => {
|
||||||
|
let dbConnection
|
||||||
|
let appContainer
|
||||||
|
let shutdownServer
|
||||||
|
let userModuleService: IUserModuleService
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd, env } as any)
|
||||||
|
shutdownServer = await startBootstrapApp({ cwd, env })
|
||||||
|
appContainer = getContainer()
|
||||||
|
userModuleService = appContainer.resolve(ModuleRegistrationName.USER)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
await shutdownServer()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should resend a single invite", async () => {
|
||||||
|
const invite = await userModuleService.createInvites({
|
||||||
|
email: "potential_member@test.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
const api = useApi()! as AxiosInstance
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/invites/${invite.id}/resend`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.invite.token).not.toEqual(invite.token)
|
||||||
|
expect(response.data.invite).toEqual(
|
||||||
|
expect.objectContaining({ email: "potential_member@test.com" })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from "./create-invites"
|
export * from "./create-invites"
|
||||||
export * from "./delete-invites"
|
export * from "./delete-invites"
|
||||||
|
export * from "./refresh-invite-tokens"
|
||||||
|
export * from "./validate-token"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { IUserModuleService, InviteDTO } from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
|
export const refreshInviteTokensStepId = "refresh-invite-tokens-step"
|
||||||
|
export const refreshInviteTokensStep = createStep(
|
||||||
|
refreshInviteTokensStepId,
|
||||||
|
async (input: string[], { container }) => {
|
||||||
|
const service: IUserModuleService = container.resolve(
|
||||||
|
ModuleRegistrationName.USER
|
||||||
|
)
|
||||||
|
|
||||||
|
const invites = await service.refreshInviteTokens(input)
|
||||||
|
|
||||||
|
return new StepResponse(invites)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./create-invites"
|
export * from "./create-invites"
|
||||||
export * from "./delete-invites"
|
export * from "./delete-invites"
|
||||||
export * from "./accept-invite"
|
export * from "./accept-invite"
|
||||||
|
export * from "./refresh-invite-tokens"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { InviteDTO, InviteWorkflow } from "@medusajs/types"
|
||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
import { refreshInviteTokensStep } from "../steps/refresh-invite-tokens"
|
||||||
|
|
||||||
|
export const refreshInviteTokensWorkflowId = "refresh-invite-tokens-workflow"
|
||||||
|
export const refreshInviteTokensWorkflow = createWorkflow(
|
||||||
|
refreshInviteTokensWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<InviteWorkflow.ResendInvitesWorkflowInputDTO>
|
||||||
|
): WorkflowData<InviteDTO[]> => {
|
||||||
|
return refreshInviteTokensStep(input.invite_ids)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||||
|
|
||||||
|
import { refreshInviteTokensWorkflow } from "@medusajs/core-flows"
|
||||||
|
|
||||||
|
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
|
const workflow = refreshInviteTokensWorkflow(req.scope)
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
invite_ids: [req.params.id],
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result: invites } = await workflow.run({ input })
|
||||||
|
|
||||||
|
res.status(200).json({ invite: invites[0] })
|
||||||
|
}
|
||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
UpdateUserDTO,
|
UpdateUserDTO,
|
||||||
} from "./mutations"
|
} from "./mutations"
|
||||||
import { FilterableUserProps, InviteDTO, UserDTO } from "./common"
|
import { FilterableUserProps, InviteDTO, UserDTO } from "./common"
|
||||||
|
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||||
|
|
||||||
import { Context } from "../shared-context"
|
import { Context } from "../shared-context"
|
||||||
import { FindConfig } from "../common"
|
import { FindConfig } from "../common"
|
||||||
import { IModuleService } from "../modules-sdk"
|
import { IModuleService } from "../modules-sdk"
|
||||||
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main service interface for the user module.
|
* The main service interface for the user module.
|
||||||
@@ -27,6 +27,11 @@ export interface IUserModuleService extends IModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<InviteDTO>
|
): Promise<InviteDTO>
|
||||||
|
|
||||||
|
refreshInviteTokens(
|
||||||
|
inviteIds: string[],
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<InviteDTO[]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method retrieves a user by its ID.
|
* This method retrieves a user by its ID.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./create-invite"
|
export * from "./create-invite"
|
||||||
export * from "./delete-invite"
|
export * from "./delete-invite"
|
||||||
export * from "./accept-invite"
|
export * from "./accept-invite"
|
||||||
|
export * from "./resend-invite"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ResendInvitesWorkflowInputDTO {
|
||||||
|
invite_ids: string[]
|
||||||
|
}
|
||||||
@@ -234,6 +234,18 @@ describe("UserModuleService - Invite", () => {
|
|||||||
}),
|
}),
|
||||||
eventName: UserEvents.invite_created,
|
eventName: UserEvents.invite_created,
|
||||||
}),
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
data: { id: "1" },
|
||||||
|
}),
|
||||||
|
eventName: "invite.token_generated",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
data: { id: "2" },
|
||||||
|
}),
|
||||||
|
eventName: "invite.token_generated",
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import * as crypto from "crypto"
|
||||||
|
|
||||||
import { Context, DAL } from "@medusajs/types"
|
import { Context, DAL } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
ModulesSdkUtils,
|
ModulesSdkUtils,
|
||||||
|
arrayDifference,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||||
|
|
||||||
@@ -13,8 +16,8 @@ type InjectedDependencies = {
|
|||||||
inviteRepository: DAL.RepositoryService
|
inviteRepository: DAL.RepositoryService
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7 days
|
// 1 day
|
||||||
const DEFAULT_VALID_INVITE_DURATION = 1000 * 60 * 60 * 24 * 7
|
const DEFAULT_VALID_INVITE_DURATION = 60 * 60 * 24
|
||||||
|
|
||||||
export default class InviteService<
|
export default class InviteService<
|
||||||
TEntity extends Invite = Invite
|
TEntity extends Invite = Invite
|
||||||
@@ -85,6 +88,48 @@ export default class InviteService<
|
|||||||
return await super.update(updates, context)
|
return await super.update(updates, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager("inviteRepository_")
|
||||||
|
async refreshInviteTokens(
|
||||||
|
inviteIds: string[],
|
||||||
|
context: Context = {}
|
||||||
|
): Promise<TEntity[]> {
|
||||||
|
const [invites, count] = await super.listAndCount(
|
||||||
|
{ id: inviteIds },
|
||||||
|
{},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
if (count !== inviteIds.length) {
|
||||||
|
const missing = arrayDifference(
|
||||||
|
inviteIds,
|
||||||
|
invites.map((invite) => invite.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`The following invites do not exist: ${missing.join(", ")}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresIn: number =
|
||||||
|
parseInt(this.getOption("valid_duration")) ||
|
||||||
|
DEFAULT_VALID_INVITE_DURATION
|
||||||
|
|
||||||
|
const updates = invites.map((invite) => {
|
||||||
|
return {
|
||||||
|
id: invite.id,
|
||||||
|
expires_at: new Date().setMilliseconds(
|
||||||
|
new Date().getMilliseconds() + expiresIn
|
||||||
|
),
|
||||||
|
token: this.generateToken({ id: invite.id }),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return await super.update(updates, context)
|
||||||
|
}
|
||||||
|
|
||||||
@InjectTransactionManager("inviteRepository_")
|
@InjectTransactionManager("inviteRepository_")
|
||||||
async validateInviteToken(
|
async validateInviteToken(
|
||||||
token: string,
|
token: string,
|
||||||
@@ -119,8 +164,10 @@ export default class InviteService<
|
|||||||
|
|
||||||
return jwt.sign(data, jwtSecret, {
|
return jwt.sign(data, jwtSecret, {
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
jwtid: crypto.randomUUID(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateToken(data: any): JwtPayload {
|
private validateToken(data: any): JwtPayload {
|
||||||
const jwtSecret = this.getOption("jwt_secret")
|
const jwtSecret = this.getOption("jwt_secret")
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
MedusaContext,
|
MedusaContext,
|
||||||
ModulesSdkUtils,
|
ModulesSdkUtils,
|
||||||
InjectManager,
|
InjectManager,
|
||||||
buildEventMessages,
|
|
||||||
CommonEvents,
|
CommonEvents,
|
||||||
UserEvents,
|
UserEvents,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -74,7 +73,53 @@ export default class UserModuleService<
|
|||||||
token: string,
|
token: string,
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<UserTypes.InviteDTO> {
|
): Promise<UserTypes.InviteDTO> {
|
||||||
return await this.inviteService_.validateInviteToken(token, sharedContext)
|
const invite = await this.inviteService_.validateInviteToken(
|
||||||
|
token,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.baseRepository_.serialize<UserTypes.InviteDTO>(invite, {
|
||||||
|
populate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectManager("baseRepository_")
|
||||||
|
@EmitEvents()
|
||||||
|
async refreshInviteTokens(
|
||||||
|
inviteIds: string[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<UserTypes.InviteDTO[]> {
|
||||||
|
const invites = await this.refreshInviteTokens_(inviteIds, sharedContext)
|
||||||
|
|
||||||
|
sharedContext.messageAggregator?.saveRawMessageData(
|
||||||
|
invites.map((invite) => ({
|
||||||
|
eventName: UserEvents.invite_token_generated,
|
||||||
|
metadata: {
|
||||||
|
service: this.constructor.name,
|
||||||
|
action: "token_generated",
|
||||||
|
object: "invite",
|
||||||
|
},
|
||||||
|
data: invite.id,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.baseRepository_.serialize<UserTypes.InviteDTO[]>(
|
||||||
|
invites,
|
||||||
|
{
|
||||||
|
populate: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
async refreshInviteTokens_(
|
||||||
|
inviteIds: string[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
return await this.inviteService_.refreshInviteTokens(
|
||||||
|
inviteIds,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
create(
|
create(
|
||||||
@@ -194,6 +239,18 @@ export default class UserModuleService<
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sharedContext.messageAggregator?.saveRawMessageData(
|
||||||
|
invites.map((invite) => ({
|
||||||
|
eventName: UserEvents.invite_token_generated,
|
||||||
|
metadata: {
|
||||||
|
service: this.constructor.name,
|
||||||
|
action: "token_generated",
|
||||||
|
object: "invite",
|
||||||
|
},
|
||||||
|
data: { id: invite.id },
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
return Array.isArray(data) ? serializedInvites : serializedInvites[0]
|
return Array.isArray(data) ? serializedInvites : serializedInvites[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +267,7 @@ export default class UserModuleService<
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return await this.inviteService_.create(toCreate)
|
return await this.inviteService_.create(toCreate, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInvites(
|
updateInvites(
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export const UserEvents = {
|
|||||||
updated: "user." + CommonEvents.UPDATED,
|
updated: "user." + CommonEvents.UPDATED,
|
||||||
invite_created: "invite." + CommonEvents.CREATED,
|
invite_created: "invite." + CommonEvents.CREATED,
|
||||||
invite_updated: "invite." + CommonEvents.UPDATED,
|
invite_updated: "invite." + CommonEvents.UPDATED,
|
||||||
|
invite_token_generated: "invite.token_generated",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user