Change password

The user will need a way to change their password, which should only be available if they are logged in and they provide their existing password. In addition as they are logged in, we can help prompt them if they get their existing password wrong. Finally as with the registration and reset password pages we can use a metered password to let the user know how strong their new password is.

The following should be added to frontend/src/pages/ChangePassword.tsx,

import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import axios from "axios";
import { Form, Formik, FormikHelpers } from "formik";
import React, { Suspense, useContext } from "react";
import { useTranslation } from "react-i18next";
import * as yup from "yup";

import PasswordField from "src/components/PasswordField";
import SecondaryButton from "src/components/SecondaryButton";
import SubmitButton from "src/components/SubmitButton";
import { useMutation } from "src/query";
import { ToastContext } from "src/ToastContext";

const MeteredPasswordField = React.lazy(
  () => import("src/components/MeteredPasswordField"),
);

interface IForm {
  currentPassword: string;
  newPassword: string;
}

const ChangePassword = () => {
  const { t } = useTranslation();
  const { addToast } = useContext(ToastContext);

  const { mutateAsync: changePassword } = useMutation(async (data: IForm) => {
    await axios.put("/members/password/", data);
  });

  const validationSchema = yup.object({
    currentPassword: yup.string().required(t("generic.required")),
    newPassword: yup.string().required(t("generic.required")),
  });

  const onSubmit = async (
    data: IForm,
    { setFieldError }: FormikHelpers<IForm>,
  ) => {
    await changePassword(data, {
      onError: (error) => {
        if (error.response) {
          if (error.response.status === 400) {
            setFieldError("newPassword", t("generic.weakPassword"));
          } else if (error.response.status === 401) {
            setFieldError(
              "currentPassword",
              t("ChangePassword.incorrectPassword"),
            );
          } else {
            addToast({
              category: "error",
              message: t("generic.tryAgainError"),
            });
          }
        } else {
          addToast({ category: "error", message: t("generic.tryAgainError") });
        }
      },
    });
  };

  return (
    <>
      <Box mb={1} mt={2}>
        <Typography component="h1" variant="h5">
          {t("ChangePassword.lead")}
        </Typography>
      </Box>
      <Formik<IForm>
        initialValues={{
          currentPassword: "",
          newPassword: "",
        }}
        onSubmit={onSubmit}
        validationSchema={validationSchema}
      >
        {({ isSubmitting }) => (
          <Form>
            <PasswordField
              autoComplete="current-password"
              fullWidth={true}
              label={t("generic.password")}
              name="currentPassword"
              required={true}
            />
            <Suspense
              fallback={
                <PasswordField
                  autoComplete="new-password"
                  fullWidth={true}
                  label={t("ChangePassword.newPassword")}
                  name="newPassword"
                  required={true}
                />
              }
            >
              <MeteredPasswordField
                autoComplete="new-password"
                fullWidth={true}
                label={t("ChangePassword.newPassword")}
                name="newPassword"
                required={true}
              />
            </Suspense>
            <SubmitButton
              label={t("ChangePassword.change")}
              submitting={isSubmitting}
            />
            <SecondaryButton label={t("generic.back")} to="/todos/" />
          </Form>
        )}
      </Formik>
    </>
  );
};

export default ChangePassword;

Then we can add the page to the routing by adding the following to frontend/src/Router.tsx,

import PrivateRoute from "src/components/PrivateRoute";
import ChangePassword from "src/pages/ChangePassword";
...

const Router = () => (
  <BrowserRouter>
    ...
    <PrivateRoute exact={true} path="/change-password/">
      <ChangePassword />
    </PrivateRoute>
  </BrowserRouter>
);