1.0.0 • Published 1 year ago

@perseidesjs/medusa-plugin-otp v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago
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;
}