diff --git a/src/api/authApi.ts b/src/api/authApi.ts index 7073c89..c415735 100644 --- a/src/api/authApi.ts +++ b/src/api/authApi.ts @@ -4,7 +4,7 @@ import { type LoginResponse, type MeResponse, type RegistrationPayload, - type User, + type ResetPasswordPayload, } from "../components/shared/types/AuthTypes"; import axiosInstance from "./axiosInstance"; @@ -13,7 +13,7 @@ export const loginRequest = async (data: LoginPayload) => { return res.data; }; -export const fetchMe = async (): Promise => { +export const fetchMe = async () => { const res = await axiosInstance.get("/api/auth/me"); return res.data.user; }; @@ -29,7 +29,23 @@ export const registrationRequest = async (data: RegistrationPayload) => { export const activateAccount = async (token: string) => { const res = await axiosInstance.post<{ message: string }>( "/api/auth/activate-account", - { token } + { token } + ); + return res.data; +}; + +export const requestPasswordReset = async (email: string) => { + const res = await axiosInstance.post<{ message: string }>( + "/api/auth/forgot-password", + { email } + ); + return res.data; +}; + +export const resetPassword = async (data: ResetPasswordPayload) => { + const res = await axiosInstance.post<{ message: string }>( + "/api/auth/reset-password", + data ); return res.data; }; diff --git a/src/components/shared/types/AuthTypes.ts b/src/components/shared/types/AuthTypes.ts index 29841cb..ea3b81a 100644 --- a/src/components/shared/types/AuthTypes.ts +++ b/src/components/shared/types/AuthTypes.ts @@ -33,3 +33,9 @@ export interface RegistrationPayload { export interface RegistrationResponse { message: string; } + +export interface ResetPasswordPayload { + token: string; + password: string; + password_confirmation: string; +} diff --git a/src/hooks/useRequestPasswordReset.ts b/src/hooks/useRequestPasswordReset.ts new file mode 100644 index 0000000..4225679 --- /dev/null +++ b/src/hooks/useRequestPasswordReset.ts @@ -0,0 +1,15 @@ +import { useMutation } from "@tanstack/react-query"; +import { requestPasswordReset } from "../api/authApi"; +import { toast } from "react-toastify"; + +export const useRequestPasswordReset = () => { + return useMutation({ + mutationFn: (email: string) => requestPasswordReset(email), + onSuccess: (data) => { + toast.success(data.message); + }, + onError: (err: any) => { + toast.error(err.response?.data?.message); + }, + }); +}; diff --git a/src/hooks/useResetPassword.ts b/src/hooks/useResetPassword.ts new file mode 100644 index 0000000..35ad449 --- /dev/null +++ b/src/hooks/useResetPassword.ts @@ -0,0 +1,16 @@ +import { useMutation } from "@tanstack/react-query"; +import type { ResetPasswordPayload } from "../components/shared/types/AuthTypes"; +import { resetPassword } from "../api/authApi"; +import { toast } from "react-toastify"; + +export const useResetPassword = () => { + return useMutation({ + mutationFn: (data: ResetPasswordPayload) => resetPassword(data), + onSuccess: (data) => { + toast.success(data.message); + }, + onError: (error: any) => { + toast.error(error.response?.data?.message); + }, + }); +}; diff --git a/src/pages/LoginPage/LoginPage.styles.ts b/src/pages/LoginPage/LoginPage.styles.ts new file mode 100644 index 0000000..27b8d6b --- /dev/null +++ b/src/pages/LoginPage/LoginPage.styles.ts @@ -0,0 +1,13 @@ +import styled from "@emotion/styled"; +import { Link } from "react-router-dom"; + +export const RegisterLink = styled(Link)({ + fontSize: "24px", + margin: "auto", + color: "#1565C0", + "&:hover":{ + color: "#4B2981" + } +}); + + diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx index ae3a873..e3c04cf 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/pages/LoginPage/LoginPage.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from "react"; import { useAuth } from "../../context/AuthContext"; import Container from "../../components/shared/Container"; import { Box, Button, TextField, Typography } from "@mui/material"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import { RegisterLink } from "./LoginPage.styles"; const LoginPage = () => { const { login, user } = useAuth(); @@ -46,6 +47,7 @@ const LoginPage = () => { margin="normal" required /> + Forgot Password? + + + Don't have and account? Register now! + + ); }; diff --git a/src/pages/NotFoundPage/NotFoundPage.tsx b/src/pages/NotFoundPage/NotFoundPage.tsx new file mode 100644 index 0000000..e2161c1 --- /dev/null +++ b/src/pages/NotFoundPage/NotFoundPage.tsx @@ -0,0 +1,22 @@ +import { Box, Typography } from "@mui/material"; +function NotFoundPage() { + return ( + + + 404 + + + Page Not Found + + The page you are looking for does not exist. + + ); +} + +export default NotFoundPage; diff --git a/src/pages/RequestResetPage/RequestResetPage.tsx b/src/pages/RequestResetPage/RequestResetPage.tsx new file mode 100644 index 0000000..edaea9b --- /dev/null +++ b/src/pages/RequestResetPage/RequestResetPage.tsx @@ -0,0 +1,61 @@ +import { Box, Button, TextField, Typography } from "@mui/material"; +import Container from "../../components/shared/Container"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { useEffect, useState } from "react"; +import { useRequestPasswordReset } from "../../hooks/useRequestPasswordReset"; + +const RequestResetPage = () => { + const { user } = useAuth(); + const [email, setEmail] = useState(""); + const navigate = useNavigate(); + const requestPasswordReset = useRequestPasswordReset(); + + useEffect(() => { + if (user) { + navigate("/"); + } + }, [user, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + requestPasswordReset.mutate(email, { + onSuccess: () => { + setEmail(""); + }, + }); + }; + + return ( + + + + Request Password Reset + +
+ setEmail(e.target.value)} + fullWidth + margin="normal" + required + /> + + + +
+
+ ); +}; + +export default RequestResetPage; diff --git a/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/src/pages/ResetPasswordPage/ResetPasswordPage.tsx new file mode 100644 index 0000000..415e6bf --- /dev/null +++ b/src/pages/ResetPasswordPage/ResetPasswordPage.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; +import { toast } from "react-toastify"; +import { useResetPassword } from "../../hooks/useResetPassword"; +import Container from "../../components/shared/Container"; +import { Box, Button, TextField, Typography } from "@mui/material"; + +const ResetPasswordPage = () => { + const navigate = useNavigate(); + const { user } = useAuth(); + const { token } = useParams<{ token: string }>(); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const resetMutation = useResetPassword(); + + useEffect(() => { + if (user) navigate("/"); + }, [user, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + if (!token) return; + + resetMutation.mutate( + { + token, + password, + password_confirmation: confirmPassword, + }, + { + onSuccess: () => { + navigate("/login"); + }, + } + ); + }; + return ( + + + + Reset Password + + +
+ setPassword(e.target.value)} + fullWidth + margin="normal" + required + /> + setConfirmPassword(e.target.value)} + fullWidth + margin="normal" + required + /> + + +
+
+ ); +}; + +export default ResetPasswordPage; diff --git a/src/pages/Unauthorized/Unauthorized.tsx b/src/pages/Unauthorized/Unauthorized.tsx index b8d16d5..fe13ee9 100644 --- a/src/pages/Unauthorized/Unauthorized.tsx +++ b/src/pages/Unauthorized/Unauthorized.tsx @@ -2,11 +2,12 @@ import { Box, Typography } from "@mui/material"; export default function Unauthorized() { return ( - 401 diff --git a/src/router/router.tsx b/src/router/router.tsx index e40c3bb..121d10d 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -6,6 +6,9 @@ import ProfilePage from "../pages/ProfilePage/Profilepage"; import { ProtectedRoute } from "../components/ProtectedRoute/ProtectedRoute"; import RegistrationPage from "../pages/RegistrationPage/RegistrationPage"; import ActivateAccountPage from "../pages/ActivateAccountPage/ActivateAccountPage"; +import RequestResetPage from "../pages/RequestResetPage/RequestResetPage"; +import NotFoundPage from "../pages/NotFoundPage/NotFoundPage"; +import ResetPasswordPage from "../pages/ResetPasswordPage/ResetPasswordPage"; const router = createBrowserRouter([ { @@ -36,6 +39,18 @@ const router = createBrowserRouter([ path: "/auth/email-activation/:token", element: , }, + { + path: "/reset-password", + element: , + }, + { + path: "/auth/reset-password/:token", + element: , + }, + { + path: "*", + element: , + }, ], }, ]);