DEV Community

Daragh Walsh
Daragh Walsh

Posted on

Handling password changes with Next.js, GraphQL and Mikro-ORM


Posted to my blog first! Please feel free to read it there.

Introduction

I am going to assume you have your application up and running, if you have not set it up yet I suggest just following along with their docs and then coming back to here. A great thing about Next.js is that you can get started very quickly.

For the login portion of this I will assume you have that handled, however I will probably write another post about handling that with this stack soon and link it here in case anyone struggles with it. It is also important to note that I will include examples and explanations of my backend code and although I understand that you may not be using this exact stack, it will be useful for explaining the logic behind my decisions.

Some of the main packages I am using are mikro-orm, type-graphql, urql, graphQL, Apollo server express, nodemailer, redis and uuid. I will again point this out in my code snippets as we go along.

Below are the steps we will take when a user wants to change their password.

Steps

  1. User selects forgot password on the website

  2. We check if the email is valid and in use

  3. We generate a token for this user

  4. We email the user a link to change the password with this token in the url

  5. The user submits the change password form

  6. We handle this password change on our backend and delete the token

Now let’s get started!

Backend Logic

When developing out certain features I like to have the backend logic roughly completed first and I then implement the frontend for it and making any necessary adjustments. As my backend uses graphQL the first step is to create my function which handles the user requesting an email to change their password.

My Context

I just want to place here my context, which is accessible in all my resolvers. The request and response objects are pretty standard and I got their types simply from hovering over them in VSCode. What important to note here is the em and redis objects. The em object is the ORM which is configured to connect to my database and the redis object is used to access my redis instance, which is where user sessions are stored.

  // my context
  context: ({ req, res }: MyContext) => ({
      em: orm.em,
      req,
      res,
      redis,
    }),
  //...

  // types.ts
  export type MyContext = {
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
  req: Request & { session: Express.Session };
  res: Response;
  redis: Redis;
};
Enter fullscreen mode Exit fullscreen mode

Forgot Password Mutation

This mutation takes an email parameter and will return a boolean depending on whether the email address was valid and if the link could be sent. Below you will see the definition of this function and a first look at the use of type-graphql.

@Mutation(() => Boolean)
  async forgotPassword(@Arg("email") email: string, @Ctx() { em, redis }: MyContext) {
  // ...
  // ...
  }
Enter fullscreen mode Exit fullscreen mode

The reason for using type-graphql is because it allows you to define schemas using only their decorators. It then allows us to inject dependencies into our resolvers and put auth guards in place, all while cutting down on code redundancy.

So the function takes an email parameter and accesses the em and redis objects (see here for clarification). The first thing we will do is check if the email address is in the database and return false if it is not present.

  // ...
const person = await em.findOne(User, { email });
    if (!person) {
      return false;
    }
  // ...
  }
Enter fullscreen mode Exit fullscreen mode

If the user is present we will generate a token using uuid's v4 function. This token is stored with the forgot-password: prefix and the key is the user's id field. The token will expire 3 days after the user makes the request.

// ...
const token = v4()
redis.set(
  `${FORGET_PASSWORD_PREFIX}${token}`,
  person.id,
  "ex",
  1000 * 60 * 60 * 24 * 3
) // 3 days
// ...
Enter fullscreen mode Exit fullscreen mode

Once the token is set and stored we will send the user the email with the link. This link will include the token and we use this to identify the user.

//..
await sendEmail(
      email,
      `<a href="http:localhost:3000/change-password/${token}">reset password</a>`
    );
    return true;
}
Enter fullscreen mode Exit fullscreen mode

The contents of the sendEmail function are taken directly from the example given in the Nodemailer docs. For clarity I will include it below.

let testAccount = await nodemailer.createTestAccount()
console.log("test account: ", testAccount)
let transporter = nodemailer.createTransport({
  host: "smtp.ethereal.email",
  port: 587,
  secure: false, // true for 465, false for other ports
  auth: {
    user: testAccount.user, // generated ethereal user
    pass: testAccount.pass, // generated ethereal password
  },
})

let info = await transporter.sendMail({
  from: '"Sample Person" <foo@example.com>', // sender address
  to: to, // list of receivers
  subject: "Change Password", // Subject line
  html,
})

console.log("Message sent: %s", info.messageId)

console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info))
Enter fullscreen mode Exit fullscreen mode

Forget Password Page

Now in our Next.js application, in the ./src/pages/ folder, we will create a change-password folder. In this folder we create a [token].tsx file.

(So the full path will be ./src/pages/change-password/[token].tsx)

Dynamic Routing

In Next.js the [param] file syntax is used for dynamic routes. This parameter will be sent as a query parameter to this page.

The next step is you must then decide when you will need to access this on the page via the props. This can be accomplished a handful of functions given to us by Next.js, however the use case will decide what function.

The three options too us are:

  1. getServerSideProps

  2. getStaticProps

I choose getServerSideProps as the data must be fetched at request time. We do not have a list of possible token's at build time.

The docs for getStaticProps states that we should only be using this function if:

The data required to render the page is available at build time ahead of a user’s request.

So in our [token].tsx file we start with the following scaffolding:

import { GetServerSideProps, NextPage } from "next";

const ChangePassword: NextPage<{token: string}> = ({token}) => {
  return (
    //..
    // form goes here
    //..
  )
};

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  return {
    props: {
      token: params.token,
    },
  };
};

export default ChangePassword;
Enter fullscreen mode Exit fullscreen mode

As we use dynamic routing, params contains this dynamic data. The reason we use params.token is because we named our file [token].tsx. If we were to name it [user-id] then the props passed would be token: params.user-id.

I then use Formik and urql to handle form state and sending the data to the server. Once the form is submitted without any errors back from the server the user is logged back in with the new password and redirected to the home page. This will now take us back to the backend for handling this data submission.

Handling the password change

Once we are back in our resolvers we create the changePassword resolver and it is important to take the time to define the type for the response to this. We can then make use of this type then when we generate our types in the frontend with the graphql-codegen package.

The UserResponse object will return an array of errors (each with a field and message field) and the user, with both having the option of being null. I choose an array of objects because I have a helper function for the frontend which will map the errors to the appropriate formik field and display them accordingly (I got this function from a Ben Awad video and I will include this below).

// toErrorMap.tsx
import { FieldError } from "../generated/graphql";

// map errors accordingly
// taken from Ben Awad video
export const toErrorMap = (errors: FieldError[]) => {
  const errorMap: Record<string, string> = {};
  errors.forEach(({ field, message }) => {
    errorMap[field] = message;
  });
  return errorMap;
};

// form.tsx
// usage example in a formik form
const form = () => {

  const handleSubmit = (values, {setErrors}) => {
    // send data via graphql
    const response = sendDataViaGraphl(values);
    if (response.data?.errors) {
      // if there’s errors
      setErrors(toErrorMap(response.data.errors))
    }
  }
  return (
  // form down here
  )
}
Enter fullscreen mode Exit fullscreen mode

Below is the schema typings I described above for the data returned from the mutation.

@ObjectType()
class FieldError {
  @Field()
  field: string
  @Field()
  message: string
}

@ObjectType()
class UserResponse {
  @Field(() => [FieldError], { nullable: true })
  errors?: FieldError[]

  @Field(() => User, { nullable: true })
  user?: User
}
Enter fullscreen mode Exit fullscreen mode

Now onto the changePassword function itself! It takes 2 arguments, token and newPassword. From our context again we take the redis, em and req objects. We also state our response type as the previously defined UserResponse type.

@Mutation(() => UserResponse)
  async changePassword(
    @Arg("token") token: string,
    @Arg("newPassword") newPassword: string,
    @Ctx() { redis, em, req }: MyContext
  ): Promise<UserResponse> {
  // ...
  // ...
  };
Enter fullscreen mode Exit fullscreen mode

The first thing we will check is the password length, it is just a basic security measure. Again make sure to note that this return matches the errors type we defined above.

// ...
{
  if (newPassword.length <= 5) {
    return {
      errors: [
        {
          field: "newPassword",
          message: "password is not long enough",
        },
      ],
    }
  }
}
// ..
Enter fullscreen mode Exit fullscreen mode

Next we move onto checking the redis database for the users id. Remember, we are accessing the redis object via context.

// ..
const key = FORGET_PASSWORD_PREFIX + token
const userId = await redis.get(key)
// ..
Enter fullscreen mode Exit fullscreen mode

Now we apply some more checks to see if the user exists both the redis and user database and if either fails we return the appropriate errors (and their corresponding messages).

// ..
if (!userId) {
  return {
    errors: [{ field: "token", message: "token expired" }],
  }
}
const user = await em.findOne(User, { id: parseInt(userId) })
if (!user) {
  return {
    errors: [{ field: "token", message: "token expired" }],
  }
}
// ..
Enter fullscreen mode Exit fullscreen mode

If there is no issues with finding the user, we then hash the password taken as a function argument and update the database.

As a security measure we delete the key from redis so the user (or someone else) cannot go back and use the same token again.

Finally we login the user using the req object via the use of a session and return the user.

// ..
user.password = await argon2.hash(newPassword);
    em.persistAndFlush(user);
    await redis.del(key);
    req.session.userId = user.id;

    return { user };
};
Enter fullscreen mode Exit fullscreen mode

And that's it! The user will be logged in on the frontend when they end up back on the home page.

Final Notes

Thanks for taking the time to read this. Should you have any feedback or questions please feel free to reach out and let me know!

Top comments (0)