Login

The login page exists for users to login to Tozo. To do this we need the user to enter their email, password, and optionally indicate they wish to be remembered and then we'll use the members API to log them in.

User's are likely to navigate between the registration, login, and forgotten password pages as they often can't remember if they have an account or what the password was. For this reason we can reduce their effort by persisting any email address they've typed in across these pages and offer links between the pages.

The following code to do all this should be placed in frontend/src/pages/Login.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, { useContext } from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router";
import * as yup from "yup";

import { AuthContext } from "src/AuthContext";
import CheckboxField from "src/components/CheckboxField";
import EmailField from "src/components/EmailField";
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";

interface IForm {
  email: string;
  password: string;
  remember: boolean;
}

interface ILocationState {
  email?: string;
  from?: string;
}

const Login = () => {
  const { t } = useTranslation();
  const history = useHistory();
  const location = useLocation<ILocationState>();
  const { setAuthenticated } = useContext(AuthContext);
  const { addToast } = useContext(ToastContext);

  const { mutateAsync: login } = useMutation(async (data: IForm) => {
    await axios.post("/sessions/", data);
  });

  const validationSchema = yup.object({
    email: yup
      .string()
      .email(t("generic.emailRequired"))
      .required(t("generic.required")),
    password: yup.string().required(t("generic.required")),
  });

  const onSubmit = async (
    data: IForm,
    { setFieldError }: FormikHelpers<IForm>,
  ) => {
    try {
      await login(data);
      setAuthenticated(true);
      history.push(location.state?.from ?? "/");
    } catch (error) {
      if (error.response?.status === 401) {
        setFieldError("email", t("Login.invalidCredentials"));
        setFieldError("password", t("Login.invalidCredentials"));
      } else {
        addToast({ category: "error", message: t("generic.tryAgainError") });
      }
    }
  };

  return (
    <>
      <Box mb={1} mt={2}>
        <Typography component="h1" variant="h5">
          {t("Login.lead")}
        </Typography>
      </Box>
      <Formik<IForm>
        initialValues={{
          email: location.state?.email ?? "",
          password: "",
          remember: false,
        }}
        onSubmit={onSubmit}
        validationSchema={validationSchema}
      >
        {({ isSubmitting, values }) => (
          <Form>
            <EmailField
              fullWidth={true}
              label={t("generic.email")}
              name="email"
              required={true}
            />
            <PasswordField
              fullWidth={true}
              label={t("generic.password")}
              name="password"
              required={true}
            />
            <CheckboxField
              fullWidth={true}
              label={t("Login.remember")}
              name="remember"
              required={true}
            />
            <SubmitButton
              label={t("generic.login")}
              submitting={isSubmitting}
            />
            <SecondaryButton
              label={t("generic.register")}
              to={{
                pathname: "/register/",
                state: { email: values.email },
              }}
            />
            <SecondaryButton
              label={t("generic.forgottenPassword")}
              to={{
                pathname: "/forgotten-password/",
                state: { email: values.email },
              }}
            />
          </Form>
        )}
      </Formik>
    </>
  );
};

export default Login;

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

import Login from "src/pages/Login";
...

const Router = () => (
  <BrowserRouter>
    ...
    <Route exact={true} path="/login/">
      <Login />
    </Route>
  </BrowserRouter>
);