diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx
index 7cf53d6..9d01e4b 100644
--- a/frontend/src/Router.tsx
+++ b/frontend/src/Router.tsx
@@ -3,6 +3,12 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
import ScrollToTop from "./components/ScrollToTop";
import TopBar from "./components/TopBar";
import Register from "./pages/Register";
+import ConfirmEmail from "./pages/ConfirmEmail";
+import Login from "./pages/Login";
+import RequireAuth from "./components/RequireAuth";
+import ChangePassword from "./pages/ChangePassword";
+import ForgottenPassword from "./pages/ForgottenPassword";
+import ResetPassword from "./pages/ResetPassword";
const Router = () => (
@@ -10,6 +16,18 @@ const Router = () => (
} />
+ } />
+ } />
+
+
+
+ }
+ />
+ } />
+ } />
);
diff --git a/frontend/src/pages/ChangePassword.tsx b/frontend/src/pages/ChangePassword.tsx
new file mode 100644
index 0000000..d0c6e3f
--- /dev/null
+++ b/frontend/src/pages/ChangePassword.tsx
@@ -0,0 +1,90 @@
+import axios from "axios";
+import { Form, Formik, FormikHelpers } from "formik";
+import { useContext } from "react";
+import { useNavigate } from "react-router-dom";
+import * as yup from "yup";
+
+import FormActions from "src/components/FormActions";
+import LazyPasswordWithStrengthField from "src/components/LazyPasswordWithStrengthField";
+import PasswordField from "src/components/PasswordField";
+import Title from "src/components/Title";
+import { ToastContext } from "src/ToastContext";
+import { useMutation } from "src/query";
+
+interface IForm {
+ currentPassword: string;
+ newPassword: string;
+}
+
+const useChangePassword = () => {
+ const { addToast } = useContext(ToastContext);
+ const { mutateAsync: changePassword } = useMutation(
+ async (data: IForm) => await axios.put("/members/password/", data),
+ );
+ const navigate = useNavigate();
+
+ return async (data: IForm, { setFieldError }: FormikHelpers) => {
+ try {
+ await changePassword(data);
+ addToast("Changed", "success");
+ navigate("/");
+ } catch (error: any) {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 400) {
+ setFieldError("newPassword", "Password is too weak");
+ } else if (error.response?.status === 401) {
+ setFieldError("currentPassword", "Incorrect password");
+ }
+ } else {
+ addToast("Try again", "error");
+ }
+ }
+ };
+};
+
+const validationSchema = yup.object({
+ currentPassword: yup.string().required("Required"),
+ newPassword: yup.string().required("Required"),
+});
+
+const ChangePassword = () => {
+ const onSubmit = useChangePassword();
+
+ return (
+ <>
+
+
+ initialValues={{ currentPassword: "", newPassword: "" }}
+ onSubmit={onSubmit}
+ validationSchema={validationSchema}
+ >
+ {({ dirty, isSubmitting }) => (
+
+ )}
+
+ >
+ );
+};
+
+export default ChangePassword;
diff --git a/frontend/src/pages/ConfirmEmail.tsx b/frontend/src/pages/ConfirmEmail.tsx
new file mode 100644
index 0000000..4663142
--- /dev/null
+++ b/frontend/src/pages/ConfirmEmail.tsx
@@ -0,0 +1,44 @@
+import LinearProgress from "@mui/material/LinearProgress";
+import axios from "axios";
+import { useContext } from "react";
+import { useParams } from "react-router";
+import { Navigate } from "react-router-dom";
+
+import { useQuery } from "../query";
+import { ToastContext } from "../ToastContext";
+
+interface IParams {
+ token?: string;
+}
+
+const ConfirmEmail = () => {
+ const { addToast } = useContext(ToastContext);
+ const params = useParams() as IParams;
+ const token = params.token ?? "";
+ const { isLoading } = useQuery(
+ ["Email"],
+ async () => await axios.put("/members/email/", { token }),
+ {
+ onError: (error: any) => {
+ if (error.response?.status === 400) {
+ if (error.response?.data.code === "TOKEN_INVALID") {
+ addToast("Invalid token", "error");
+ } else if (error.response?.data.code === "TOKEN_EXPIRED") {
+ addToast("Token expired", "error");
+ }
+ } else {
+ addToast("Try again", "error");
+ }
+ },
+ onSuccess: () => addToast("Thanks", "success"),
+ },
+ );
+
+ if (isLoading) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+export default ConfirmEmail;
diff --git a/frontend/src/pages/ForgottenPassword.tsx b/frontend/src/pages/ForgottenPassword.tsx
new file mode 100644
index 0000000..e5ef44f
--- /dev/null
+++ b/frontend/src/pages/ForgottenPassword.tsx
@@ -0,0 +1,82 @@
+import axios from "axios";
+import { Form, Formik } from "formik";
+import { useContext } from "react";
+import { useLocation, useNavigate } from "react-router";
+import * as yup from "yup";
+
+import EmailField from "src/components/EmailField";
+import FormActions from "src/components/FormActions";
+import Title from "src/components/Title";
+import { useMutation } from "src/query";
+import { ToastContext } from "src/ToastContext";
+
+interface IForm {
+ email: string;
+}
+
+const useForgottenPassword = () => {
+ const navigate = useNavigate();
+ const { addToast } = useContext(ToastContext);
+
+ const { mutateAsync: forgottenPassword } = useMutation(
+ async (data: IForm) =>
+ await axios.post("/members/forgotten-password/", data),
+ );
+
+ return async (data: IForm) => {
+ try {
+ await forgottenPassword(data);
+ addToast("Reset link sent to your email", "success");
+ navigate("/login/");
+ } catch {
+ addToast("Try again", "error");
+ }
+ };
+};
+
+const validationSchema = yup.object({
+ email: yup.string().email("Email invalid").required("Required"),
+});
+
+const ForgottenPassword = () => {
+ const onSubmit = useForgottenPassword();
+ const location = useLocation();
+
+ return (
+ <>
+
+
+ initialValues={{
+ email: (location.state as any)?.email ?? "",
+ }}
+ onSubmit={onSubmit}
+ validationSchema={validationSchema}
+ >
+ {({ dirty, isSubmitting, values }) => (
+
+ )}
+
+ >
+ );
+};
+
+export default ForgottenPassword;
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
new file mode 100644
index 0000000..75b3225
--- /dev/null
+++ b/frontend/src/pages/Login.tsx
@@ -0,0 +1,99 @@
+import axios from "axios";
+import { Form, Formik, FormikHelpers } from "formik";
+import { useContext } from "react";
+import { useLocation, useNavigate } from "react-router";
+import * as yup from "yup";
+
+import { AuthContext } from "../AuthContext";
+import EmailField from "../components/EmailField";
+import FormActions from "../components/FormActions";
+import PasswordField from "../components/PasswordField";
+import Title from "../components/Title";
+import { ToastContext } from "../ToastContext";
+import { useMutation } from "../query";
+
+interface IForm {
+ email: string;
+ password: string;
+}
+
+const useLogin = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { addToast } = useContext(ToastContext);
+ const { setAuthenticated } = useContext(AuthContext);
+ const { mutateAsync: login } = useMutation(
+ async (data: IForm) => await axios.post("/sessions/", data),
+ );
+
+ return async (data: IForm, { setFieldError }: FormikHelpers) => {
+ try {
+ await login(data);
+ setAuthenticated(true);
+ navigate((location.state as any)?.from ?? "/");
+ } catch (error: any) {
+ if (error.response?.status === 401) {
+ setFieldError("email", "Invalid credentials");
+ setFieldError("password", "Invalid credentials");
+ } else {
+ addToast("Try again", "error");
+ }
+ }
+ };
+};
+
+const validationSchema = yup.object({
+ email: yup.string().email("Email invalid").required("Required"),
+ password: yup.string().required("Required"),
+});
+
+const Login = () => {
+ const onSubmit = useLogin();
+ const location = useLocation();
+
+ return (
+ <>
+
+
+ initialValues={{
+ email: (location.state as any)?.email ?? "",
+ password: "",
+ }}
+ onSubmit={onSubmit}
+ validationSchema={validationSchema}
+ >
+ {({ dirty, isSubmitting, values }) => (
+
+ )}
+
+ >
+ );
+};
+
+export default Login;
diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx
index d776085..33fec26 100644
--- a/frontend/src/pages/Register.tsx
+++ b/frontend/src/pages/Register.tsx
@@ -5,12 +5,12 @@ import { useNavigate } from "react-router";
import { useLocation } from "react-router-dom";
import * as yup from "yup";
-import EmailField from "../components/EmailField";
-import FormActions from "../components/FormActions";
-import LazyPasswordWithStrengthField from "../components/LazyPasswordWithStrengthField";
-import Title from "../components/Title";
-import { ToastContext } from "../ToastContext";
-import { useMutation } from "../query";
+import EmailField from "src/components/EmailField";
+import FormActions from "src/components/FormActions";
+import LazyPasswordWithStrengthField from "src/components/LazyPasswordWithStrengthField";
+import Title from "src/components/Title";
+import { ToastContext } from "src/ToastContext";
+import { useMutation } from "src/query";
interface IForm {
email: string;
diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx
new file mode 100644
index 0000000..4757d8c
--- /dev/null
+++ b/frontend/src/pages/ResetPassword.tsx
@@ -0,0 +1,90 @@
+import axios from "axios";
+import { Form, Formik, FormikHelpers } from "formik";
+import { useContext } from "react";
+import { useNavigate, useParams } from "react-router";
+import * as yup from "yup";
+
+import LazyPasswordWithStrengthField from "src/components/LazyPasswordWithStrengthField";
+import FormActions from "src/components/FormActions";
+import Title from "src/components/Title";
+import { useMutation } from "src/query";
+import { ToastContext } from "src/ToastContext";
+
+interface IForm {
+ password: string;
+}
+
+interface IParams {
+ token?: string;
+}
+
+const useResetPassword = () => {
+ const navigate = useNavigate();
+ const params = useParams() as IParams;
+ const token = params.token ?? "";
+ const { addToast } = useContext(ToastContext);
+
+ const { mutateAsync: reset } = useMutation(
+ async (password: string) =>
+ await axios.put("/members/reset-password/", { password, token }),
+ );
+
+ return async (data: IForm, { setFieldError }: FormikHelpers) => {
+ try {
+ await reset(data.password);
+ addToast("Success", "success");
+ navigate("/login/");
+ } catch (error: any) {
+ if (error.response?.status === 400) {
+ if (error.response?.data.code === "WEAK_PASSWORD") {
+ setFieldError("newPassword", "Password is too weak");
+ } else if (error.response?.data.code === "TOKEN_INVALID") {
+ addToast("Invalid token", "error");
+ } else if (error.response?.data.code === "TOKEN_EXPIRED") {
+ addToast("Token expired", "error");
+ }
+ } else {
+ addToast("Try again", "error");
+ }
+ }
+ };
+};
+
+const validationSchema = yup.object({
+ email: yup.string().email("Email invalid").required("Required"),
+});
+
+const ResetPassword = () => {
+ const onSubmit = useResetPassword();
+
+ return (
+ <>
+
+
+ initialValues={{ password: "" }}
+ onSubmit={onSubmit}
+ validationSchema={validationSchema}
+ >
+ {({ dirty, isSubmitting, values }) => (
+
+ )}
+
+ >
+ );
+};
+
+export default ResetPassword;