Back to Medusa

{metadata.title}

www/apps/resources/app/commerce-modules/auth/reset-password/page.mdx

2.14.217.9 KB
Original Source

import { Prerequisites } from "docs-ui"

export const metadata = { title: Send Reset Password Email Notification, }

{metadata.title}

In this guide, you'll learn how to handle the auth.password_reset event to send a reset password email (or other notification type) to users.

<Note title="Looking for no-code docs?">

Refer to this Medusa Admin User Guide to learn how to reset your user admin password using the dashboard.

</Note>

Reset Password Flow Overview

Users of any actor type (admin, customer, or custom actor type) can request to reset their password. The flow for resetting a password is as follows:

  1. The user requests to reset their password either through the frontend (for example, Medusa Admin) or the Generate Reset Password Token API route.
  2. The Medusa application generates a password reset token and emits the auth.password_reset event.
    • At this point, you can handle the event to send a notification to the user with instructions on how to reset their password.
  3. The user receives the notification and clicks on the link to reset their password.

In this guide, you'll implement a subscriber that handles the auth.password_reset event to send an email notification to the user with instructions on how to reset their password.

After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa Admin, storefront, or API routes.


Prerequisites: Notification Module Provider

To send an email or notification to the user, you must have a Notification Module Provider set up.

Medusa provides providers like SendGrid and Resend, and you can also create your own custom provider.

Refer to the Notification Module documentation for a list of available providers and how to set them up.

Testing with the Local Notification Module Provider

For testing purposes, you can use the Local Notification Module Provider by adding this to your medusa-config.ts:

ts
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})

The Local provider logs email details to your terminal instead of sending actual emails, which is useful for development and testing.


Create the Reset Password Subscriber

To create a subscriber that handles the auth.password_reset event, create the file src/subscribers/password-reset.ts with the following content:

export const highlights=[ ["8", "data", "The data payload of the event."], ["9", "entity_id", "The user's identifier, which is the email when using the emailpass provider."], ["10", "token", "The password reset token."], ["11", "actor_type", "The user's actor type."], ["20", "urlPrefix", "Set the page's URL based on the user's actor type."], ["31", "createNotifications", "Send a notification to the user."], ["33", "email", "The channel to send the notification through."], ["35", "template", "The template defined in the third-party provider."], ["36", "data", "The data to pass to the template in the third-party provider."], ["38", "reset_url", "The frontend URL to redirect the user to reset their password."] ]

ts
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function resetPasswordTokenHandler({
  event: { data: {
    entity_id: email,
    token,
    actor_type,
  } },
  container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )
  const config = container.resolve("configModule")

  let urlPrefix = ""

  if (actor_type === "customer") {
    urlPrefix = config.admin.storefrontUrl || "https://storefront.com"
  } else {
    const backendUrl = config.admin.backendUrl !== "/" ? config.admin.backendUrl :
      "http://localhost:9000"
    const adminPath = config.admin.path
    urlPrefix = `${backendUrl}${adminPath}`
  }

  await notificationModuleService.createNotifications({
    to: email,
    channel: "email",
    // TODO replace with template ID in notification provider
    template: "password-reset",
    data: {
      // a URL to a frontend application
      reset_url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "auth.password_reset",
}

The subscriber receives the following data through the event payload:

  • entity_id: The identifier of the user. When using the emailpass provider, it's the user's email.
  • token: The token to reset the user's password.
  • actor_type: The user's actor type. For example, if the user is a customer, the actor_type is customer. If it's an admin user, the actor_type is user.
<Note>

This event's payload previously had an actorType field. It was renamed to actor_type after Medusa v2.0.7.

</Note>

Reset Password URL

Based on the user's actor type, you set the URL prefix to redirect the user to the appropriate frontend page to reset their password:

  • If the user is a customer, you set the URL prefix to the storefront URL.
  • If the user is an admin, you set the URL prefix to the backend URL, which is constructed from the config.admin.backendUrl and config.admin.path values.

Note that the Medusa Admin has a reset password form at /reset-password?token={token}&email={email}.

Notification Configurations

For the notification, you can configure the following fields:

  • to: The identifier to send the notification to, which in this case is the email.
  • channel: The channel to send the notification through, which in this case is email.
  • template: The template ID of the email to send. This ID depends on the Notification Module provider you use. For example, if you use SendGrid, this would be the ID of the SendGrid template.
  • data: The data payload to pass to the template. You can pass additional fields, if necessary.

Test It Out

After you set up the Notification Module Provider, create a template in the provider, and create the subscriber, you can test the reset password flow.

Start the Medusa application with the following command:

bash
npm run dev

Then, open the Medusa Admin (locally at http://localhost:9000/app) and click on "Reset" in the login form. Then, enter the email of the user you want to reset the password for.

Once you reset the password, you should see that the auth.password_reset event is emitted in the server's logs:

bash
info:    Processing auth.password_reset which has 1 subscribers

If you're using an email Notification Module Provider, check the user's email inbox for the reset password email with the link to reset their password.

If you're using the Local provider, check your terminal for the logged email details.


Next Steps: Implementing Frontend

In your frontend, you must have a page that accepts token and email query parameters.

The page shows the user password fields to enter their new password, then submits the new password, token, and email to the Reset Password Route.

The Medusa Admin already has a reset password page at /reset-password?token={token}&email={email}. So, you only need to implement this page in your storefront or custom admin dashboard.

Examples


Example Notification Templates

The following section provides example notification templates for some Notification Module Providers.

SendGrid

<Note>

Refer to the SendGrid Notification Module Provider documentation for more details on how to set up SendGrid.

</Note>

The following HTML template can be used with SendGrid to send a reset password email:

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reset Your Password</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
            background-color: #ffffff;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 465px;
            margin: 40px auto;
            border: 1px solid #eaeaea;
            border-radius: 5px;
            padding: 20px;
        }
        .header {
            text-align: center;
            margin: 30px 0;
        }
        .title {
            color: #000000;
            font-size: 24px;
            font-weight: normal;
            margin: 0;
        }
        .content {
            margin: 32px 0;
        }
        .text {
            color: #000000;
            font-size: 14px;
            line-height: 24px;
            margin: 0 0 16px 0;
        }
        .button-container {
            text-align: center;
            margin: 32px 0;
        }
        .reset-button {
            background-color: #000000;
            border-radius: 3px;
            color: #ffffff;
            font-size: 12px;
            font-weight: 600;
            text-decoration: none;
            text-align: center;
            padding: 12px 20px;
            display: inline-block;
        }
        .reset-button:hover {
            background-color: #333333;
        }
        .url-section {
            margin: 32px 0;
        }
        .url-link {
            color: #2563eb;
            text-decoration: none;
            font-size: 14px;
            line-height: 24px;
            word-break: break-all;
        }
        .disclaimer {
            margin: 32px 0;
        }
        .disclaimer-text {
            color: #666666;
            font-size: 12px;
            line-height: 24px;
            margin: 0 0 8px 0;
        }
        .security-footer {
            margin-top: 32px;
            padding-top: 20px;
            border-top: 1px solid #eaeaea;
        }
        .security-text {
            color: #666666;
            font-size: 12px;
            line-height: 24px;
            margin: 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1 class="title">Reset Your Password</h1>
        </div>

        <div class="content">
            <p class="text">
                Hello{{#if email}} {{email}}{{/if}},
            </p>
            <p class="text">
                We received a request to reset your password. Click the button below to create a new password for your account.
            </p>
        </div>

        <div class="button-container">
            <a href="{{reset_url}}" class="reset-button">
                Reset Password
            </a>
        </div>

        <div class="url-section">
            <p class="text">
                Or copy and paste this URL into your browser:
            </p>
            <a href="{{reset_url}}" class="url-link">
                {{reset_url}}
            </a>
        </div>

        <div class="disclaimer">
            <p class="disclaimer-text">
                This password reset link will expire soon for security reasons.
            </p>
            <p class="disclaimer-text">
                If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
            </p>
        </div>

        <div class="security-footer">
            <p class="security-text">
                For security reasons, never share this reset link with anyone. If you're having trouble with the button above, copy and paste the URL into your web browser.
            </p>
        </div>
    </div>
</body>
</html>

Make sure to pass the reset_url variable to the template, which contains the URL to reset the password.

You can also customize the template further to show other information.

Resend

If you've integrated Resend as explained in the Resend Integration Guide, you can add a new template for password reset emails at src/modules/resend/emails/password-reset.tsx:

tsx
import { 
  Text, 
  Container, 
  Heading, 
  Html, 
  Section, 
  Tailwind, 
  Head, 
  Preview, 
  Body, 
  Link,
  Button, 
} from "@react-email/components"

type PasswordResetEmailProps = {
  reset_url: string
  email?: string
}

function PasswordResetEmailComponent({ reset_url, email }: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Tailwind>
        <Body className="bg-white my-auto mx-auto font-sans px-2">
          <Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
            <Section className="mt-[32px]">
              <Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
                Reset Your Password
              </Heading>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-black text-[14px] leading-[24px]">
                Hello{email ? ` ${email}` : ""},
              </Text>
              <Text className="text-black text-[14px] leading-[24px]">
                We received a request to reset your password. Click the button below to create a new password for your account.
              </Text>
            </Section>

            <Section className="text-center mt-[32px] mb-[32px]">
              <Button
                className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
                href={reset_url}
              >
                Reset Password
              </Button>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-black text-[14px] leading-[24px]">
                Or copy and paste this URL into your browser:
              </Text>
              <Link
                href={reset_url}
                className="text-blue-600 no-underline text-[14px] leading-[24px] break-all"
              >
                {reset_url}
              </Link>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-[#666666] text-[12px] leading-[24px]">
                This password reset link will expire soon for security reasons.
              </Text>
              <Text className="text-[#666666] text-[12px] leading-[24px] mt-2">
                If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
              </Text>
            </Section>

            <Section className="mt-[32px] pt-[20px] border-t border-solid border-[#eaeaea]">
              <Text className="text-[#666666] text-[12px] leading-[24px]">
                For security reasons, never share this reset link with anyone. If you're having trouble with the button above, copy and paste the URL into your web browser.
              </Text>
            </Section>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}

export const passwordResetEmail = (props: PasswordResetEmailProps) => (
  <PasswordResetEmailComponent {...props} />
)

// Mock data for preview/development
const mockPasswordReset: PasswordResetEmailProps = {
  reset_url: "https://your-app.com/reset-password?token=sample-reset-token-123",
  email: "[email protected]",
}

export default () => <PasswordResetEmailComponent {...mockPasswordReset} />

Feel free to customize the email template further to match your branding and style, or to add additional information.

Then, in the Resend Module's service at src/modules/resend/service.ts, add the new template to the templates object and Templates type:

ts
// other imports...
import { passwordResetEmail } from "./emails/password-reset"

enum Templates {
  // ...
  PASSWORD_RESET = "password-reset",
}

const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
  // ...
  [Templates.PASSWORD_RESET]: passwordResetEmail,
}

Finally, find the getTemplateSubject function in the ResendNotificationProviderService and add a case for the USER_INVITED template:

ts
class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...

  private getTemplateSubject(template: Templates) {
    // ...
    switch (template) {
      // ...
      case Templates.PASSWORD_RESET:
        return "Reset Your Password"
    }
  }
}