@perseidesjs/medusa-plugin-otp
v1.0.0
Published
Implements OTP for your next Medusa project
Downloads
24
Readme
npm install @perseidesjs/medusa-plugin-otp
const plugins = [
`medusa-fulfillment-manual`,
`medusa-payment-manual`,
`@perseidesjs/medusa-plugin-otp`,
]
const plugins = [
`medusa-fulfillment-manual`,
`medusa-payment-manual`,
{
resolve: `@perseidesjs/medusa-otp`,
/** @type {import('@perseidesjs/medusa-plugin-otp').PluginOptions} */
options: {
ttl: 30, // In seconds, the time to live of the OTP before expiration
digits: 6, // The number of digits of the OTP (e.g. 123456)
},
},
]
import { Customer as MedusaCustomer } from '@medusajs/medusa'
import { Column, Entity } from 'typeorm'
@Entity()
export class Customer extends MedusaCustomer {
@Column({ type: 'text' })
otp_secret: string
}
Don't to create the migration for this model :
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddOtpSecretToCustomer1719843922955 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "customer" ADD "otp_secret" text`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "customer" DROP COLUMN "otp_secret"`)
}
}
// src/subscribers/customer-created.ts
import { Logger, SubscriberArgs, SubscriberConfig } from '@medusajs/medusa'
import type { TOTPService } from '@perseidesjs/medusa-plugin-otp'
import { EntityManager } from 'typeorm'
import CustomerService from '../services/customer'
type CustomerCreatedEventData = {
id: string // Customer ID
}
/**
* This subscriber will be triggered when a new customer is created.
* It will add an OTP secret to the customer for the sake of OTP authentication.
*/
export default async function setOtpSecretForCustomerHandler({
data,
container,
}: SubscriberArgs<CustomerCreatedEventData>) {
const logger = container.resolve<Logger>('logger')
const activityId = logger.activity(
`Adding OTP secret to customer with ID : ${data.id}`,
)
const customerService = container.resolve<CustomerService>('customerService')
const totpService = container.resolve<TOTPService>('totpService')
const otpSecret = totpService.generateSecret()
await customerService.update(data.id, {
otp_secret: otpSecret,
})
logger.success(
activityId,
`Successfully added OTP secret to customer with ID : ${data.id}!`,
)
}
export const config: SubscriberConfig = {
event: CustomerService.Events.CREATED,
context: {
subscriberId: 'set-otp-for-customer-handler',
},
}
// src/api/store/auth/route.ts
import {
StorePostAuthReq,
defaultStoreCustomersFields,
validator,
type AuthService,
type MedusaRequest,
type MedusaResponse,
} from '@medusajs/medusa'
import { defaultRelations } from '@medusajs/medusa/dist/api/routes/store/auth'
import type { TOTPService } from '@perseidesjs/medusa-plugin-otp'
import { EntityManager } from 'typeorm'
import CustomerService from '../../../services/customer'
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const validated = await validator(StorePostAuthReq, req.body)
const authService: AuthService = req.scope.resolve('authService')
const manager: EntityManager = req.scope.resolve('manager')
const result = await manager.transaction(async (transactionManager) => {
return await authService
.withTransaction(transactionManager)
.authenticateCustomer(validated.email, validated.password)
})
if (!result.success) {
res.sendStatus(401)
return
}
const customerService: CustomerService = req.scope.resolve('customerService')
const totpService: TOTPService = req.scope.resolve('totpService')
const customer = await customerService.retrieve(result.customer?.id || '', {
relations: defaultRelations,
select: [...defaultStoreCustomersFields, 'otp_secret'],
})
const otp = await totpService.generate(customer.id, customer.otp_secret)
const { otp_secret, ...rest } = customer // We omit the otp_secret from the response, you can also handle this in the CustomerService
res.json({ customer: rest })
}
// src/subscribers/otp-generated.ts
import type { Logger, SubscriberArgs, SubscriberConfig } from "@medusajs/medusa";
import { TOTPService } from "@perseidesjs/medusa-plugin-otp";
import type CustomerService from "../services/customer";
/**
* Send the OTP to the customer whenever the TOTP is generated.
*/
export default async function sendTOTPToCustomerHandler({
data,
container
}: SubscriberArgs<{ key: string }>) { // The key here is the customer ID
const logger = container.resolve<Logger>("logger")
const customerService = container.resolve<CustomerService>("customerService")
const customer = await customerService.retrieve(data.key).catch((e) => {
// In case you are using multiple OTP, if it fails it means the key is invalid / not a customer ID
logger.failure(activityId, `An error occured while retrieving the customer with ID : ${data.key}!`)
throw e
})
const activityId = logger.activity(`Sending OTP to customer with ID : ${customer.id}`)
// Use your NotificationService here to send the OTP to the customer (e.g. SendGrid)
logger.success(activityId, `Successfully sent OTP to customer with ID : ${customer.id}!`)
}
export const config: SubscriberConfig = {
event: TOTPService.Events.GENERATED,
context: {
subscriberId: 'send-totp-to-customer-handler'
}
}
// src/api/store/auth/otp/route.ts
import { validator, type MedusaRequest, type MedusaResponse } from "@medusajs/medusa";
import { IsEmail, IsString, MaxLength, MinLength } from "class-validator";
import type { TOTPService } from "@perseidesjs/medusa-plugin-otp";
import type CustomerService from "../../../../services/customer";
export async function POST(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const validated = await validator(StoreVerifyOTP, req.body);
const customerService = req.scope.resolve<CustomerService>("customerService");
const totpService = req.scope.resolve<TOTPService>("totpService");
const customer = await customerService.retrieveRegisteredByEmail(validated.email);
const isValid = await totpService.verify(customer.id, validated.otp)
if (!isValid) {
res.status(400).send({ error: "OTP is invalid" });
return
}
// Set customer id on session, this is stored on the server (connect_sid).
req.session.customer_id = customer.id;
res.status(200).json({ customer })
}
class StoreVerifyOTP {
@IsString()
otp: string;
@IsEmail()
email: string;
}