feat: Refresh invite (#6469)

This commit is contained in:
Philip Korsholm
2024-02-27 07:16:52 +00:00
committed by GitHub
parent 7bddb58542
commit e747f9d4aa
13 changed files with 249 additions and 6 deletions
@@ -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] })
}
+6 -1
View File
@@ -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",
}),
]) ])
}) })
}) })
+49 -2
View File
@@ -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")
+60 -3
View File
@@ -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(
+1
View File
@@ -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",
} }