From 4be15e7cea41f2d73c0180ecc49f1679321b7cba Mon Sep 17 00:00:00 2001 From: pacifiquemboni Date: Sun, 21 Jul 2024 17:12:39 +0200 Subject: [PATCH] Delivers [#187355054] updating password after x amount of time Fixing how user navigate to the profile Fixing how user navigate to the profile Fixing Profile css and error message --- .env.example | 2 +- src/App.tsx | 3 + src/Pages/Login/Login.tsx | 234 ++++++++++-------- .../resetPassword/ResetPasswordPage.scss | 12 +- src/Pages/resetPassword/updatePassXAmount.tsx | 186 ++++++++++++++ src/components/AvailableProduct/product.tsx | 13 +- src/components/NavBar.tsx | 13 +- src/components/personalInfo/personalInfo.tsx | 2 +- .../personalInfo/personalInfoStyles.scss | 5 + src/components/services/wishlistService.tsx | 32 ++- src/redux/slices/cartSlice.ts | 70 +++--- 11 files changed, 413 insertions(+), 159 deletions(-) create mode 100644 src/Pages/resetPassword/updatePassXAmount.tsx diff --git a/.env.example b/.env.example index 3172d30..c513e99 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ BACKEND_URL = 'your backend url' GOOGLE_CLIENT_ID='your google client id' REACT_APP_BACKEND_URL="YOUR_BACKEND_URL" - +REACT_APP_PASSWORD_EXPIRATION_PERIOD_MINUTES="minutes you need your password to be expired " diff --git a/src/App.tsx b/src/App.tsx index b95470a..feafadb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,7 @@ import Orders from './components/Orders/Orders'; import SingleProductPage from './Pages/SingleProductPage/SingleProductPage'; import BuySingleItem from './Pages/BuySingleItem/BuySingleItem'; import AddProduct from './Pages/BuyerDashboard/BuyerDashboard'; +import UpdatePasswordAfterXAmountOfTime from './Pages/resetPassword/updatePassXAmount'; const App: React.FC = () => ( @@ -50,6 +51,8 @@ const App: React.FC = () => ( } /> } /> } /> + } /> + } /> }/> diff --git a/src/Pages/Login/Login.tsx b/src/Pages/Login/Login.tsx index d45725a..ef40f26 100644 --- a/src/Pages/Login/Login.tsx +++ b/src/Pages/Login/Login.tsx @@ -29,6 +29,8 @@ const validatePassword = (password: string): boolean => { interface DecodedToken { userId: string; role: string; + passwordLastChanged: string; + expirationPeriod: number; } const Login: React.FC = () => { @@ -70,10 +72,13 @@ const Login: React.FC = () => { } setFormErrors({}); try { - const resultAction = await dispatch(loginUser({ - email, password, - userId: undefined - })); + const resultAction = await dispatch( + loginUser({ + email, + password, + userId: undefined, + }) + ); if (loginUser.fulfilled.match(resultAction)) { if (resultAction.payload.userId) { navigate(`/verify/${resultAction.payload.userId}`); @@ -82,7 +87,39 @@ const Login: React.FC = () => { if (token) { const decodedToken = decodeToken(token); if (decodedToken) { - if (decodedToken.role === "buyer") { + const lastPasswordChangeDate = new Date( + decodedToken.passwordLastChanged + ); + + const minutes = process.env + .REACT_APP_PASSWORD_EXPIRATION_PERIOD_MINUTES + ? parseInt( + process.env.REACT_APP_PASSWORD_EXPIRATION_PERIOD_MINUTES, + 10 + ) + : 0; + // const expirationPeriod = minutes ? parseInt(minutes, 10) : 0; + console.log( + "password expiration", + process.env.REACT_APP_PASSWORD_EXPIRATION_PERIOD_MINUTES + ); + console.log("lastpassword changed", lastPasswordChangeDate); + + const currentTime = new Date(); + const timeInMillis = minutes * 60 * 1000; // Correct conversion from minutes to milliseconds + console.log("current time", currentTime); + console.log("time in mills", timeInMillis); + console.log( + "different in time", + currentTime.getTime() - lastPasswordChangeDate.getTime() + ); + if ( + currentTime.getTime() - lastPasswordChangeDate.getTime() > + timeInMillis + ) { + navigate(`/update/new-password?q=${token}`); + }else{ + if (decodedToken.role === "buyer") { navigate(`/${decodedToken.userId}`); } if (decodedToken.role === "seller") { @@ -91,6 +128,9 @@ const Login: React.FC = () => { if (decodedToken.role === "admin") { navigate(`/adminDash/${decodedToken.userId}`); } + } + + } } } @@ -122,113 +162,103 @@ const Login: React.FC = () => { }, }); - useEffect(() => { - if (isSuccessfully || isSucceeded) { - // Decode token here and redirect - const token = localStorage.getItem("token"); // Assuming token is stored in localStorage - if (token) { - const decodedToken = decodeToken(token); - if (decodedToken) { - if (decodedToken.role === "buyer") { - navigate(`/${decodedToken.userId}`); - } - if (decodedToken.role === "seller") { - navigate(`/sellerDash/${decodedToken.userId}`); - } - if (decodedToken.role === "admin") { - navigate(`/adminDash/${decodedToken.userId}`); - } - } else { - // Handle invalid token or decoding failure - console.error("Failed to decode token."); - } - } else { - console.error("Token not found in localStorage."); - } - } - }, [isSuccessfully, isSucceeded, navigate]); + return (
-
-
-

Login into your account

- -
- - -
-
- -
+
+ +

Login into your account

+ +
+ - setShowPassword(!showPassword)} - />
- {formErrors.password && ( - {formErrors.password} - )} -
- Forgot password? - - - -

Or

-
- -
-
-

- Don't have an account? Signup -

-
- -
-
-

Welcome to OnesAndZeroes

- This is vendor svg -

We Deliver Anywhere in the World

+ +

Or

+
+ +
+
+

+ Don't have an account? Signup +

+
+ +
+
+

Welcome to OnesAndZeroes

+ This is vendor svg +

We Deliver Anywhere in the World

+
+ {loading && } + {!loading && error && ( + + )} + {!loading && isError && ( + + )}
- {loading && } - {!loading && error && } - {!loading && isError && } -
); }; -export default Login; \ No newline at end of file +export default Login; diff --git a/src/Pages/resetPassword/ResetPasswordPage.scss b/src/Pages/resetPassword/ResetPasswordPage.scss index ca3229b..1b7a3f5 100644 --- a/src/Pages/resetPassword/ResetPasswordPage.scss +++ b/src/Pages/resetPassword/ResetPasswordPage.scss @@ -22,7 +22,10 @@ body { margin: 100px auto; padding: 20px; display: grid; - +.alert{ + left: 10%; + right: 10%; +} label { margin: .4rem 0; display: block; @@ -100,4 +103,11 @@ body { background-color: $primary-color; } } + @media screen and (max-width: 768px) { + .alert{ + font-size: smaller; + left: 2%; + right: 2%; + } + } } diff --git a/src/Pages/resetPassword/updatePassXAmount.tsx b/src/Pages/resetPassword/updatePassXAmount.tsx new file mode 100644 index 0000000..ac35ef8 --- /dev/null +++ b/src/Pages/resetPassword/updatePassXAmount.tsx @@ -0,0 +1,186 @@ +import React, { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import axios from "axios"; +import "./ResetPasswordPage.scss"; +import Header from "../../components/Header"; +import { BACKEND_URL } from "../../constants/api"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +const UpdatePasswordAfterXAmountOfTime: React.FC = () => { + const location = useLocation(); + const navigate = useNavigate(); + const queryParams = new URLSearchParams(location.search); + const token = queryParams.get("q"); + const [password, setPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [notification, setNotification] = useState(""); + const [error, setError] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwordStrength, setPasswordStrength] = useState(""); + const [passwordsMatch, setPasswordsMatch] = useState(true); + + const updateToast = ( + toastId: any, + message: string, + type: "success" | "error" + ) => { + toast.update(toastId, { + render: message, + type: type, + isLoading: false, + autoClose: 5000, + closeOnClick: true, + hideProgressBar: false, + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const toastId = toast.loading("Changing Password..."); + + if (password !== newPassword) { + setError(true); + setNotification("Passwords must match!"); + updateToast(toastId, "Passwords must match!", "error"); + return; + } + + try { + await axios.post(`${BACKEND_URL}/api/users/reset-password/${token}`, { + newPassword, + }); + updateToast( + toastId, + "Password changed successfully, please log in", + "success" + ); + setTimeout(() => { + localStorage.removeItem("token"); + navigate("/login"); + }, 2000); // 2 second delay + } catch (err) { + if (axios.isAxiosError(err)) { + const message = err.response?.data?.message || `Error: ${err.message}`; + updateToast(toastId, message, "error"); + } else { + updateToast( + toastId, + "Failed to send the email, please try again", + "error" + ); + } + } + }; + + const togglePasswordVisibility = () => { + setShowPassword((prevState) => !prevState); + }; + + const toggleConfirmPasswordVisibility = () => { + setShowConfirmPassword((prevState) => !prevState); + }; + + const handlePasswordChange = (e: React.ChangeEvent) => { + const { value } = e.target; + setPassword(value); + setPasswordStrength(getPasswordStrength(value)); + }; + + const handleNewPasswordChange = (e: React.ChangeEvent) => { + const { value } = e.target; + setNewPassword(value); + setPasswordsMatch(password === value); + }; + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + toast.info("Copying and pasting is not allowed in this field."); + }; + + const validatePassword = (password: string): boolean => { + const passwordRegex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + return passwordRegex.test(password); + }; + const getPasswordStrength = (password: string): string => { + if (validatePassword(password)) return "Strong"; + return "Weak"; + }; + + return ( + <> +
+
+ + error--v1 +

It's been a while since you last updated your password. For your + security, please take a moment to change it now. Keeping your + password up-to-date helps protect your account from unauthorized + access. Thank you for your cooperation! + +

+
+ +
+ +
+ + + + +
+ {passwordStrength && ( +

+ Password strength: {passwordStrength} +

+ )} +
+ +
+ + + + +
+ {!passwordsMatch && ( +

Passwords must match!

+ )} +
+ +
+
+ + + ); +}; + +export default UpdatePasswordAfterXAmountOfTime; diff --git a/src/components/AvailableProduct/product.tsx b/src/components/AvailableProduct/product.tsx index f1b7ace..070ebaa 100644 --- a/src/components/AvailableProduct/product.tsx +++ b/src/components/AvailableProduct/product.tsx @@ -67,19 +67,24 @@ const Product: React.FC = ({ const handleAddToWishlist = async () => { if (!token) { - setMessage('Please Login to continue'); + setMessage('Please log in to continue'); setMessageType('error'); return; } try { - const response = await addToWishlist(productId, token); - setMessage(response.data.message); + // Call the addToWishlist function + await addToWishlist(productId, token); + + // If successful, set the success message + setMessage('Product added to wishlist successfully'); setMessageType('success'); } catch (error: any) { + // Handle errors and set error message setMessage(error.response?.data?.message || 'Failed to add product to wishlist'); setMessageType('error'); } }; + const handleAddProductInCart = async (productId: string) => { const quantity = 1; @@ -127,7 +132,7 @@ const Product: React.FC = ({ {loadingStates[productId] ? 'Adding...' : 'Add to Cart'}
- + {/* */} ); }; diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index e9bef71..be614dc 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Link, useParams } from "react-router-dom"; +import { Link, useLocation, useParams } from "react-router-dom"; import { connect, useDispatch, useSelector } from "react-redux"; import Toast from "./Toast/Toast"; import "../styles/Header.scss"; @@ -8,6 +8,7 @@ import { fetchProductsInCart } from "../redux/slices/cartSlice"; import { AppDispatch, RootState } from "../redux/store"; import Cart from "./cart/cart"; import CartModal from "./cartModal/modal"; +import { decodeToken } from "react-jwt"; interface NavbarProps { loggedInSuccessfuly: boolean; @@ -15,22 +16,28 @@ interface NavbarProps { token: string; products: any[]; fetchProductsInCart: () => void; + } +interface decodedToken { + userId: string; + role: string; +} const Navbar: React.FC = ({ loggedInSuccessfuly, isSuccessfully, token, }) => { - const { id } = useParams<{ id: string }>(); + const id = localStorage.getItem('userId'); const dispatch = useDispatch(); const { products = [], loading } = useSelector( (state: RootState) => state.cart ); const [clicked, setClicked] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); - // const [products, setProducts] = useState([]); + + const openModal = (e: { preventDefault: () => void }) => { e.preventDefault(); diff --git a/src/components/personalInfo/personalInfo.tsx b/src/components/personalInfo/personalInfo.tsx index 946171b..813bbf1 100644 --- a/src/components/personalInfo/personalInfo.tsx +++ b/src/components/personalInfo/personalInfo.tsx @@ -106,7 +106,7 @@ const PersonalInfo: React.FC = () => { )} -
+
diff --git a/src/components/personalInfo/personalInfoStyles.scss b/src/components/personalInfo/personalInfoStyles.scss index 7c638f0..1f554c5 100644 --- a/src/components/personalInfo/personalInfoStyles.scss +++ b/src/components/personalInfo/personalInfoStyles.scss @@ -24,6 +24,8 @@ $primaryFontFamily: "Nunito Sans", sans-serif; position: relative; z-index: 1; background-color: white; + // border: 1px solid red; + } .personal-info-header { @@ -48,6 +50,7 @@ $primaryFontFamily: "Nunito Sans", sans-serif; width: 100%; height: auto; display: flex; + // border: 1px solid yellow; .details-part1 { width: 45%; height: 90px; @@ -58,6 +61,7 @@ $primaryFontFamily: "Nunito Sans", sans-serif; margin: auto; font-size: large; table { + margin-top: -30px; th { text-align: right; } @@ -86,6 +90,7 @@ $primaryFontFamily: "Nunito Sans", sans-serif; margin: auto; font-size: large; table { + margin-top: -30px; th { text-align: right; } diff --git a/src/components/services/wishlistService.tsx b/src/components/services/wishlistService.tsx index a9376e3..9453cde 100644 --- a/src/components/services/wishlistService.tsx +++ b/src/components/services/wishlistService.tsx @@ -1,17 +1,33 @@ import axios from 'axios'; +import { toast } from 'react-toastify'; const BASE_URL = process.env.REACT_APP_BACKEND_URL ; export const addToWishlist = async (productId: string, token: string) => { - return await axios.post( - `${BASE_URL}/api/wishlist/${productId}`, - {}, - { - headers: { - Authorization: `Bearer ${token}`, - }, + try { + // Perform the POST request + await axios.post( + `${BASE_URL}/api/wishlist/${productId}`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + // Show success message after successful request + toast.success("Product added to wishlist successfully"); + + } catch (error) { + // Handle errors and show error message if the request fails + if (axios.isAxiosError(error)) { + const message = error.response?.data?.message || "An error occurred"; + toast.error(message); + } else { + toast.error("An unexpected error occurred"); } - ); + } }; export const getWishlist = async (token: string) => { diff --git a/src/redux/slices/cartSlice.ts b/src/redux/slices/cartSlice.ts index e25e1f1..201d6b1 100644 --- a/src/redux/slices/cartSlice.ts +++ b/src/redux/slices/cartSlice.ts @@ -75,8 +75,8 @@ export const fetchProductsInCart = createAsyncThunk( const token = localStorage.getItem("token"); if (!token) { - throw new Error('Bearer token is not available'); - toast.error("login first") + throw new Error("Bearer token is not available"); + toast.error("login first"); } try { const response = await axios.get(`${BACKEND_URL}/api/carts`, { @@ -106,7 +106,6 @@ export const fetchTotalInCart = createAsyncThunk( if (!token) { // throw new Error('Bearer token is not available'); - } try { const response = await axios.get(`${BACKEND_URL}/api/carts`, { @@ -134,7 +133,6 @@ export const updateProductQuantityInCart = createAsyncThunk( const token = localStorage.getItem("token"); if (!token) { - throw new Error("Bearer token is not available"); } @@ -162,18 +160,17 @@ export const updateProductQuantityInCart = createAsyncThunk( ); export const deleteProductInCart = createAsyncThunk( "cart/deleteproduct", - async ({ productId}: { productId: string;}) => { + async ({ productId }: { productId: string }) => { const token = localStorage.getItem("token"); if (!token) { - throw new Error("Bearer token is not available"); } try { const response = await axios.delete( `${BACKEND_URL}/api/carts/product/${productId}`, - + { headers: { Authorization: `Bearer ${token}`, @@ -198,14 +195,13 @@ export const clearCart = createAsyncThunk( const token = localStorage.getItem("token"); if (!token) { - throw new Error("Bearer token is not available"); } try { const response = await axios.delete( `${BACKEND_URL}/api/carts/clear`, - + { headers: { Authorization: `Bearer ${token}`, @@ -279,40 +275,36 @@ const cartSlice = createSlice({ state.loading = false; state.error = action.error.message || "Failed to fetch total in cart"; }) - .addCase(updateProductQuantityInCart.pending, (state) => { - state.loading = true; - state.error = null; - }) - .addCase( - updateProductQuantityInCart.fulfilled, - (state, action: PayloadAction) => { - - state.products = action.payload.Products; + .addCase(updateProductQuantityInCart.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase( + updateProductQuantityInCart.fulfilled, + (state, action: PayloadAction) => { + state.products = action.payload.Products; + state.loading = false; + } + ) + .addCase(updateProductQuantityInCart.rejected, (state, action) => { state.loading = false; - } - ) - .addCase(updateProductQuantityInCart.rejected, (state, action) => { - state.loading = false; - state.error = action.error.message || "Failed to update quantity in cart"; - }) - .addCase(clearCart.pending, (state) => { - state.loading = true; - state.error = null; - }) - .addCase( - clearCart.fulfilled, - (state, action: PayloadAction) => { - + state.error = + action.error.message || "Failed to update quantity in cart"; + }) + .addCase(clearCart.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(clearCart.fulfilled, (state, action: PayloadAction) => { state.products = []; state.total = 0; state.loading = false; - } - ) - .addCase(clearCart.rejected, (state, action) => { - state.loading = false; - state.error = action.error.message || "Failed to update quantity in cart"; - }) - + }) + .addCase(clearCart.rejected, (state, action) => { + state.loading = false; + state.error = + action.error.message || "Failed to update quantity in cart"; + }); }, });