Feat(medusa, user, core-flows): User creation with invites (#6413)

**What**
- add accept invite endpoint 

**Invite accept flow**
- authenticate using the auth endpoint to create and auth-user
- invoke accept endpoint with token and info to create user
This commit is contained in:
Philip Korsholm
2024-02-19 05:29:15 +00:00
committed by GitHub
parent 5977a38ef4
commit f6d38163bb
28 changed files with 399 additions and 40 deletions
+1
View File
@@ -1 +1,2 @@
export { default as UserModuleService } from "./user-module"
export { default as InviteService } from "./invite"
+126
View File
@@ -0,0 +1,126 @@
import { Context, DAL } from "@medusajs/types"
import {
InjectTransactionManager,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import { Invite } from "@models"
import { InviteServiceTypes } from "@types"
import jwt, { JwtPayload } from "jsonwebtoken"
type InjectedDependencies = {
inviteRepository: DAL.RepositoryService
}
// 7 days
const DEFAULT_VALID_INVITE_DURATION = 1000 * 60 * 60 * 24 * 7
export default class InviteService<
TEntity extends Invite = Invite
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
Invite
)<TEntity> {
// eslint-disable-next-line max-len
protected readonly inviteRepository_: DAL.RepositoryService<TEntity>
protected options_: { jwt_secret: string; valid_duration: number } | undefined
constructor(container: InjectedDependencies) {
super(container)
this.inviteRepository_ = container.inviteRepository
}
public withModuleOptions(options: any) {
const service = new InviteService<TEntity>(this.__container__)
service.options_ = options
return service
}
private getOption(key: string) {
if (!this.options_) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Options are not configured for InviteService, call "withModuleOptions" and provide options`
)
}
return this.options_[key]
}
create(
data: InviteServiceTypes.CreateInviteDTO,
context?: Context
): Promise<TEntity>
create(
data: InviteServiceTypes.CreateInviteDTO[],
context?: Context
): Promise<TEntity[]>
@InjectTransactionManager("inviteRepository_")
async create(
data:
| InviteServiceTypes.CreateInviteDTO
| InviteServiceTypes.CreateInviteDTO[],
context: Context = {}
): Promise<TEntity | TEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
const invites = await super.create(data_, context)
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_")
async validateInviteToken(
token: string,
context?: Context
): Promise<TEntity> {
const decoded = this.validateToken(token)
return await super.retrieve(decoded.payload.id, {}, context)
}
private generateToken(data: any): string {
const jwtSecret: string = this.getOption("jwt_secret")
const expiresIn: number =
parseInt(this.getOption("valid_duration")) ||
DEFAULT_VALID_INVITE_DURATION
if (!jwtSecret) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No jwt_secret was provided in the UserModule's options. Please add one."
)
}
return jwt.sign(data, jwtSecret, {
expiresIn,
})
}
private validateToken(data: any): JwtPayload {
const jwtSecret = this.getOption("jwt_secret")
if (!jwtSecret) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No jwt_secret was provided in the UserModule's options. Please add one."
)
}
return jwt.verify(data, jwtSecret, { complete: true })
}
}
+17 -8
View File
@@ -10,16 +10,18 @@ import {
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { Invite, User } from "@models"
import InviteService from "./invite"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
userService: ModulesSdkTypes.InternalModuleService<any>
inviteService: ModulesSdkTypes.InternalModuleService<any>
inviteService: InviteService<any>
}
const generateMethodForModels = [Invite]
@@ -46,7 +48,7 @@ export default class UserModuleService<
protected baseRepository_: DAL.RepositoryService
protected readonly userService_: ModulesSdkTypes.InternalModuleService<TUser>
protected readonly inviteService_: ModulesSdkTypes.InternalModuleService<TInvite>
protected readonly inviteService_: InviteService<TInvite>
constructor(
{ userService, inviteService, baseRepository }: InjectedDependencies,
@@ -57,7 +59,17 @@ export default class UserModuleService<
this.baseRepository_ = baseRepository
this.userService_ = userService
this.inviteService_ = inviteService
this.inviteService_ = inviteService.withModuleOptions(
this.moduleDeclaration
)
}
@InjectTransactionManager("baseRepository_")
async validateInviteToken(
token: string,
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.InviteDTO> {
return await this.inviteService_.validateInviteToken(token, sharedContext)
}
create(
@@ -146,14 +158,11 @@ export default class UserModuleService<
data: UserTypes.CreateInviteDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TInvite[]> {
// expiration date in 10 days
const expirationDate = new Date().setDate(new Date().getDate() + 10)
const toCreate = data.map((invite) => {
return {
...invite,
expires_at: new Date(expirationDate),
token: "placeholder", // TODO: generate token
expires_at: new Date(),
token: "placeholder",
}
})