├── Procfile
├── frontend
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── images
│ │ ├── alexa.jpg
│ │ ├── camera.jpg
│ │ ├── mouse.jpg
│ │ ├── phone.jpg
│ │ ├── airpods.jpg
│ │ ├── kosells.png
│ │ ├── icon_user.png
│ │ └── playstation.jpg
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── assets
│ │ ├── 404Error.png
│ │ ├── githubLogo.png
│ │ ├── googleLogo.png
│ │ ├── twitterLogo.png
│ │ └── linkedinLogo.png
│ ├── styles
│ │ ├── loader.css
│ │ ├── product-page.css
│ │ ├── product.css
│ │ ├── update-toast.css
│ │ ├── error-page.css
│ │ ├── login-register.css
│ │ ├── social-login-option.css
│ │ ├── product-carousel.css
│ │ ├── check-status.css
│ │ ├── footer.css
│ │ ├── profile-page.css
│ │ ├── skeleton.css
│ │ └── header.css
│ ├── components
│ │ ├── SkeletonShimmer.js
│ │ ├── BasicSkeleton.js
│ │ ├── Loader.js
│ │ ├── FormContainer.js
│ │ ├── CarouselSkeleton.js
│ │ ├── ProductSkeleton.js
│ │ ├── Meta.js
│ │ ├── Message.js
│ │ ├── Rating.js
│ │ ├── Paginate.js
│ │ ├── Footer.js
│ │ ├── Product.js
│ │ ├── SearchBox.js
│ │ ├── ProductCarousel.js
│ │ ├── SocialLoginOptions.js
│ │ ├── CheckoutStatus.js
│ │ ├── ImageMagnifier.js
│ │ ├── CheckoutForm.js
│ │ └── Header.js
│ ├── constants
│ │ ├── cartConstants.js
│ │ ├── orderConstants.js
│ │ ├── productConstants.js
│ │ └── userConstants.js
│ ├── index.js
│ ├── pages
│ │ ├── ErrorPage.js
│ │ ├── ConfirmPage.js
│ │ ├── PaymentPage.js
│ │ ├── OrderListPage.js
│ │ ├── UserListPage.js
│ │ ├── ShippingPage.js
│ │ ├── UserEditPage.js
│ │ ├── PasswordResetPage.js
│ │ ├── HomePage.js
│ │ ├── ProductListPage.js
│ │ └── RegisterPage.js
│ ├── index.css
│ ├── utils
│ │ └── getDateString.js
│ ├── ServiceWorkerWrapper.js
│ ├── reducers
│ │ ├── cartReducers.js
│ │ ├── productReducers.js
│ │ ├── orderReducers.js
│ │ └── userReducers.js
│ ├── actions
│ │ ├── cartActions.js
│ │ ├── productActions.js
│ │ └── orderActions.js
│ ├── service-worker.js
│ ├── store.js
│ ├── App.js
│ └── serviceWorkerRegistration.js
├── env.md
└── package.json
├── backend
├── controllers
│ ├── configControllers.js
│ ├── authControllers.js
│ ├── productControllers.js
│ └── orderControllers.js
├── utils
│ ├── generateGravatar.js
│ ├── getAuthErrorCode.js
│ ├── transporter.js
│ ├── generateToken.js
│ └── sendMail.js
├── routes
│ ├── configRoutes.js
│ ├── productRoutes.js
│ ├── orderRoutes.js
│ ├── uploadRoutes.js
│ ├── userRoutes.js
│ └── authRoutes.js
├── models
│ ├── tokenModel.js
│ ├── userModel.js
│ ├── productModel.js
│ └── orderModel.js
├── config
│ ├── db.js
│ └── passportSetup.js
├── middleware
│ ├── errorMiddleware.js
│ └── authMiddleware.js
├── data
│ ├── users.js
│ └── products.js
├── seeder.js
└── server.js
├── LICENSE
├── package.json
└── env.md
/Procfile:
--------------------------------------------------------------------------------
1 | web: node backend/server.js
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/favicon-16x16.png
--------------------------------------------------------------------------------
/frontend/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/favicon-32x32.png
--------------------------------------------------------------------------------
/frontend/public/images/alexa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/alexa.jpg
--------------------------------------------------------------------------------
/frontend/public/images/camera.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/camera.jpg
--------------------------------------------------------------------------------
/frontend/public/images/mouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/mouse.jpg
--------------------------------------------------------------------------------
/frontend/public/images/phone.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/phone.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/404Error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/src/assets/404Error.png
--------------------------------------------------------------------------------
/frontend/public/images/airpods.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/airpods.jpg
--------------------------------------------------------------------------------
/frontend/public/images/kosells.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/kosells.png
--------------------------------------------------------------------------------
/frontend/src/assets/githubLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/src/assets/githubLogo.png
--------------------------------------------------------------------------------
/frontend/src/assets/googleLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/src/assets/googleLogo.png
--------------------------------------------------------------------------------
/frontend/src/assets/twitterLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/src/assets/twitterLogo.png
--------------------------------------------------------------------------------
/frontend/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/frontend/public/images/icon_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/icon_user.png
--------------------------------------------------------------------------------
/frontend/public/images/playstation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/images/playstation.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/linkedinLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/src/assets/linkedinLogo.png
--------------------------------------------------------------------------------
/frontend/src/styles/loader.css:
--------------------------------------------------------------------------------
1 | div.loader {
2 | display: block;
3 | width: 5rem;
4 | height: 5rem;
5 | margin: 5em auto;
6 | }
--------------------------------------------------------------------------------
/frontend/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/frontend/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackSorcerer403/MERN-Ecommerce/HEAD/frontend/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/frontend/src/styles/product-page.css:
--------------------------------------------------------------------------------
1 | .review-avatar {
2 | width: 3em;
3 | height: 3em;
4 | border-radius: 50%;
5 | margin-right: 1em;
6 | }
--------------------------------------------------------------------------------
/frontend/env.md:
--------------------------------------------------------------------------------
1 | # Environment variables for the client side code
2 |
3 | REACT_APP_STRIPE_PUBLISHABLE_KEY = `Stripe publishable key for accepting payments`
4 |
5 | REACT_APP_BASE_URL = `URL for the server APIs`
6 |
--------------------------------------------------------------------------------
/frontend/src/components/SkeletonShimmer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SkeletonShimmer = () => {
4 | return (
5 |
8 | );
9 | };
10 |
11 | export default SkeletonShimmer;
12 |
--------------------------------------------------------------------------------
/frontend/src/components/BasicSkeleton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../styles/skeleton.css';
3 |
4 | const BasicSkeleton = ({ type }) => {
5 | const classes = `skeleton ${type}`;
6 |
7 | return
;
8 | };
9 |
10 | export default BasicSkeleton;
11 |
--------------------------------------------------------------------------------
/frontend/src/styles/product.css:
--------------------------------------------------------------------------------
1 | p.product-title {
2 | min-height: 7ch;
3 | }
4 |
5 | @media screen and (max-width: 430px) {
6 | p.product-title {
7 | min-height: auto;
8 | }
9 |
10 | img.product-image {
11 | aspect-ratio: 1.5;
12 | object-fit: cover;
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/frontend/src/constants/cartConstants.js:
--------------------------------------------------------------------------------
1 | export const CART_ADD_ITEM = 'CART_ADD_ITEM';
2 | export const CART_REMOVE_ITEM = 'CART_REMOVE_ITEM';
3 | export const CART_RESET = 'CART_RESET';
4 | export const CART_SAVE_SHIPPING_ADDRESS = 'CART_SAVE_SHIPPING_ADDRESS';
5 | export const CART_SAVE_PAYMENT_METHOD = 'CART_SAVE_PAYMENT_METHOD';
6 |
--------------------------------------------------------------------------------
/frontend/src/styles/update-toast.css:
--------------------------------------------------------------------------------
1 | .update-toast {
2 | z-index: 100;
3 | position: fixed;
4 | top: 1%;
5 | right: 1%;
6 | transition: all 0.3s ease-in-out;
7 | }
8 |
9 | @media screen and (max-width: 430px) {
10 | .update-toast {
11 | top: auto;
12 | bottom: 1%;
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/frontend/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner } from 'react-bootstrap';
3 | import '../styles/loader.css';
4 |
5 | const Loader = () => {
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default Loader;
17 |
--------------------------------------------------------------------------------
/frontend/src/styles/error-page.css:
--------------------------------------------------------------------------------
1 | img.error-img {
2 | width: 15rem;
3 | object-fit: cover;
4 | }
5 |
6 | p.text-error {
7 | font-size: 1.2em;
8 | }
9 |
10 | @media screen and (max-width: 430px) {
11 | img.error-img {
12 | width: 10rem;
13 | margin-top: 5em;
14 | }
15 |
16 | p.text-error {
17 | font-size: 1em;
18 | }
19 | }
--------------------------------------------------------------------------------
/backend/controllers/configControllers.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler';
2 |
3 | // @desc fetch PAYPAL client id credential from .env file
4 | // @route GET /api/config/paypal
5 | // @access PRIVATE
6 | const getPaypalClientId = asyncHandler(async (req, res) => {
7 | res.send(process.env.PAYPAL_CLIENT_ID);
8 | });
9 |
10 | export { getPaypalClientId };
11 |
--------------------------------------------------------------------------------
/frontend/src/components/FormContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, Row, Col } from 'react-bootstrap';
3 |
4 | const FormContainer = ({ children }) => {
5 | return (
6 |
7 |
8 |
9 | {children}
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default FormContainer;
17 |
--------------------------------------------------------------------------------
/backend/utils/generateGravatar.js:
--------------------------------------------------------------------------------
1 | import gravatar from 'gravatar';
2 |
3 | const generateGravatar = (email) => {
4 | // generate a url for the gravatar that is using https
5 | const avatar = gravatar.url(email, {
6 | protocol: 'https',
7 | s: '200', // size: 200x200
8 | r: 'PG', // rating: PG
9 | d: 'identicon', // default: identicon
10 | });
11 | return avatar;
12 | };
13 |
14 | export default generateGravatar;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/CarouselSkeleton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BasicSkeleton from './BasicSkeleton';
3 | import SkeletonShimmer from './SkeletonShimmer';
4 |
5 | const CarouselSkeleton = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default CarouselSkeleton;
15 |
--------------------------------------------------------------------------------
/backend/routes/configRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { getPaypalClientId } from '../controllers/configControllers.js';
3 | import { protectRoute } from '../middleware/authMiddleware.js';
4 |
5 | const router = express.Router();
6 |
7 | // @desc fetch PAYPAL client id credential
8 | // @route GET /api/config/paypal
9 | // @access PRIVATE
10 | router.route('/paypal').get(protectRoute, getPaypalClientId);
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/frontend/src/components/ProductSkeleton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BasicSkeleton from './BasicSkeleton';
3 | import SkeletonShimmer from './SkeletonShimmer';
4 |
5 | const ProductSkeleton = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default ProductSkeleton;
17 |
--------------------------------------------------------------------------------
/backend/models/tokenModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | // store the refresh tokens in the db
4 | const tokenSchema = mongoose.Schema(
5 | {
6 | email: {
7 | type: String,
8 | required: true,
9 | },
10 | token: {
11 | type: String,
12 | required: true,
13 | },
14 | },
15 | { timestamps: true }
16 | );
17 |
18 | // delete the refresh tokens every 7 days
19 | tokenSchema.index({ createdAt: 1 }, { expires: '7d' });
20 |
21 | const Token = mongoose.model('Token', tokenSchema);
22 |
23 | export default Token;
24 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import App from './App';
5 | import store from './store';
6 | import { HelmetProvider } from 'react-helmet-async';
7 | import './bootstrap.min.css';
8 | import './index.css';
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById('root')
19 | );
20 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | // connect to the mongoDB collection
4 | const connectDB = () => {
5 | mongoose
6 | .connect(process.env.MONGO_URI, {
7 | useUnifiedTopology: true,
8 | useNewUrlParser: true,
9 | useCreateIndex: true,
10 | })
11 | .then((res) =>
12 | console.log(
13 | `MongoDB Connected: ${res.connection.host}`.cyan.underline.bold
14 | )
15 | )
16 | .catch((err) => {
17 | console.error(`Error: ${err.message}`.red.underline.bold);
18 | process.exit(1);
19 | });
20 | };
21 |
22 | export default connectDB;
23 |
--------------------------------------------------------------------------------
/backend/utils/getAuthErrorCode.js:
--------------------------------------------------------------------------------
1 | // This is used to send the correct error code to be sent to as the parameter after redirecting to frontend after failed passport login
2 | const getAuthErrorCode = (msg) => {
3 | // This msg is obtained as the flash message from the passport login routes
4 | if (msg === 'Registered using google account') return 0;
5 | if (msg === 'Registered using github account') return 1;
6 | if (msg === 'Registered using twitter account') return 2;
7 | if (msg === 'Registered using linkedin account') return 3;
8 | };
9 |
10 | export default getAuthErrorCode;
11 |
--------------------------------------------------------------------------------
/frontend/src/components/Meta.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Helmet } from 'react-helmet-async';
3 |
4 | const Meta = ({ title, description, keywords }) => {
5 | return (
6 |
7 | {title}
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | Meta.defaultProps = {
15 | title: 'Welcome to Kosells',
16 | keywords: 'Electronics, Kosells, Ecommerce, Rajat',
17 | description: 'Buy the best products at the lowest prices',
18 | };
19 | export default Meta;
20 |
--------------------------------------------------------------------------------
/backend/utils/transporter.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import dotenv from 'dotenv';
3 | dotenv.config();
4 |
5 | // configure the transporter for nodemailer to use gmail account to send mails
6 | const transporter = nodemailer.createTransport({
7 | service: 'gmail',
8 | auth: {
9 | type: 'OAuth2',
10 | user: process.env.MAIL_USERNAME,
11 | pass: process.env.MAIL_PASSWORD,
12 | clientId: process.env.OAUTH_CLIENT_ID,
13 | clientSecret: process.env.OAUTH_CLIENT_SECRET,
14 | refreshToken: process.env.OAUTH_REFRESH_TOKEN,
15 | },
16 | });
17 |
18 | export default transporter;
19 |
--------------------------------------------------------------------------------
/frontend/src/pages/ErrorPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import errorImg from '../assets/404Error.png';
3 | import { Link } from 'react-router-dom';
4 | import '../styles/error-page.css';
5 |
6 | // 404 page
7 | const ErrorPage = () => {
8 | return (
9 |
10 |
11 |
12 | Looks like this page does not
13 | exist.
14 |
15 |
16 | Go Back to the Home Page
17 |
18 |
19 | );
20 | };
21 |
22 | export default ErrorPage;
23 |
--------------------------------------------------------------------------------
/backend/middleware/errorMiddleware.js:
--------------------------------------------------------------------------------
1 | // handle 404 errors
2 |
3 | const notFound = (req, res, next) => {
4 | const error = new Error(`Route not found - ${req.originalUrl}`);
5 | res.status(404);
6 | next(error);
7 | };
8 |
9 | // custom error handler to return json instead of HTML when any error is thrown
10 | const errorHandler = (err, req, res, next) => {
11 | // check the status code of the response
12 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
13 | res.status(statusCode);
14 | res.json({
15 | message: err.message,
16 | stack: process.env.NODE_ENV === 'production' ? null : err.stack,
17 | });
18 | };
19 |
20 | export { notFound, errorHandler };
21 |
--------------------------------------------------------------------------------
/frontend/src/styles/login-register.css:
--------------------------------------------------------------------------------
1 | .form-heading {
2 | display: flex;
3 | flex-flow: row wrap;
4 | align-items: center;
5 | justify-content: space-between;
6 | margin: 0 -1em;
7 | margin-bottom: 1.5em;
8 | }
9 |
10 | .form-inner-container {
11 | margin: 1.5em 0;
12 | padding: 1em;
13 | padding-top: 0;
14 | box-shadow: 0 6px 16px 0 rgb(0 0 0 / 20%);
15 | }
16 |
17 | .form-heading h1 {
18 | width: 50%;
19 | text-align: center;
20 | margin: 0;
21 | font-size: 1.4em;
22 | font-weight: 600;
23 | cursor: pointer;
24 | border: 2px solid ghostwhite;
25 | border-top: 0;
26 | transition: all 0.2s ease-in-out;
27 |
28 | }
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | main {
2 | min-height: 85vh;
3 | }
4 |
5 | h4 {
6 | margin: 0.8rem 0;
7 | }
8 |
9 | h1 {
10 | padding: 1rem 0;
11 | font-size: 1.8rem;
12 | }
13 |
14 | h2 {
15 | padding: 0.2rem 0;
16 | font-size: 1.4rem;
17 | }
18 |
19 | table td {
20 | vertical-align: middle;
21 | }
22 |
23 | .alert-custom {
24 | transition: all 0.4s ease-in-out;
25 | }
26 |
27 | .rating span {
28 | margin: 0.1rem;
29 | }
30 |
31 | button.cart-delete-btn {
32 | margin-left: 1em;
33 | }
34 |
35 | .form-control:focus {
36 | box-shadow: none;
37 | }
38 |
39 | @media (max-width: 430px) {
40 | button.remember-password {
41 | font-size: 0.8rem;
42 | }
43 | }
--------------------------------------------------------------------------------
/frontend/src/utils/getDateString.js:
--------------------------------------------------------------------------------
1 | // there is a need to convert mongodb dates to readable date formats in various pages
2 | // this util function does that, and has a second argument to decide whether the time has to be included
3 | const getDateString = (date, showTime = true) => {
4 | const options = {
5 | year: 'numeric',
6 | month: 'short',
7 | day: 'numeric',
8 | };
9 | const timeStr = new Date(date).toLocaleTimeString('en', {
10 | timeStyle: 'short',
11 | hour12: true,
12 | timeZone: 'IST',
13 | });
14 |
15 | let result = '';
16 | if (showTime) result += `${timeStr} `;
17 | return result + new Date(date).toLocaleDateString('en', options);
18 | };
19 |
20 | export default getDateString;
21 |
--------------------------------------------------------------------------------
/frontend/src/styles/social-login-option.css:
--------------------------------------------------------------------------------
1 | .social-login-container {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | align-items: center;
5 | justify-content: space-between;
6 | margin-bottom: 1em;
7 | padding: 0;
8 | }
9 |
10 | .social-login-line {
11 | border: 0;
12 | width: 33%;
13 | height: 0;
14 | border-top: 1px solid #cdcdcd;
15 | }
16 |
17 | .social-login-content {
18 | padding: 0;
19 | margin: 0 1%;
20 | font-weight: 400;
21 | font-size: 0.9rem;
22 | }
23 |
24 | @media screen and (max-width: 430px) {
25 | .social-login-line {
26 | width: 30%;
27 | }
28 |
29 | .social-login-content {
30 | font-size: 0.8rem;
31 | }
32 | }
--------------------------------------------------------------------------------
/frontend/src/styles/product-carousel.css:
--------------------------------------------------------------------------------
1 | .carousel-item-next,
2 | .carousel-item-prev,
3 | .carousel-item:active {
4 | display: flex
5 | }
6 |
7 | .carousel-inner {
8 | text-align: center;
9 | }
10 |
11 | div.carousel-caption {
12 | font-size: 1.2em;
13 | padding: 0;
14 | margin-bottom: -0.5em;
15 | }
16 |
17 | .carousel img {
18 | height: 30vh;
19 | border-radius: 50%;
20 | width: 30vh;
21 | object-fit: cover;
22 | margin: 0.5em auto 2.5em auto;
23 | }
24 |
25 | .carousel a {
26 | margin: 0 auto;
27 | }
28 |
29 | @media screen and (max-width: 900px) {
30 | .carousel-caption h2 {
31 | font-size: 2.5vw;
32 | }
33 | }
34 |
35 | @media screen and (max-width: 430px) {
36 | .carousel {
37 | display: none;
38 | }
39 | }
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Kosells Ecommerce",
3 | "short_name": "Kosells",
4 | "icons": [
5 | {
6 | "src": "/favicon-16x16.png",
7 | "sizes": "16x16",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/favicon-32x32.png",
12 | "sizes": "32x32",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "/android-chrome-192x192.png",
17 | "sizes": "192x192",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "/android-chrome-512x512.png",
22 | "sizes": "512x512",
23 | "type": "image/png"
24 | }
25 | ],
26 | "theme_color": "#2c3e50",
27 | "orientation": "portrait",
28 | "background_color": "#dee2e6",
29 | "display": "standalone",
30 | "description": "An E-commerce app to buy the best products at the lowest prices! Built by Rajat",
31 | "start_url": "/"
32 | }
33 |
--------------------------------------------------------------------------------
/backend/data/users.js:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcryptjs';
2 |
3 | const users = [
4 | {
5 | name: 'Admin',
6 | email: 'admin@kosells.com',
7 | password: bcrypt.hashSync('pass123', 12),
8 | isAdmin: true,
9 | isConfirmed: true,
10 | avatar: '/images/icon_user.png',
11 | },
12 | {
13 | name: 'Rajat',
14 | email: 'rajat@kosells.com',
15 | password: bcrypt.hashSync('pass123', 12),
16 | isConfirmed: true,
17 | avatar: '/images/icon_user.png',
18 | },
19 | {
20 | name: 'Ravi',
21 | email: 'ravi@kosells.com',
22 | password: bcrypt.hashSync('pass123', 12),
23 | isConfirmed: true,
24 | avatar: '/images/icon_user.png',
25 | },
26 | {
27 | name: 'Voca',
28 | email: 'dataxom889@flmmo.com',
29 | password: bcrypt.hashSync('pass123', 12),
30 | isConfirmed: true,
31 | avatar: '/images/icon_user.png',
32 | },
33 | ];
34 |
35 | export default users;
36 |
--------------------------------------------------------------------------------
/frontend/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Alert } from 'react-bootstrap';
3 |
4 | const Message = ({ variant, duration, children, dismissible }) => {
5 | const [visible, setVisible] = useState(true);
6 |
7 | useEffect(() => {
8 | setVisible(true);
9 | }, []);
10 |
11 | useEffect(() => {
12 | if (duration) {
13 | setTimeout(() => setVisible(false), duration * 1000);
14 | }
15 | }, [duration]);
16 |
17 | return (
18 | setVisible(false)}
20 | dismissible={dismissible}
21 | className='alert-custom'
22 | style={visible ? { display: 'block' } : { display: 'none' }}
23 | variant={variant}>
24 | {children}
25 |
26 | );
27 | };
28 |
29 | Message.defaultProps = {
30 | variant: 'info',
31 | dismissible: false,
32 | };
33 |
34 | export default Message;
35 |
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 |
3 | // generate a JWT token for the various applications represented by the 'option' argument
4 | const generateToken = (id, option) => {
5 | if (option === 'access') {
6 | return jwt.sign({ id }, process.env.JWT_ACCESS_TOKEN_SECRET, {
7 | expiresIn: 60 * 60, // 1 hour
8 | });
9 | } else if (option === 'refresh') {
10 | return jwt.sign({ id }, process.env.JWT_REFRESH_TOKEN_SECRET, {
11 | expiresIn: '7d', // 7 days
12 | });
13 | } else if (option === 'email') {
14 | return jwt.sign({ id }, process.env.JWT_EMAIL_TOKEN_SECRET, {
15 | expiresIn: 60 * 15, // 15 minutes
16 | });
17 | } else if (option === 'forgot password') {
18 | return jwt.sign({ id }, process.env.JWT_FORGOT_PASSWORD_TOKEN_SECRET, {
19 | expiresIn: 60 * 10, // 10 minutes
20 | });
21 | }
22 | };
23 |
24 | export default generateToken;
25 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler';
2 | import getAuthErrorCode from '../utils/getAuthErrorCode.js';
3 |
4 | // To redirect to some route caught by the react router in the frontend, after successfully logging in
5 | const frontendURL = process.env.FRONTEND_BASE_URL;
6 |
7 | // controller for the routes that handle the success redirects from all 4 passport strategies
8 | const passportLoginSuccess = asyncHandler(async (req, res) => {
9 | res.redirect(`${frontendURL}/login?login=success&id=${req.user._id}`);
10 | });
11 |
12 | // controller for the routes that handle the failure redirects from all 4 passport strategies
13 | const passportLoginFailure = asyncHandler(async (req, res) => {
14 | const errorMsg = req.flash('error')[0];
15 | const errorCode = getAuthErrorCode(errorMsg);
16 | res.redirect(`${frontendURL}/login?login=failed&errorCode=${errorCode}`);
17 | });
18 |
19 | export { passportLoginSuccess, passportLoginFailure };
20 |
--------------------------------------------------------------------------------
/frontend/src/components/Rating.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Rating = ({ value, text, color }) => {
5 | return (
6 | // show full/half star icon depending on rating value
7 |
8 | {[1, 2, 3, 4, 5].map((ele, idx) => (
9 |
10 | = ele
14 | ? 'fas fa-star'
15 | : value >= ele - 0.5
16 | ? 'fas fa-star-half-alt'
17 | : 'far fa-star'
18 | }
19 | title={`${value} Stars`}
20 | />
21 |
22 | ))}
23 | {text && text}
24 |
25 | );
26 | };
27 |
28 | Rating.defaultProps = {
29 | color: '#f8e825',
30 | };
31 |
32 | Rating.propTypes = {
33 | value: PropTypes.number.isRequired,
34 | text: PropTypes.string,
35 | color: PropTypes.string,
36 | };
37 |
38 | export default Rating;
39 |
--------------------------------------------------------------------------------
/frontend/src/styles/check-status.css:
--------------------------------------------------------------------------------
1 | .status-bar {
2 | width: max-content;
3 | display: flex;
4 | flex-flow: row nowrap;
5 | align-items: center;
6 | margin: 1em auto 0 auto;
7 | }
8 |
9 | .status-checkpoint {
10 | display: flex;
11 | flex-flow: column nowrap;
12 | align-items: center;
13 | padding: 0;
14 | }
15 |
16 | .status-checkpoint .circle {
17 | width: 0.9em;
18 | height: 0.9em;
19 | border-radius: 50%;
20 | border: 0.2em solid #2c3e50;
21 | }
22 |
23 | .status-bar .connection {
24 | align-self: flex-start;
25 | border-top: 1px solid #95a5a6;
26 | margin: 0;
27 | margin-top: 0.5em;
28 | padding: 0;
29 | width: 6rem;
30 | }
31 |
32 | .status-bar a.nav-link {
33 | padding: 0;
34 | }
35 |
36 | @media screen and (max-width: 430px) {
37 | .status-bar {
38 | width: auto;
39 | justify-content: space-between;
40 | }
41 |
42 | .status-bar .connection {
43 | display: none;
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/frontend/src/styles/footer.css:
--------------------------------------------------------------------------------
1 | footer.footer-container {
2 | display: flex;
3 | justify-content: flex-end;
4 | align-items: center;
5 | margin-bottom: 1em;
6 | }
7 |
8 | .footer-icons {
9 | width: 12%;
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-evenly;
13 | }
14 |
15 | .footer-icons a {
16 | color: #212529;
17 | }
18 |
19 | .footer-icon {
20 | font-size: 1.2em;
21 | line-height: 1.2em;
22 | object-fit: cover;
23 | transform: translateY(0);
24 | transition: transform 0.3s ease-in-out;
25 | cursor: pointer;
26 | }
27 |
28 | .footer-icon:hover {
29 | transform: translateY(-20%);
30 | }
31 |
32 |
33 | .footer-copyright {
34 | font-weight: bold;
35 | font-size: 1em;
36 | line-height: 1em;
37 | }
38 |
39 | @media screen and (max-width:430px) {
40 | .footer-icons {
41 | width: 30%;
42 | }
43 |
44 | footer.footer-container {
45 | margin-right: 0;
46 | margin-bottom: 0.2em;
47 | font-size: 0.9em;
48 | }
49 | }
--------------------------------------------------------------------------------
/frontend/src/components/Paginate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pagination } from 'react-bootstrap';
3 | import { LinkContainer } from 'react-router-bootstrap';
4 |
5 | // different paginate components for products, and for admin panel view
6 | const Paginate = ({
7 | pages,
8 | page,
9 | isAdmin = false,
10 | keyword = '',
11 | forOrders = false,
12 | forUsers = false,
13 | }) => {
14 | return (
15 | pages > 1 && (
16 |
17 | {[...Array(pages).keys()].map((ele) => (
18 |
31 |
32 | {ele + 1}
33 |
34 |
35 | ))}
36 |
37 | )
38 | );
39 | };
40 |
41 | export default Paginate;
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Rajat M
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
20 |
26 |
32 |
33 | Welcome to Kosells
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/backend/routes/productRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {
3 | deleteProduct,
4 | getAllProducts,
5 | getProductById,
6 | createProduct,
7 | updateProduct,
8 | createProductReview,
9 | getTopProducts,
10 | } from '../controllers/productControllers.js';
11 | import { protectRoute, isAdmin } from '../middleware/authMiddleware.js';
12 |
13 | const router = express.Router();
14 |
15 | // @desc fetch all the products, create a product
16 | // @route GET /api/products
17 | // @access PUBLIC
18 | router
19 | .route('/')
20 | .get(getAllProducts)
21 | .post(protectRoute, isAdmin, createProduct);
22 |
23 | // @desc fetch top rated products
24 | // @route GET /api/products/top
25 | // @access PUBLIC
26 | router.route('/top').get(getTopProducts);
27 |
28 | // @desc Fetch a single product by id, Delete a product, update a product
29 | // @route GET /api/products/:id
30 | // @access PUBLIC & PRIVATE/ADMIN
31 | router
32 | .route('/:id')
33 | .get(getProductById)
34 | .delete(protectRoute, isAdmin, deleteProduct)
35 | .put(protectRoute, isAdmin, updateProduct);
36 |
37 | // @desc Create a product review
38 | // @route POST /api/products/:id/reviews
39 | // @access PRIVATE
40 | router.route('/:id/reviews').post(protectRoute, createProductReview);
41 |
42 | export default router;
43 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container } from 'react-bootstrap';
3 | import '../styles/footer.css';
4 |
5 | const Footer = () => {
6 | return (
7 |
8 |
9 |
39 | ©2021 Kosells
40 |
41 |
42 | );
43 | };
44 |
45 | export default Footer;
46 |
--------------------------------------------------------------------------------
/frontend/src/styles/profile-page.css:
--------------------------------------------------------------------------------
1 | .profile-page-image {
2 | position: relative;
3 | display: inline-block;
4 | }
5 |
6 | .profile-page-image .image-overlay,
7 | .profile-page-image .image-overlay-product {
8 | display: none;
9 | }
10 |
11 | .profile-page-image:hover .image-overlay {
12 | height: 6.3em;
13 | width: 6.3em;
14 | border: 1px solid #ced4da;
15 | border-radius: 50%;
16 | background: rgba(0, 0, 0, .5);
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | font-size: 0.8em;
21 | font-weight: lighter;
22 | padding: 1em;
23 | margin: 0;
24 | color: ghostwhite;
25 | text-align: center;
26 | display: inline-block;
27 | -webkit-box-sizing: border-box;
28 | -moz-box-sizing: border-box;
29 | box-sizing: border-box;
30 | cursor: pointer;
31 | }
32 |
33 | .profile-page-image:hover .image-overlay-product {
34 | height: 85%;
35 | width: 100%;
36 | border: 1px solid #ced4da;
37 | background: rgba(0, 0, 0, .5);
38 | position: absolute;
39 | top: 0;
40 | left: 0;
41 | font-size: 0.8em;
42 | font-weight: lighter;
43 | padding: 1.5em;
44 | margin: 0;
45 | border-radius: 0.25rem;
46 | color: ghostwhite;
47 | text-align: center;
48 | display: inline-block;
49 | -webkit-box-sizing: border-box;
50 | -moz-box-sizing: border-box;
51 | box-sizing: border-box;
52 | cursor: pointer;
53 | }
--------------------------------------------------------------------------------
/frontend/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Rating from './Rating';
3 | import { Link } from 'react-router-dom';
4 | import { Card } from 'react-bootstrap';
5 | import '../styles/product.css';
6 |
7 | const Product = ({ product }) => {
8 | return (
9 |
10 |
11 |
18 |
19 |
20 |
21 |
24 |
25 | {product.name}
26 |
27 |
28 |
29 |
30 | {product && product.rating && (
31 | 1 ? 's' : ''
35 | }`}
36 | />
37 | )}
38 |
39 |
40 |
41 | {product.price &&
42 | product.price.toLocaleString('en-IN', {
43 | maximumFractionDigits: 2,
44 | style: 'currency',
45 | currency: 'INR',
46 | })}
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Product;
54 |
--------------------------------------------------------------------------------
/frontend/src/styles/skeleton.css:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | background: #ddd;
5 | margin: 10px 0;
6 | overflow: hidden;
7 | }
8 |
9 | .skeleton-product-wrapper {
10 | position: relative;
11 | margin-top: 1em;
12 | background: #c0c0c0;
13 | border-radius: 10px;
14 | padding: 0.5em;
15 | overflow: hidden;
16 | }
17 |
18 | .skeleton.box {
19 | height: 25ch;
20 | width: 100%;
21 | }
22 |
23 | .skeleton-product-wrapper .skeleton.box {
24 | margin-top: 0;
25 | margin-bottom: 10%;
26 | border-radius: 10px 10px 0 0;
27 | }
28 |
29 | .skeleton.title {
30 | height: 3ch;
31 | }
32 |
33 | .skeleton.text {
34 | width: 100%;
35 | height: 2ch;
36 | margin: 1em 0;
37 | }
38 |
39 | /* for the animations */
40 |
41 | .shimmer-wrapper {
42 | position: absolute;
43 | top: 0;
44 | bottom: 0;
45 | width: 100%;
46 | height: 100%;
47 | animation: loading 2s infinite;
48 | }
49 |
50 | .shimmer {
51 | width: 50%;
52 | height: 100%;
53 | background: rgba(255, 255, 255, 0.3);
54 | transform: skewX(-20deg);
55 | box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0.05);
56 | }
57 |
58 | @keyframes loading {
59 | 0% {
60 | transform: translateX(-150%);
61 | }
62 |
63 | 50% {
64 | transform: translateX(50%);
65 | }
66 |
67 | 100% {
68 | transform: translateX(150%);
69 | }
70 | }
--------------------------------------------------------------------------------
/frontend/src/ServiceWorkerWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { ToastContainer, Toast } from 'react-bootstrap';
3 | import * as serviceWorkerRegistration from './serviceWorkerRegistration';
4 | import './styles/update-toast.css';
5 |
6 | const ServiceWorkerWrapper = () => {
7 | const [showReload, setShowReload] = useState(false);
8 | const [waitingWorker, setWaitingWorker] = useState(null);
9 |
10 | const onSWUpdate = (registration) => {
11 | setShowReload(true);
12 | setWaitingWorker(registration.waiting);
13 | };
14 |
15 | // register the service worker on page load
16 | useEffect(() => {
17 | serviceWorkerRegistration.register({ onUpdate: onSWUpdate });
18 | }, []);
19 |
20 | // skip waiting and install new updates on page reload
21 | const reloadPage = () => {
22 | waitingWorker?.postMessage({ type: 'SKIP_WAITING' });
23 | setShowReload(false);
24 | window.location.reload(true);
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 |
32 | New Version Available
33 |
34 |
35 |
36 | Reload to see whats new!
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default ServiceWorkerWrapper;
44 |
--------------------------------------------------------------------------------
/frontend/src/constants/orderConstants.js:
--------------------------------------------------------------------------------
1 | export const ORDER_CREATE_REQUEST = 'ORDER_CREATE_REQUEST';
2 | export const ORDER_CREATE_SUCCESS = 'ORDER_CREATE_SUCCESS';
3 | export const ORDER_CREATE_FAILURE = 'ORDER_CREATE_FAILURE';
4 | export const ORDER_CREATE_RESET = 'ORDER_CREATE_RESET';
5 |
6 | export const ORDER_DETAILS_REQUEST = 'ORDER_DETAILS_REQUEST';
7 | export const ORDER_DETAILS_SUCCESS = 'ORDER_DETAILS_SUCCESS';
8 | export const ORDER_DETAILS_FAILURE = 'ORDER_DETAILS_FAILURE';
9 |
10 | export const ORDER_PAY_REQUEST = 'ORDER_PAY_REQUEST';
11 | export const ORDER_PAY_SUCCESS = 'ORDER_PAY_SUCCESS';
12 | export const ORDER_PAY_FAILURE = 'ORDER_PAY_FAILURE';
13 | export const ORDER_PAY_RESET = 'ORDER_PAY_RESET';
14 |
15 | export const ORDER_DELIVER_REQUEST = 'ORDER_DELIVER_REQUEST';
16 | export const ORDER_DELIVER_SUCCESS = 'ORDER_DELIVER_SUCCESS';
17 | export const ORDER_DELIVER_FAILURE = 'ORDER_DELIVER_FAILURE';
18 | export const ORDER_DELIVER_RESET = 'ORDER_DELIVER_RESET';
19 |
20 | export const ORDER_USER_LIST_REQUEST = 'ORDER_USER_LIST_REQUEST';
21 | export const ORDER_USER_LIST_SUCCESS = 'ORDER_USER_LIST_SUCCESS';
22 | export const ORDER_USER_LIST_FAILURE = 'ORDER_USER_LIST_FAILURE';
23 | export const ORDER_USER_LIST_RESET = 'ORDER_USER_LIST_RESET';
24 |
25 | export const ORDER_ALL_LIST_REQUEST = 'ORDER_ALL_LIST_REQUEST';
26 | export const ORDER_ALL_LIST_SUCCESS = 'ORDER_ALL_LIST_SUCCESS';
27 | export const ORDER_ALL_LIST_FAILURE = 'ORDER_ALL_LIST_FAILURE';
28 |
--------------------------------------------------------------------------------
/frontend/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CART_ADD_ITEM,
3 | CART_REMOVE_ITEM,
4 | CART_SAVE_SHIPPING_ADDRESS,
5 | CART_SAVE_PAYMENT_METHOD,
6 | CART_RESET,
7 | } from '../constants/cartConstants';
8 |
9 | export const cartReducer = (
10 | state = { cartItems: [], shippingAddress: {} },
11 | action
12 | ) => {
13 | switch (action.type) {
14 | case CART_ADD_ITEM:
15 | const item = action.payload;
16 |
17 | // check if the item exists in the cart
18 | const existingItem = state.cartItems.find(
19 | (ele) => ele.product === item.product
20 | );
21 | if (existingItem) {
22 | return {
23 | ...state,
24 | cartItems: state.cartItems.map((ele) =>
25 | ele.product === existingItem.product ? item : ele
26 | ),
27 | };
28 | } else {
29 | return {
30 | ...state,
31 | cartItems: [...state.cartItems, item],
32 | };
33 | }
34 | case CART_REMOVE_ITEM:
35 | return {
36 | ...state,
37 | cartItems: state.cartItems.filter(
38 | (ele) => ele.product !== action.payload
39 | ),
40 | };
41 | case CART_SAVE_SHIPPING_ADDRESS:
42 | return {
43 | ...state,
44 | shippingAddress: action.payload,
45 | };
46 | case CART_SAVE_PAYMENT_METHOD:
47 | return {
48 | ...state,
49 | paymentMethod: action.payload,
50 | };
51 | case CART_RESET: {
52 | return {
53 | cartItems: [],
54 | shippingAddress: action.payload,
55 | };
56 | }
57 | default:
58 | return { ...state };
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/backend/routes/orderRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {
3 | addOrderItems,
4 | getOrderById,
5 | updateOrderToPay,
6 | updateOrderToDeliver,
7 | getMyOrders,
8 | getAllOrders,
9 | stripePayment,
10 | } from '../controllers/orderControllers.js';
11 | import { protectRoute, isAdmin } from '../middleware/authMiddleware.js';
12 |
13 | const router = express.Router();
14 |
15 | // @desc create a new order, get all orders
16 | // @route GET /api/orders
17 | // @access PRIVATE && PRIVATE/ADMIN
18 | router
19 | .route('/')
20 | .post(protectRoute, addOrderItems)
21 | .get(protectRoute, isAdmin, getAllOrders);
22 |
23 | // @desc fetch the orders of the user logged in
24 | // @route GET /api/orders/myorders
25 | // @access PRIVATE
26 | router.route('/myorders').get(protectRoute, getMyOrders);
27 |
28 | // @desc create payment intent for stripe payment
29 | // @route POST /api/orders/stripe-payment
30 | // @access PUBLIC
31 | router.route('/stripe-payment').post(stripePayment);
32 |
33 | // @desc get an order by id
34 | // @route GET /api/orders/:id
35 | // @access PRIVATE
36 | router.route('/:id').get(protectRoute, getOrderById);
37 |
38 | // @desc update the order object once paid
39 | // @route PUT /api/orders/:id/pay
40 | // @access PRIVATE
41 | router.route('/:id/pay').put(protectRoute, updateOrderToPay);
42 |
43 | // @desc update the order object once delivered
44 | // @route PUT /api/orders/:id/pay
45 | // @access PRIVATE/ADMIN
46 | router.route('/:id/deliver').put(protectRoute, isAdmin, updateOrderToDeliver);
47 |
48 | export default router;
49 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import bcrypt from 'bcryptjs';
3 |
4 | const userSchema = mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | // required: true,
18 | },
19 | isAdmin: {
20 | type: Boolean,
21 | required: true,
22 | default: false,
23 | },
24 | isConfirmed: {
25 | type: Boolean,
26 | required: true,
27 | default: false,
28 | },
29 | avatar: {
30 | type: String,
31 | required: true,
32 | },
33 | // one of the following 4 will be filled, or the password field is available
34 | googleID: {
35 | type: String,
36 | },
37 | githubID: {
38 | type: String,
39 | },
40 | twitterID: {
41 | type: String,
42 | },
43 | linkedinID: {
44 | type: String,
45 | },
46 | },
47 | {
48 | timestamps: true,
49 | }
50 | );
51 |
52 | // function to check of passwords are matching
53 | userSchema.methods.matchPassword = async function (enteredPassword) {
54 | return await bcrypt.compare(enteredPassword, this.password);
55 | };
56 |
57 | // encrypt password before saving
58 | userSchema.pre('save', async function (next) {
59 | const user = this;
60 | if (!user.isModified('password')) {
61 | return next();
62 | }
63 | const salt = bcrypt.genSaltSync(10);
64 | const hash = bcrypt.hashSync(user.password, salt);
65 | user.password = hash;
66 | next();
67 | });
68 |
69 | const User = mongoose.model('User', userSchema);
70 |
71 | export default User;
72 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Form, InputGroup } from 'react-bootstrap';
3 |
4 | const SearchBox = ({ history }) => {
5 | const [keyword, setKeyword] = useState('');
6 |
7 | // search for the keyword by redirecting to homepage with param
8 | const handleSearch = (e) => {
9 | e.preventDefault();
10 | if (keyword.trim()) {
11 | history.push(`/search/${keyword}`);
12 | } else {
13 | history.push('/');
14 | }
15 | };
16 |
17 | return (
18 |
57 | );
58 | };
59 |
60 | export default SearchBox;
61 |
--------------------------------------------------------------------------------
/backend/models/productModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | // a schema for stroing reviews for each product
4 | const reviewsSchema = mongoose.Schema(
5 | {
6 | user: {
7 | type: mongoose.Schema.Types.ObjectId,
8 | required: true,
9 | ref: 'User',
10 | },
11 | name: { type: String, required: true },
12 | avatar: { type: String, required: true },
13 | rating: { type: Number, required: true, default: 0 },
14 | review: { type: String, required: true },
15 | },
16 | { timestamps: true }
17 | );
18 |
19 | const productSchema = mongoose.Schema(
20 | {
21 | user: {
22 | type: mongoose.Schema.Types.ObjectId,
23 | required: true,
24 | ref: 'User',
25 | },
26 | name: {
27 | type: String,
28 | required: true,
29 | },
30 | image: {
31 | type: String,
32 | required: true,
33 | },
34 | brand: {
35 | type: String,
36 | required: true,
37 | },
38 | category: {
39 | type: String,
40 | required: true,
41 | },
42 | description: {
43 | type: String,
44 | required: true,
45 | },
46 | // store an array of review objs
47 | reviews: [reviewsSchema],
48 | rating: {
49 | type: Number,
50 | required: true,
51 | default: 0,
52 | },
53 | numReviews: {
54 | type: Number,
55 | required: true,
56 | default: 0,
57 | },
58 | price: {
59 | type: Number,
60 | required: true,
61 | default: 0,
62 | },
63 | countInStock: {
64 | type: Number,
65 | required: true,
66 | default: 0,
67 | },
68 | },
69 | {
70 | timestamps: true,
71 | }
72 | );
73 |
74 | const Product = mongoose.model('Product', productSchema);
75 |
76 | export default Product;
77 |
--------------------------------------------------------------------------------
/backend/routes/uploadRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import multer from 'multer';
3 | import multerS3 from 'multer-s3';
4 | import aws from 'aws-sdk';
5 | import dotenv from 'dotenv';
6 | dotenv.config();
7 | const router = express.Router();
8 |
9 | // Create a new instance of the S3 bucket object with the correct user credentials
10 | const s3 = new aws.S3({
11 | accessKeyId: process.env.S3_ACCESS_KEY_ID,
12 | secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
13 | Bucket: 'kosellsbucket',
14 | });
15 |
16 | // Setup the congifuration needed to use multer
17 | const upload = multer({
18 | // Set the storage as the S3 bucker using the correct configuration
19 | storage: multerS3({
20 | s3,
21 | acl: 'public-read', // public S3 object, that can be read
22 | bucket: 'kosellsbucket', // bucket name
23 | key: function (req, file, cb) {
24 | // callback to name the file object in the S3 bucket
25 | // The filename is prefixed with the current time, to avoid multiple files of same name being uploaded to the bucket
26 | cb(null, `${new Date().getTime()}__${file.originalname}`);
27 | },
28 | }),
29 | limits: {
30 | fileSize: 5000000, // maximum file size of 5 MB per file
31 | },
32 |
33 | // Configure the list of file types that are valid
34 | fileFilter(req, file, cb) {
35 | if (!file.originalname.match(/\.(jpeg|jpg|png|webp|svg)$/)) {
36 | return cb(new Error('Unsupported file format'));
37 | }
38 | cb(undefined, true);
39 | },
40 | });
41 |
42 | router.post('/', upload.single('image'), (req, res) => {
43 | if (req.file) res.send(req.file.location);
44 | else {
45 | res.status(401);
46 | throw new Error('Invalid file type');
47 | }
48 | });
49 |
50 | export default router;
51 |
--------------------------------------------------------------------------------
/backend/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import asyncHandler from 'express-async-handler';
3 | import User from '../models/userModel.js';
4 |
5 | const protectRoute = asyncHandler(async (req, res, next) => {
6 | let token;
7 |
8 | // if the header includes a Bearer token
9 | if (
10 | req.headers.authorization &&
11 | req.headers.authorization.startsWith('Bearer')
12 | ) {
13 | try {
14 | // get only the token string
15 | token = req.headers.authorization.split(' ')[1];
16 |
17 | // decode the token to get the corresponding user's id
18 | const decodedToken = jwt.verify(
19 | token,
20 | process.env.JWT_ACCESS_TOKEN_SECRET
21 | );
22 |
23 | // fetch that user from db, but not get the user's password and set this fetched user to the req.user
24 | req.user = await User.findById(decodedToken.id).select('-password');
25 | next();
26 | } catch (error) {
27 | console.log(error);
28 | res.status(401);
29 | throw new Error('Not authorised. Token failed');
30 | }
31 | }
32 | // if the header includes a token for social login case
33 | else if (
34 | req.headers.authorization &&
35 | req.headers.authorization.startsWith('SocialLogin')
36 | ) {
37 | const id = req.headers.authorization.split(' ')[1];
38 | req.user = await User.findById(id);
39 | token = id;
40 | next();
41 | }
42 |
43 | if (!token) {
44 | res.status(401);
45 | throw new Error('Not authorized, no token available');
46 | }
47 | });
48 |
49 | const isAdmin = (req, res, next) => {
50 | if (req.user && req.user.isAdmin) next();
51 | else {
52 | res.status(401);
53 | throw new Error('Not authorised admin');
54 | }
55 | };
56 |
57 | export { protectRoute, isAdmin };
58 |
--------------------------------------------------------------------------------
/frontend/src/constants/productConstants.js:
--------------------------------------------------------------------------------
1 | export const PRODUCT_LIST_REQUEST = 'PRODUCT_LIST_REQUEST';
2 | export const PRODUCT_LIST_SUCCESS = 'PRODUCT_LIST_SUCCESS';
3 | export const PRODUCT_LIST_FAILURE = 'PRODUCT_LIST_FAILURE';
4 |
5 | export const PRODUCT_DETAILS_REQUEST = 'PRODUCT_DETAILS_REQUEST';
6 | export const PRODUCT_DETAILS_SUCCESS = 'PRODUCT_DETAILS_SUCCESS';
7 | export const PRODUCT_DETAILS_FAILURE = 'PRODUCT_DETAILS_FAILURE';
8 |
9 | export const PRODUCT_DELETE_REQUEST = 'PRODUCT_DELETE_REQUEST';
10 | export const PRODUCT_DELETE_SUCCESS = 'PRODUCT_DELETE_SUCCESS';
11 | export const PRODUCT_DELETE_FAILURE = 'PRODUCT_DELETE_FAILURE';
12 |
13 | export const PRODUCT_CREATE_REQUEST = 'PRODUCT_CREATE_REQUEST';
14 | export const PRODUCT_CREATE_SUCCESS = 'PRODUCT_CREATE_SUCCESS';
15 | export const PRODUCT_CREATE_FAILURE = 'PRODUCT_CREATE_FAILURE';
16 | export const PRODUCT_CREATE_RESET = 'PRODUCT_CREATE_RESET';
17 |
18 | export const PRODUCT_UPDATE_REQUEST = 'PRODUCT_UPDATE_REQUEST';
19 | export const PRODUCT_UPDATE_SUCCESS = 'PRODUCT_UPDATE_SUCCESS';
20 | export const PRODUCT_UPDATE_FAILURE = 'PRODUCT_UPDATE_FAILURE';
21 | export const PRODUCT_UPDATE_RESET = 'PRODUCT_UPDATE_RESET';
22 |
23 | export const PRODUCT_CREATE_REVIEW_REQUEST = 'PRODUCT_CREATE_REVIEW_REQUEST';
24 | export const PRODUCT_CREATE_REVIEW_SUCCESS = 'PRODUCT_CREATE_REVIEW_SUCCESS';
25 | export const PRODUCT_CREATE_REVIEW_FAILURE = 'PRODUCT_CREATE_REVIEW_FAILURE';
26 | export const PRODUCT_CREATE_REVIEW_RESET = 'PRODUCT_CREATE_REVIEW_RESET';
27 |
28 | export const PRODUCT_TOP_RATED_REQUEST = 'PRODUCT_TOP_RATED_REQUEST';
29 | export const PRODUCT_TOP_RATED_SUCCESS = 'PRODUCT_TOP_RATED_SUCCESS';
30 | export const PRODUCT_TOP_RATED_FAILURE = 'PRODUCT_TOP_RATED_FAILURE';
31 |
--------------------------------------------------------------------------------
/frontend/src/components/ProductCarousel.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { Carousel, CarouselItem, Image } from 'react-bootstrap';
5 | import Message from './Message';
6 | import { getTopRatedProducts } from '../actions/productActions';
7 | import '../styles/product-carousel.css';
8 | import CarouselSkeleton from '../components/CarouselSkeleton';
9 |
10 | const ProductCarousel = () => {
11 | const dispatch = useDispatch();
12 |
13 | const productTopRated = useSelector((state) => state.productTopRated);
14 | const { error, loading, products } = productTopRated;
15 |
16 | useEffect(() => {
17 | dispatch(getTopRatedProducts());
18 | }, [dispatch]);
19 | return (
20 | <>
21 | {loading && }
22 | {error && (
23 |
24 | {error}
25 |
26 | )}
27 | {/* render carousel only on large screens */}
28 |
34 | {products &&
35 | products.map((product) => (
36 |
37 |
38 |
43 |
44 | {product.name} (
45 | {product.price.toLocaleString('en-IN', {
46 | maximumFractionDigits: 2,
47 | style: 'currency',
48 | currency: 'INR',
49 | })}
50 | )
51 |
52 |
53 |
54 | ))}
55 |
56 | >
57 | );
58 | };
59 |
60 | export default ProductCarousel;
61 |
--------------------------------------------------------------------------------
/backend/seeder.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import colors from 'colors';
3 | import dotenv from 'dotenv';
4 | import users from './data/users.js';
5 | import products from './data/products.js';
6 | import User from './models/userModel.js';
7 | import Product from './models/productModel.js';
8 | import Order from './models/orderModel.js';
9 | import Token from './models/tokenModel.js';
10 | import connectDB from './config/db.js';
11 |
12 | dotenv.config();
13 | connectDB();
14 |
15 | const importData = async () => {
16 | try {
17 | // delete all the current data in all three collections
18 | await User.deleteMany();
19 | await Product.deleteMany();
20 | await Order.deleteMany();
21 | await Token.deleteMany();
22 |
23 | // create an array os users to seed into the DB
24 | const newUsers = await User.insertMany(users);
25 |
26 | // get the admin user document's id
27 | const adminUser = newUsers[0]._id;
28 |
29 | // add this admin user as the user that added all these products into the DB
30 | const sampleProducts = products.map((product) => ({
31 | ...product,
32 | user: adminUser,
33 | }));
34 |
35 | await Product.insertMany(sampleProducts);
36 |
37 | console.log('Data inserted in to the DB'.green.inverse);
38 | process.exit();
39 | } catch (err) {
40 | console.error(`Error: ${err.message}`.red.inverse);
41 | }
42 | };
43 |
44 | const destroyData = async () => {
45 | try {
46 | // delete all the current data in all three collections
47 | await User.deleteMany();
48 | await Product.deleteMany();
49 | await Order.deleteMany();
50 | await Token.deleteMany();
51 |
52 | console.log('Data deleted from the DB'.red.inverse);
53 | process.exit();
54 | } catch (err) {
55 | console.error(`Error: ${err.message}`.red.inverse);
56 | }
57 | };
58 |
59 | // check the npm flag and call appropriate function
60 | if (process.argv[2] === '-d') destroyData();
61 | else importData();
62 |
--------------------------------------------------------------------------------
/backend/models/orderModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const orderSchema = mongoose.Schema(
4 | {
5 | // add a reference to the corresponding user
6 | user: {
7 | type: mongoose.Schema.Types.ObjectId,
8 | required: true,
9 | ref: 'User',
10 | },
11 | orderItems: [
12 | {
13 | qty: { type: Number, required: true, default: 0 },
14 | name: { type: String, required: true },
15 | price: { type: Number, required: true, default: 0 },
16 | image: { type: String, required: true },
17 | product: {
18 | type: mongoose.Schema.Types.ObjectId,
19 | required: true,
20 | ref: 'Product',
21 | },
22 | },
23 | ],
24 | shippingAddress: {
25 | address: { type: String, required: true },
26 | city: { type: String, required: true },
27 | postalCode: { type: String, required: true },
28 | country: { type: String, required: true },
29 | },
30 | paymentMethod: {
31 | type: String,
32 | required: true,
33 | },
34 | // depends on if stripe or paypal method is used
35 | paymentResult: {
36 | id: { type: String },
37 | status: { type: String },
38 | update_time: { type: String },
39 | email_address: { type: String },
40 | },
41 | itemsPrice: {
42 | type: Number,
43 | required: true,
44 | default: 0.0,
45 | },
46 | taxPrice: {
47 | type: Number,
48 | required: true,
49 | default: 0.0,
50 | },
51 | shippingPrice: {
52 | type: Number,
53 | required: true,
54 | default: 0.0,
55 | },
56 | totalPrice: {
57 | type: Number,
58 | required: true,
59 | default: 0.0,
60 | },
61 | isPaid: {
62 | type: Boolean,
63 | default: false,
64 | },
65 | isDelivered: {
66 | type: Boolean,
67 | default: false,
68 | },
69 | paidAt: {
70 | type: Date,
71 | },
72 | deliveredAt: {
73 | type: Date,
74 | },
75 | },
76 | {
77 | timestamps: true,
78 | }
79 | );
80 |
81 | const Order = mongoose.model('Order', orderSchema);
82 |
83 | export default Order;
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-ecommerce-app",
3 | "version": "1.0.0",
4 | "description": "MERN stack E-commerce app",
5 | "main": "server.js",
6 | "type": "module",
7 | "engines": {
8 | "node": "14.x"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1",
12 | "start": "node backend/server.js",
13 | "server": "nodemon backend/server.js",
14 | "client": "npm start --prefix frontend",
15 | "dev": "concurrently \"npm run server\" \"npm run client\"",
16 | "data:import": "node backend/seeder.js",
17 | "data:destroy": "node backend/seeder.js -d",
18 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/Rajatm544/MERN-Ecommerce.git"
23 | },
24 | "author": "Rajat M",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/Rajatm544/MERN-Ecommerce/issues"
28 | },
29 | "homepage": "https://github.com/Rajatm544/MERN-Ecommerce#readme",
30 | "dependencies": {
31 | "aws-sdk": "^2.983.0",
32 | "bcryptjs": "^2.4.3",
33 | "colors": "^1.4.0",
34 | "compression": "^1.7.4",
35 | "connect-flash": "^0.1.1",
36 | "cookie-session": "^1.4.0",
37 | "cors": "^2.8.5",
38 | "dotenv": "^10.0.0",
39 | "express": "^4.17.1",
40 | "express-async-handler": "^1.1.4",
41 | "express-session": "^1.17.2",
42 | "gravatar": "^1.8.2",
43 | "jsonwebtoken": "^8.5.1",
44 | "mongoose": "^5.13.0",
45 | "morgan": "^1.10.0",
46 | "multer": "^1.4.3",
47 | "multer-s3": "^2.9.0",
48 | "nodemailer": "^6.6.3",
49 | "passport": "^0.4.1",
50 | "passport-github2": "^0.1.12",
51 | "passport-google-oauth20": "^2.0.0",
52 | "passport-linkedin-oauth2": "^2.0.0",
53 | "passport-twitter": "^1.0.4",
54 | "stripe": "^8.176.0"
55 | },
56 | "devDependencies": {
57 | "concurrently": "^6.2.0",
58 | "nodemon": "^2.0.8"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/frontend/src/styles/header.css:
--------------------------------------------------------------------------------
1 | button.navbar-toggle-btn:focus {
2 | box-shadow: none;
3 | }
4 |
5 | .navbar-brand {
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-evenly;
9 | width: 12%;
10 | }
11 |
12 | .nav-logo {
13 | width: 1.8em;
14 | height: 1.8em;
15 | }
16 |
17 | .nav-cart-size {
18 | border: 1px solid red;
19 | position: absolute;
20 | height: 1em;
21 | width: 1em;
22 | top: 25%;
23 | color: white;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | text-align: center;
28 | padding: 0.1em;
29 | border-radius: 50%;
30 | background: #FF0800;
31 | margin: 0;
32 | margin-left: 0.8em;
33 | font-weight: bold;
34 | }
35 |
36 | .nav-avatar-container {
37 | display: flex;
38 | align-items: center;
39 | margin-left: 0.8em;
40 | }
41 |
42 | .nav-avatar {
43 | height: 35px;
44 | width: 35px;
45 | border-radius: 50%;
46 | border: 0.2px solid white;
47 | filter: brightness(1.1);
48 | }
49 |
50 | .navbar-icons {
51 | font-size: 1.2em;
52 | }
53 |
54 | @media screen and (max-width:430px) {
55 | nav.navbar {
56 | padding: 0.5rem 0;
57 | }
58 |
59 | .nav-cart-size {
60 | top: 25%;
61 | text-align: center;
62 | padding: 0em;
63 | margin-left: 0.5em;
64 | }
65 |
66 | div.nav-mobile {
67 | display: flex;
68 | flex-flow: row nowrap;
69 | align-items: center;
70 | width: 50%;
71 | }
72 |
73 | a.nav-link {
74 | font-size: 1rem;
75 | }
76 |
77 | .navbar-icons {
78 | font-size: 1em;
79 | }
80 |
81 | .navbar-dropdown-cover {
82 | display: none;
83 | visibility: hidden;
84 | }
85 |
86 | .nav-logo {
87 | margin-right: 0.25em;
88 | }
89 |
90 | .navbar-brand {
91 | width: auto;
92 | max-width: 50%;
93 | }
94 |
95 | .nav-avatar-container {
96 | margin-left: 0;
97 | padding: 0;
98 | }
99 | }
--------------------------------------------------------------------------------
/frontend/src/actions/cartActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import {
3 | CART_ADD_ITEM,
4 | CART_REMOVE_ITEM,
5 | CART_SAVE_SHIPPING_ADDRESS,
6 | CART_SAVE_PAYMENT_METHOD,
7 | } from '../constants/cartConstants';
8 |
9 | // get the product id and the quantity of the item to add to the cart
10 | export const addItem = (id, qty) => async (dispatch, getState) => {
11 | try {
12 | const { data } = await axios.get(`/api/products/${id}`);
13 | dispatch({
14 | type: CART_ADD_ITEM,
15 | payload: {
16 | product: data._id,
17 | name: data.name,
18 | image: data.image,
19 | price: data.price,
20 | countInStock: data.countInStock,
21 | qty,
22 | },
23 | });
24 |
25 | // update the local storage with the new cart
26 | localStorage.setItem(
27 | 'cartItems',
28 | JSON.stringify(getState().cart.cartItems)
29 | );
30 | } catch (error) {
31 | console.error(error);
32 | }
33 | };
34 |
35 | // get the product id to be removed from the cart
36 | export const removeItem = (id) => async (dispatch, getState) => {
37 | try {
38 | dispatch({
39 | type: CART_REMOVE_ITEM,
40 | payload: id,
41 | });
42 | // update the local storage with the updated cart
43 | localStorage.setItem(
44 | 'cartItems',
45 | JSON.stringify(getState().cart.cartItems)
46 | );
47 | } catch (error) {
48 | console.log(error);
49 | }
50 | };
51 |
52 | // get the shipping address data and dispatch corresponding action
53 | export const saveShippingAddress = (data) => async (dispatch) => {
54 | try {
55 | dispatch({
56 | type: CART_SAVE_SHIPPING_ADDRESS,
57 | payload: data,
58 | });
59 | localStorage.setItem('shippingAddress', JSON.stringify(data));
60 | } catch (error) {
61 | console.log(error);
62 | }
63 | };
64 |
65 | // get the option for payment and update the local storage as well
66 | export const savePaymentMethod = (data) => async (dispatch) => {
67 | try {
68 | dispatch({
69 | type: CART_SAVE_PAYMENT_METHOD,
70 | payload: data,
71 | });
72 | localStorage.setItem('paymentMethod', JSON.stringify(data));
73 | } catch (error) {
74 | console.log(error);
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/frontend/src/components/SocialLoginOptions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, Image } from 'react-bootstrap';
3 | import googleLogo from '../assets/googleLogo.png';
4 | import githubLogo from '../assets/githubLogo.png';
5 | import twitterLogo from '../assets/twitterLogo.png';
6 | import linkedinLogo from '../assets/linkedinLogo.png';
7 | import '../styles/social-login-option.css';
8 |
9 | const SocialLoginOptions = () => {
10 | const baseURL = process.env.REACT_APP_BASE_URL;
11 | // display a list of 4 options and make API request to passport login on click
12 | return (
13 |
79 | );
80 | };
81 |
82 | export default SocialLoginOptions;
83 |
--------------------------------------------------------------------------------
/env.md:
--------------------------------------------------------------------------------
1 | # All Environment variables for the server side code
2 |
3 | NODE_ENV = `development or production`
4 |
5 | PORT = `Any port for the server API`
6 |
7 | MONGO_URI = `Mongo DB Atlas connection URI`
8 |
9 | JWT_ACCESS_TOKEN_SECRET = `Random string of alphanumeric characters`
10 |
11 | JWT_REFRESH_TOKEN_SECRET = `Random string of alphanumeric characters`
12 |
13 | JWT_EMAIL_TOKEN_SECRET = `Random string of alphanumeric characters`
14 |
15 | JWT_FORGOT_PASSWORD_TOKEN_SECRET = `Random string of alphanumeric characters`
16 |
17 | MAIL_USERNAME = `Email id of the account used to send the verification email`
18 |
19 | MAIL_PASSWORD = `password for the account used to send the verification email`
20 |
21 | OAUTH_CLIENT_ID = `Google client id for using gmail along with nodemailer`
22 |
23 | OAUTH_CLIENT_SECRET = `Google client secret for using gmail along with nodemailer`
24 |
25 | OAUTH_REFRESH_TOKEN = `Google refresh token to send mails using gmail account in nodemailer`
26 |
27 | GOOGLE_OAUTH_CLIENT_ID = `Google oauth 2.0 client id for using google oauth strategy of passport.js`
28 |
29 | GOOGLE_OAUTH_CLIENT_SECRET = `Google oauth 2.0 client secret for using google oauth strategy of passport.js`
30 |
31 | GITHUB_CLIENT_ID = `Github client id for using github strategy of passport.js`
32 |
33 | GITHUB_CLIENT_SECRET = `Github client secret for using github strategy of passport.js`
34 |
35 | TWITTER_CONSUMER_KEY = `Twitter consumer key for using twitter strategy of passport.js`
36 |
37 | TWITTER_CONSUMER_SECRET = `Twitter consumer secret for using twitter strategy of passport.js`
38 |
39 | LINKEDIN_CLIENT_ID = `Linkedin client id for using linkedin strategy of passport.js`
40 |
41 | LINKEDIN_CLIENT_SECRET = `Linkedin client secret for using linkedin strategy of passport.js`
42 |
43 | COOKIE_SESSION_KEY = `Random string of alphanumeric characters`
44 |
45 | PAYPAL_CLIENT_ID = `Paypal client id to accept paypal payments`
46 |
47 | S3_SECRET_ACCESS_KEY =`AWS S3 storage bucket's secret access key for the aws-sdk`
48 |
49 | S3_ACCESS_KEY_ID = `AWS S3 storage bucket's secret access ID for the aws-sdk`
50 |
51 | STRIPE_SECRET_KEY = `Stripe secret key for accepting credit/debit card payments`
52 |
53 | FRONTEND_BASE_URL = `URL for server side`
54 |
55 | BACKEND_BASE_URL = `URL for the client side`
56 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:5000/",
6 | "dependencies": {
7 | "@stripe/react-stripe-js": "^1.5.0",
8 | "@stripe/stripe-js": "^1.18.0",
9 | "@testing-library/jest-dom": "^5.11.4",
10 | "@testing-library/react": "^11.1.0",
11 | "@testing-library/user-event": "^12.1.10",
12 | "axios": "^0.21.1",
13 | "bootstrap": "^5.0.2",
14 | "follow-redirects": "^1.14.8",
15 | "immer": "^9.0.7",
16 | "install": "^0.13.0",
17 | "is-svg": "^4.3.2",
18 | "json-schema": "^0.4.0",
19 | "node-forge": "^1.2.1",
20 | "nth-check": "^2.0.1",
21 | "postcss": "^8.4.5",
22 | "react": "^17.0.2",
23 | "react-bootstrap": "^2.0.0-beta.6",
24 | "react-dom": "^17.0.2",
25 | "react-helmet-async": "^1.1.2",
26 | "react-paypal-button-v2": "^2.6.3",
27 | "react-redux": "^7.2.4",
28 | "react-router-bootstrap": "^0.25.0",
29 | "react-router-dom": "^5.2.0",
30 | "react-scripts": "4.0.3",
31 | "react-stripe-checkout": "^2.6.3",
32 | "redux": "^4.1.0",
33 | "redux-devtools-extension": "^2.13.9",
34 | "redux-thunk": "^2.3.0",
35 | "web-vitals": "^0.2.4",
36 | "workbox-background-sync": "^5.1.3",
37 | "workbox-broadcast-update": "^5.1.3",
38 | "workbox-cacheable-response": "^5.1.3",
39 | "workbox-core": "^5.1.3",
40 | "workbox-expiration": "^5.1.3",
41 | "workbox-google-analytics": "^5.1.3",
42 | "workbox-navigation-preload": "^5.1.3",
43 | "workbox-precaching": "^5.1.3",
44 | "workbox-range-requests": "^5.1.3",
45 | "workbox-routing": "^5.1.3",
46 | "workbox-strategies": "^5.1.3",
47 | "workbox-streams": "^5.1.3",
48 | "xmldom": "^0.6.0"
49 | },
50 | "scripts": {
51 | "start": "react-scripts start",
52 | "build": "react-scripts build",
53 | "test": "react-scripts test",
54 | "eject": "react-scripts eject"
55 | },
56 | "eslintConfig": {
57 | "extends": [
58 | "react-app",
59 | "react-app/jest"
60 | ]
61 | },
62 | "browserslist": {
63 | "production": [
64 | ">0.2%",
65 | "not dead",
66 | "not op_mini all"
67 | ],
68 | "development": [
69 | "last 1 chrome version",
70 | "last 1 firefox version",
71 | "last 1 safari version"
72 | ]
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/frontend/src/components/CheckoutStatus.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Nav } from 'react-bootstrap';
3 | import { LinkContainer } from 'react-router-bootstrap';
4 | import '../styles/check-status.css';
5 |
6 | // there are 4 steps in the checkout process
7 | // step 1 is logging in
8 | // step 2 is shipping address input
9 | // step 3 is selecting payment option
10 | // step 4 is placing the order and seeing payment button
11 | const CheckoutStatus = ({ step1, step2, step3, step4 }) => {
12 | return (
13 |
14 |
15 |
21 | {step1 ? (
22 |
23 | Sign In
24 |
25 | ) : (
26 |
Sign In
27 | )}
28 |
29 |
30 |
31 |
39 | {step2 ? (
40 |
41 | Shipping
42 |
43 | ) : (
44 |
Shipping
45 | )}
46 |
47 |
48 |
49 |
50 |
58 | {step3 ? (
59 |
60 | Payment
61 |
62 | ) : (
63 |
Payment
64 | )}
65 |
66 |
67 |
68 |
69 |
77 | {step4 ? (
78 |
79 | Place Order
80 |
81 | ) : (
82 |
Place Order
83 | )}
84 |
85 |
86 | );
87 | };
88 |
89 | export default CheckoutStatus;
90 |
--------------------------------------------------------------------------------
/backend/utils/sendMail.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import transporter from '../utils/transporter.js';
3 | import generateToken from '../utils/generateToken.js';
4 |
5 | dotenv.config();
6 |
7 | const sendMail = async (id, email, option) => {
8 | const frontendURL = process.env.FRONTEND_BASE_URL;
9 |
10 | // send email for the email verification option
11 | if (option === 'email verification') {
12 | // create a new JWT to verify user via email
13 | const emailToken = generateToken(id, 'email');
14 | const url = `${frontendURL}/user/confirm/${emailToken}`;
15 |
16 | // set the correct mail option
17 | const mailOptions = {
18 | from: process.env.EMAIL, // sender address
19 | to: email,
20 | subject: 'Confirm your email for Kosells', // Subject line
21 | html: `
22 |
Account Created!
23 | Click this link to
24 |
verify your account
25 |
26 | Note that this link is valid only for the next 15 minutes.
27 |
28 |
29 | `,
30 | };
31 |
32 | const mailSent = await transporter.sendMail(
33 | mailOptions,
34 | (err, info) => {
35 | if (err) {
36 | console.log(err);
37 | } else {
38 | console.log(info);
39 | }
40 | }
41 | );
42 |
43 | // send a promise since nodemailer is async
44 | if (mailSent) return Promise.resolve(1);
45 | }
46 | // send a mail for resetting password if forgot password
47 | else if (option === 'forgot password') {
48 | // create a new JWT to verify user via email
49 | const forgetPasswordToken = generateToken(id, 'forgot password');
50 | const url = `${frontendURL}/user/password/reset/${forgetPasswordToken}`;
51 | const mailOptions = {
52 | from: process.env.EMAIL, // sender address
53 | to: email,
54 | subject: 'Reset Password for Kosells', // Subject line
55 | html: `
56 |
Reset Password for your Kosells account
57 |
58 | Forgot your password? No worries! Just click this link to
59 |
reset your password .
60 |
61 | Note that this link is valid for only the next 10 minutes.
62 |
63 |
64 | `,
65 | };
66 |
67 | const mailSent = await transporter.sendMail(
68 | mailOptions,
69 | (err, info) => {
70 | if (err) {
71 | console.log(err);
72 | } else {
73 | console.log(info);
74 | }
75 | }
76 | );
77 |
78 | if (mailSent) return Promise.resolve(1);
79 | }
80 | };
81 |
82 | export default sendMail;
83 |
--------------------------------------------------------------------------------
/frontend/src/pages/ConfirmPage.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | import React, { useState, useEffect } from 'react';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { Link } from 'react-router-dom';
5 | import { Card } from 'react-bootstrap';
6 | import Loader from '../components/Loader';
7 | import Message from '../components/Message';
8 | import { confirmUser } from '../actions/userActions';
9 | import Meta from '../components/Meta';
10 |
11 | const ConfirmPage = ({ match, history }) => {
12 | const dispatch = useDispatch();
13 | const userConfirm = useSelector((state) => state.userConfirm); // get the userInfo to check if user is confirmed or not
14 | const { loading, error, isConfirmed } = userConfirm;
15 |
16 | const userLogin = useSelector((state) => state.userLogin);
17 | const { userInfo } = userLogin;
18 | const [isLoggedIn, setIsLoggedIn] = useState(false);
19 |
20 | useEffect(() => {
21 | if (userInfo) {
22 | setIsLoggedIn(true);
23 | } else {
24 | setIsLoggedIn(false);
25 | }
26 | }, []);
27 |
28 | useEffect(() => {
29 | // confirm user once the email token is available
30 | dispatch(confirmUser(match.params.token, isLoggedIn));
31 | }, [dispatch, match, isLoggedIn]);
32 |
33 | if (loading || (!isConfirmed && !error)) {
34 | return ;
35 | } else if (error) {
36 | // redirect to login page after a 10 seconds
37 | setTimeout(() => {
38 | history.push('/login');
39 | }, 10000);
40 | return (
41 |
42 | Verfication Failed. Please try to login again.
43 |
44 | );
45 | } else if (isConfirmed) {
46 | // set a variable in local storage to fill email aftrer redirecting to login page after email confirmation
47 | localStorage.setItem('fillEmailOnLoginPage', 'true');
48 | return (
49 |
50 |
51 |
52 | Account Confirmed
53 |
54 | {setIsLoggedIn
55 | ? 'Your account has been successfully verified! Go on and shop for the best deals of the day!'
56 | : `Your account has been successfully verified! Please
57 | login and start exploring the best deals on all your
58 | favorite products.`}
59 |
60 | {!setIsLoggedIn ? Login : null}
61 |
62 |
63 | );
64 | }
65 | };
66 |
67 | export default ConfirmPage;
68 |
--------------------------------------------------------------------------------
/backend/data/products.js:
--------------------------------------------------------------------------------
1 | const products = [
2 | {
3 | name: 'Airpods Wireless Bluetooth Headphones',
4 | image: '/images/airpods.jpg',
5 | description:
6 | 'Bluetooth technology lets you connect it with compatible devices wirelessly High-quality AAC audio offers immersive listening experience Built-in microphone allows you to take calls while working',
7 | brand: 'Apple',
8 | category: 'Electronics',
9 | price: 14999,
10 | countInStock: 10,
11 | rating: 4.5,
12 | numReviews: 12,
13 | },
14 | {
15 | name: 'iPhone 11 Pro 256GB Memory',
16 | image: '/images/phone.jpg',
17 | description:
18 | 'Introducing the iPhone 11 Pro. A transformative triple-camera system that adds tons of capability without complexity. An unprecedented leap in battery life',
19 | brand: 'Apple',
20 | category: 'Electronics',
21 | price: 49990,
22 | countInStock: 7,
23 | rating: 4.0,
24 | numReviews: 8,
25 | },
26 | {
27 | name: 'Cannon EOS 80D DSLR Camera',
28 | image: '/images/camera.jpg',
29 | description:
30 | 'Characterized by versatile imaging specs, the Canon EOS 80D further clarifies itself using a pair of robust focusing systems and an intuitive design',
31 | brand: 'Cannon',
32 | category: 'Electronics',
33 | price: 92500,
34 | countInStock: 5,
35 | rating: 3,
36 | numReviews: 12,
37 | },
38 | {
39 | name: 'Sony Playstation 4 Pro White Version',
40 | image: '/images/playstation.jpg',
41 | description:
42 | 'The ultimate home entertainment center starts with PlayStation. Whether you are into gaming, HD movies, television, music',
43 | brand: 'Sony',
44 | category: 'Electronics',
45 | price: 24500,
46 | countInStock: 11,
47 | rating: 5,
48 | numReviews: 12,
49 | },
50 | {
51 | name: 'Logitech G-Series Gaming Mouse',
52 | image: '/images/mouse.jpg',
53 | description:
54 | 'Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience',
55 | brand: 'Logitech',
56 | category: 'Electronics',
57 | price: 2495,
58 | countInStock: 7,
59 | rating: 3.5,
60 | numReviews: 10,
61 | },
62 | {
63 | name: 'Amazon Echo Dot 3rd Generation',
64 | image: '/images/alexa.jpg',
65 | description:
66 | 'Meet Echo Dot - Our most popular smart speaker with a fabric design. It is our most compact smart speaker that fits perfectly into small space',
67 | brand: 'Amazon',
68 | category: 'Electronics',
69 | price: 1949,
70 | countInStock: 0,
71 | rating: 4,
72 | numReviews: 12,
73 | },
74 | ];
75 |
76 | export default products;
77 |
--------------------------------------------------------------------------------
/backend/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {
3 | authUser,
4 | getUserProfile,
5 | getUserData,
6 | getAccessToken,
7 | registerUser,
8 | confirmUser,
9 | mailForEmailVerification,
10 | mailForPasswordReset,
11 | resetUserPassword,
12 | updateUserProfile,
13 | getAllUsers,
14 | deleteUser,
15 | getUserById,
16 | updateUser,
17 | } from '../controllers/userControllers.js';
18 | import { protectRoute, isAdmin } from '../middleware/authMiddleware.js';
19 |
20 | const router = express.Router();
21 |
22 | // @desc register a new user & get all users if admin
23 | // @route POST /api/users/
24 | // @access PUBLIC || PRIVATE?ADMIN
25 | router.route('/').post(registerUser).get(protectRoute, isAdmin, getAllUsers);
26 |
27 | // @desc authenticate user and get token
28 | // @route POST /api/users/login
29 | // @access PUBLIC
30 | router.route('/login').post(authUser);
31 |
32 | // @desc confirm the email address of the registered user
33 | // @route GET /api/users/confirm
34 | // @access PUBLIC
35 | router.route('/confirm/:token').get(confirmUser);
36 |
37 | // @desc send a mail with the link to verify mail, to be used if the user forgot to verify mail after registration
38 | // @route POST /api/users/confirm
39 | // @access PUBLIC
40 | router.route('/confirm').post(mailForEmailVerification);
41 |
42 | // @desc send a mail with the link to reset password
43 | // @route POST /api/users/reset
44 | // and
45 | // @desc reset password of any verified user
46 | // @route PUT /api/users/reset
47 |
48 | // @access PUBLIC
49 | router.route('/reset').post(mailForPasswordReset).put(resetUserPassword);
50 |
51 | // @desc obtain new access tokens using the refresh tokens
52 | // @route GET /api/users/refresh
53 | // @access PUBLIC
54 | router.route('/refresh').post(getAccessToken);
55 |
56 | // @desc get data for an authenticated user, and update data for an authenticated user
57 | // @route PUT & GET /api/users/profile
58 | // @access PRIVATE
59 | router
60 | .route('/profile')
61 | .get(protectRoute, getUserProfile)
62 | .put(protectRoute, updateUserProfile);
63 |
64 | // @desc get user data for google login in the frontend
65 | // @route POST /api/users/passport/data
66 | // @access PUBLIC
67 | router.route('/passport/data').post(getUserData);
68 |
69 | // @desc Delete a user, get a user by id, update the user
70 | // @route DELETE /api/users/:id
71 | // @access PRIVATE/ADMIN
72 | router
73 | .route('/:id')
74 | .delete(protectRoute, isAdmin, deleteUser)
75 | .get(protectRoute, isAdmin, getUserById)
76 | .put(protectRoute, isAdmin, updateUser);
77 |
78 | export default router;
79 |
--------------------------------------------------------------------------------
/frontend/src/constants/userConstants.js:
--------------------------------------------------------------------------------
1 | export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST';
2 | export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS';
3 | export const USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
4 | export const USER_LOGIN_REFRESH_REQUEST = 'USER_LOGIN_REFRESH_REQUEST';
5 | export const USER_LOGIN_REFRESH_SUCCESS = 'USER_LOGIN_REFRESH_SUCCESS';
6 | export const USER_LOGIN_REFRESH_FAILURE = 'USER_LOGIN_REFRESH_FAILURE';
7 | export const USER_LOGOUT = 'USER_LOGOUT';
8 |
9 | export const USER_REGISTER_REQUEST = 'USER_REGISTER_REQUEST';
10 | export const USER_REGISTER_SUCCESS = 'USER_REGISTER_SUCCESS';
11 | export const USER_REGISTER_FAILURE = 'USER_REGISTER_FAILURE';
12 |
13 | export const USER_RESET_PASSWORD_REQUEST = 'USER_RESET_PASSWORD_REQUEST';
14 | export const USER_RESET_PASSWORD_SUCCESS = 'USER_RESET_PASSWORD_SUCCESS';
15 | export const USER_RESET_PASSWORD_FAILURE = 'USER_RESET_PASSWORD_FAILURE';
16 |
17 | // To send the email verification link
18 | export const USER_EMAIL_VERIFICATION_REQUEST =
19 | 'USER_EMAIL_VERIFICATION_REQUEST';
20 | export const USER_EMAIL_VERIFICATION_SUCCESS =
21 | 'USER_EMAIL_VERIFICATION_SUCCESS';
22 | export const USER_EMAIL_VERIFICATION_FAILURE =
23 | 'USER_EMAIL_VERIFICATION_FAILURE';
24 |
25 | // To actually verify the email token and confirm the user email
26 | export const USER_CONFIRM_REQUEST = 'USER_CONFIRM_REQUEST';
27 | export const USER_CONFIRM_SUCCESS = 'USER_CONFIRM_SUCCESS';
28 | export const USER_CONFIRM_FAILURE = 'USER_CONFIRM_FAILURE';
29 |
30 | export const USER_DETAILS_REQUEST = 'USER_DETAILS_REQUEST';
31 | export const USER_DETAILS_SUCCESS = 'USER_DETAILS_SUCCESS';
32 | export const USER_DETAILS_FAILURE = 'USER_DETAILS_FAILURE';
33 | export const USER_DETAILS_RESET = 'USER_DETAILS_RESET';
34 |
35 | export const USER_PROFILE_UPDATE_REQUEST = 'USER_PROFILE_UPDATE_REQUEST';
36 | export const USER_PROFILE_UPDATE_SUCCESS = 'USER_PROFILE_UPDATE_SUCCESS';
37 | export const USER_PROFILE_UPDATE_FAILURE = 'USER_PROFILE_UPDATE_FAILURE';
38 | export const USER_PROFILE_UPDATE_RESET = 'USER_PROFILE_UPDATE_RESET';
39 |
40 | export const USER_LIST_REQUEST = 'USER_LIST_REQUEST';
41 | export const USER_LIST_SUCCESS = 'USER_LIST_SUCCESS';
42 | export const USER_LIST_FAILURE = 'USER_LIST_FAILURE';
43 | export const USER_LIST_RESET = 'USER_LIST_RESET';
44 |
45 | export const USER_DELETE_REQUEST = 'USER_DELETE_REQUEST';
46 | export const USER_DELETE_SUCCESS = 'USER_DELETE_SUCCESS';
47 | export const USER_DELETE_FAILURE = 'USER_DELETE_FAILURE';
48 |
49 | export const USER_UPDATE_REQUEST = 'USER_UPDATE_REQUEST';
50 | export const USER_UPDATE_SUCCESS = 'USER_UPDATE_SUCCESS';
51 | export const USER_UPDATE_FAILURE = 'USER_UPDATE_FAILURE';
52 | export const USER_UPDATE_RESET = 'USER_UPDATE_RESET';
53 |
--------------------------------------------------------------------------------
/frontend/src/components/ImageMagnifier.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Image } from 'react-bootstrap';
3 |
4 | const ImageMagnifier = ({
5 | src,
6 | alt,
7 | title,
8 | width,
9 | height,
10 | magnifierHeight = 200,
11 | magnifieWidth = 200,
12 | zoomLevel = 1.5,
13 | }) => {
14 | const [[x, y], setXY] = useState([0, 0]);
15 | const [[imgWidth, imgHeight], setSize] = useState([0, 0]);
16 | const [showMagnifier, setShowMagnifier] = useState(false);
17 | return (
18 |
24 |
{
31 | // update image size and turn-on magnifier
32 | const elem = e.currentTarget;
33 | const { width, height } = elem.getBoundingClientRect();
34 | setSize([width, height]);
35 | setShowMagnifier(true);
36 | }}
37 | onMouseMove={(e) => {
38 | // update cursor position
39 | const elem = e.currentTarget;
40 | const { top, left } = elem.getBoundingClientRect();
41 |
42 | // calculate cursor position on the image
43 | const x = e.pageX - left - window.pageXOffset;
44 | const y = e.pageY - top - window.pageYOffset;
45 | setXY([x, y]);
46 | }}
47 | onMouseLeave={() => {
48 | // close magnifier
49 | setShowMagnifier(false);
50 | }}
51 | alt={alt}
52 | />
53 |
54 |
87 |
88 | );
89 | };
90 |
91 | export default ImageMagnifier;
92 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import dotenv from 'dotenv';
3 | import connectDB from './config/db.js';
4 | import colors from 'colors'; // color the statements in the server side console log
5 | import morgan from 'morgan'; // show the API endpoints
6 | import compression from 'compression'; // use gzip compression in the express server
7 | import cors from 'cors'; // allow cross origin requests
8 | import passport from 'passport'; // for all social login options
9 | import cookieSession from 'cookie-session'; // for implementing cookie sessions for passport
10 | import flash from 'connect-flash'; // so that passport flash messages can work
11 | import path from 'path';
12 |
13 | // middleware
14 | import { notFound, errorHandler } from './middleware/errorMiddleware.js';
15 |
16 | import productRoutes from './routes/productRoutes.js';
17 | import userRoutes from './routes/userRoutes.js';
18 | import authRoutes from './routes/authRoutes.js';
19 | import orderRoutes from './routes/orderRoutes.js';
20 | import configRoutes from './routes/configRoutes.js';
21 | import uploadRoutes from './routes/uploadRoutes.js';
22 | import setupPassport from './config/passportSetup.js';
23 |
24 | dotenv.config();
25 | const app = express();
26 |
27 | // use morgan in development mode
28 | if (process.env.NODE_ENV === 'development') app.use(morgan('dev'));
29 |
30 | // connect to the mongoDB database
31 | connectDB();
32 |
33 | app.use(express.json()); // middleware to use req.body
34 | app.use(cors()); // to avoid CORS errors
35 | app.use(compression()); // to use gzip
36 |
37 | // use cookie sessions
38 | app.use(
39 | cookieSession({
40 | maxAge: 1000 * 60 * 60 * 24, // 1 day
41 | keys: [process.env.COOKIE_SESSION_KEY],
42 | })
43 | );
44 |
45 | // initialise passport middleware to use sessions, and flash messages
46 | app.use(passport.initialize());
47 | app.use(passport.session());
48 | app.use(flash());
49 |
50 | // setup passport
51 | setupPassport();
52 |
53 | // configure all the routes
54 | app.use('/api/products', productRoutes);
55 | app.use('/api/users', userRoutes);
56 | app.use('/api/auth', authRoutes);
57 | app.use('/api/orders', orderRoutes);
58 | app.use('/api/config', configRoutes);
59 | app.use('/api/upload', uploadRoutes);
60 |
61 | const __dirname = path.resolve();
62 |
63 | // To prepare for deployment
64 | if (process.env.NODE_ENV === 'production') {
65 | app.use(express.static(path.join(__dirname, '/frontend/build')));
66 |
67 | app.use('*', (req, res) =>
68 | res.sendFile(path.resolve(__dirname, 'frontend', 'build', 'index.html'))
69 | );
70 | }
71 |
72 | // middleware to act as fallback for all 404 errors
73 | app.use(notFound);
74 |
75 | // configure a custome error handler middleware
76 | app.use(errorHandler);
77 |
78 | const PORT = process.env.PORT || 5000;
79 | app.listen(PORT, () =>
80 | console.log(
81 | `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow
82 | .bold
83 | )
84 | );
85 |
--------------------------------------------------------------------------------
/frontend/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from 'workbox-core';
11 | import { ExpirationPlugin } from 'workbox-expiration';
12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
13 | import { registerRoute } from 'workbox-routing';
14 | import { StaleWhileRevalidate } from 'workbox-strategies';
15 |
16 | clientsClaim();
17 |
18 | // Precache all of the assets generated by your build process.
19 | // Their URLs are injected into the manifest variable below.
20 | // This variable must be present somewhere in your service worker file,
21 | // even if you decide not to use precaching. See https://cra.link/PWA
22 | precacheAndRoute(self.__WB_MANIFEST);
23 |
24 | // Set up App Shell-style routing, so that all navigation requests
25 | // are fulfilled with your index.html shell. Learn more at
26 | // https://developers.google.com/web/fundamentals/architecture/app-shell
27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
28 | registerRoute(
29 | // Return false to exempt requests from being fulfilled by index.html.
30 | ({ request, url }) => {
31 | // If this isn't a navigation, skip.
32 | if (request.mode !== 'navigate') {
33 | return false;
34 | } // If this is a URL that starts with /_, skip.
35 |
36 | if (url.pathname.startsWith('/_')) {
37 | return false;
38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
39 |
40 | // ignore caching the routes that have /api/auth in the react app as they are meant to be API endpoint calls
41 | if (url.pathname.indexOf('/api/auth') !== -1) {
42 | return false;
43 | }
44 |
45 | if (url.pathname.match(fileExtensionRegexp)) {
46 | return false;
47 | } // Return true to signal that we want to use the handler.
48 |
49 | return true;
50 | },
51 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
52 | );
53 |
54 | // An example runtime caching route for requests that aren't handled by the
55 | // precache, in this case same-origin .png requests like those from in public/
56 | registerRoute(
57 | // Add in any other file extensions or routing criteria as needed.
58 | ({ url }) =>
59 | url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
60 | new StaleWhileRevalidate({
61 | cacheName: 'images',
62 | plugins: [
63 | // Ensure that once this runtime cache reaches a maximum size the
64 | // least-recently used images are removed.
65 | new ExpirationPlugin({ maxEntries: 50 }),
66 | ],
67 | })
68 | );
69 |
70 | // This allows the web app to trigger skipWaiting via
71 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
72 | self.addEventListener('message', (event) => {
73 | if (event.data && event.data.type === 'SKIP_WAITING') {
74 | self.skipWaiting();
75 | }
76 | });
77 |
78 | // Any other custom service worker logic can go here.
79 |
--------------------------------------------------------------------------------
/frontend/src/pages/PaymentPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { Form, Button, Col } from 'react-bootstrap';
4 | import FormContainer from '../components/FormContainer';
5 | import CheckoutStatus from '../components/CheckoutStatus';
6 | import { savePaymentMethod } from '../actions/cartActions';
7 | import { refreshLogin, getUserDetails } from '../actions/userActions';
8 |
9 | const PaymentPage = ({ history }) => {
10 | const dispatch = useDispatch();
11 | const cart = useSelector((state) => state.cart);
12 | const { shippingAddress } = cart;
13 |
14 | const [paymentMethod, setPaymentMethod] = useState('Credit/Debit Card'); // default option is the stripe one, but users might not understand 'stripe'
15 | const userLogin = useSelector((state) => state.userLogin);
16 | const { userInfo } = userLogin;
17 |
18 | const userDetails = useSelector((state) => state.userDetails);
19 | const { error } = userDetails;
20 |
21 | // fetch user details
22 | useEffect(() => {
23 | userInfo
24 | ? userInfo.isSocialLogin
25 | ? dispatch(getUserDetails(userInfo.id))
26 | : dispatch(getUserDetails('profile'))
27 | : dispatch(getUserDetails('profile'));
28 | }, [userInfo, dispatch]);
29 |
30 | // refresh the access tokens when user details throws an error
31 | useEffect(() => {
32 | if (error && userInfo && !userInfo.isSocialLogin) {
33 | const user = JSON.parse(localStorage.getItem('userInfo'));
34 | user && dispatch(refreshLogin(user.email));
35 | }
36 | }, [error, dispatch, userInfo]);
37 |
38 | // if shipping address is empty, redirect
39 | useEffect(() => {
40 | if (!shippingAddress) {
41 | history.push('/shipping');
42 | }
43 | // eslint-disable-next-line react-hooks/exhaustive-deps
44 | }, []);
45 |
46 | const handleChange = (e) => {
47 | setPaymentMethod(e.target.value);
48 | };
49 |
50 | const handleSubmit = (e) => {
51 | e.preventDefault();
52 | dispatch(savePaymentMethod(paymentMethod));
53 | history.push('/placeorder');
54 | };
55 |
56 | return (
57 |
58 | {/* three steps are done in the checkout process */}
59 |
60 |
66 |
Payment Method
67 |
69 |
70 |
80 |
90 |
91 |
92 |
93 |
94 | Continue
95 |
96 |
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default PaymentPage;
104 |
--------------------------------------------------------------------------------
/frontend/src/components/CheckoutForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { Form, Button } from 'react-bootstrap';
5 | import { payOrder } from '../actions/orderActions';
6 | import { savePaymentMethod } from '../actions/cartActions';
7 | import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; // for stripe CC component
8 | import Message from '../components/Message';
9 |
10 | const CheckoutForm = ({ price, orderID }) => {
11 | const [error, setError] = useState(''); // from the stripe component itself
12 | const dispatch = useDispatch();
13 | const [clientSecret, setClientSecret] = useState(''); // from the payment intent sent from server
14 | const stripe = useStripe();
15 | const elements = useElements();
16 |
17 | const userLogin = useSelector((state) => state.userLogin);
18 | const { userInfo } = userLogin;
19 |
20 | // STEP 1: create a payment intent and getting the secret
21 | useEffect(() => {
22 | const getClientSecret = async () => {
23 | const { data } = await axios.post(
24 | '/api/orders/stripe-payment',
25 | { price, email: userInfo.email },
26 | {
27 | headers: {
28 | 'Content-Type': 'application/json',
29 | },
30 | }
31 | );
32 | setClientSecret(data.clientSecret);
33 | };
34 |
35 | if (userInfo && price) getClientSecret();
36 | }, [price, userInfo]);
37 |
38 | // STEP 2: make the payment after filling the form properly
39 | const makePayment = async (e) => {
40 | e.preventDefault();
41 | if (!stripe || !elements) {
42 | // Stripe.js has not yet loaded.
43 | // Make sure to disable form submission until Stripe.js has loaded.
44 | return;
45 | }
46 | if (clientSecret) {
47 | const payload = await stripe.confirmCardPayment(clientSecret, {
48 | payment_method: {
49 | card: elements.getElement(CardElement),
50 | billing_details: {
51 | name: userInfo.name,
52 | email: userInfo.email,
53 | },
54 | },
55 | });
56 | // console.log(payload.error);
57 | if (!payload.error) {
58 | dispatch(savePaymentMethod('Stripe'));
59 | dispatch(
60 | payOrder(orderID, {
61 | ...payload.paymentIntent,
62 | paymentMode: 'stripe',
63 | })
64 | );
65 | } else {
66 | setError(payload.error.message);
67 | }
68 | } else {
69 | window.location.reload();
70 | }
71 | };
72 |
73 | // render a checkout form for filling details about credit or debit cards
74 | return (
75 |
86 |
103 |
104 |
105 |
106 | Pay Now
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default CheckoutForm;
114 |
--------------------------------------------------------------------------------
/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, combineReducers } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { composeWithDevTools } from 'redux-devtools-extension';
4 |
5 | import {
6 | productListReducer,
7 | productDetailsReducer,
8 | productDeleteReducer,
9 | productCreateReducer,
10 | productCreateReviewReducer,
11 | productUpdateReducer,
12 | productTopRatedReducer,
13 | } from './reducers/productReducers';
14 |
15 | import { cartReducer } from './reducers/cartReducers';
16 |
17 | import {
18 | userLoginReducer,
19 | userLoginRefreshReducer,
20 | userRegisterReducer,
21 | userSendEmailVerficationReducer,
22 | userConfirmReducer,
23 | userResetPasswordReducer,
24 | userDetailsReducer,
25 | userProfileUpdateReducer,
26 | userListReducer,
27 | userDeleteReducer,
28 | userUpdateReducer,
29 | } from './reducers/userReducers';
30 |
31 | import {
32 | orderCreateReducer,
33 | orderDetailsReducer,
34 | orderPayReducer,
35 | orderDeliverReducer,
36 | orderListUserReducer,
37 | orderListAllReducer,
38 | } from './reducers/orderReducers';
39 |
40 | // combine all the above reducers to the store
41 | const reducer = combineReducers({
42 | productList: productListReducer,
43 | productDetails: productDetailsReducer,
44 | productDelete: productDeleteReducer,
45 | productCreate: productCreateReducer,
46 | productCreateReview: productCreateReviewReducer,
47 | productUpdate: productUpdateReducer,
48 | productTopRated: productTopRatedReducer,
49 | cart: cartReducer,
50 | userLogin: userLoginReducer,
51 | userLoginRefresh: userLoginRefreshReducer,
52 | userRegister: userRegisterReducer,
53 | userSendEmailVerfication: userSendEmailVerficationReducer,
54 | userConfirm: userConfirmReducer,
55 | userResetPassword: userResetPasswordReducer,
56 | userDetails: userDetailsReducer,
57 | userProfileUpdate: userProfileUpdateReducer,
58 | userList: userListReducer,
59 | userDelete: userDeleteReducer,
60 | userUpdate: userUpdateReducer,
61 | orderCreate: orderCreateReducer,
62 | orderDetails: orderDetailsReducer,
63 | orderPay: orderPayReducer,
64 | orderDeliver: orderDeliverReducer,
65 | orderListUser: orderListUserReducer,
66 | orderListAll: orderListAllReducer,
67 | });
68 |
69 | // get a few cart items from the local storage
70 | const cartItemsFromLocalStorage = localStorage.getItem('cartItems')
71 | ? JSON.parse(localStorage.getItem('cartItems'))
72 | : [];
73 |
74 | // get the user info from local storage
75 | const userInfoFromLocalStorage = localStorage.getItem('userInfo')
76 | ? JSON.parse(localStorage.getItem('userInfo'))
77 | : null;
78 |
79 | // get the shipping address from local storage
80 | const shippingAddressFromLocalStorage = localStorage.getItem('shippingAddress')
81 | ? JSON.parse(localStorage.getItem('shippingAddress'))
82 | : {};
83 |
84 | // get refresh token from the local storage
85 | const tokenInfoFromLocalStoage = localStorage.getItem('refreshToken')
86 | ? localStorage.getItem('refreshToken')
87 | : null;
88 |
89 | // set the initial state based on above local storage values
90 | const initialState = {
91 | cart: {
92 | cartItems: [...cartItemsFromLocalStorage],
93 | shippingAddress: shippingAddressFromLocalStorage,
94 | },
95 | userLogin: {
96 | userInfo: userInfoFromLocalStorage,
97 | },
98 | userLoginRefresh: {
99 | tokenInfo: tokenInfoFromLocalStoage,
100 | },
101 | };
102 |
103 | // user redux thunk for making async calls
104 | const middleware = [thunk];
105 |
106 | // create the redux store
107 | const store = createStore(
108 | reducer,
109 | initialState,
110 | composeWithDevTools(applyMiddleware(...middleware))
111 | );
112 |
113 | export default store;
114 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3 | import { Container } from 'react-bootstrap';
4 | import Header from './components/Header';
5 | import Footer from './components/Footer';
6 |
7 | import HomePage from './pages/HomePage';
8 | import ProductPage from './pages/ProductPage';
9 | import CartPage from './pages/CartPage';
10 | import LoginPage from './pages/LoginPage';
11 | import RegisterPage from './pages/RegisterPage';
12 | import ProfilePage from './pages/ProfilePage';
13 | import ConfirmPage from './pages/ConfirmPage';
14 | import ShippingPage from './pages/ShippingPage';
15 | import PaymentPage from './pages/PaymentPage';
16 | import PlaceOrderPage from './pages/PlaceOrderPage';
17 | import OrderPage from './pages/OrderPage';
18 | import PasswordResetPage from './pages/PasswordResetPage';
19 | import UserListPage from './pages/UserListPage';
20 | import UserEditPage from './pages/UserEditPage';
21 | import ProductListPage from './pages/ProductListPage';
22 | import ProductEditPage from './pages/ProductEditPage';
23 | import OrderListPage from './pages/OrderListPage';
24 | import ErrorPage from './pages/ErrorPage';
25 |
26 | // for showing the 'new update available' banner and to register the service worker
27 | import ServiceWorkerWrapper from './ServiceWorkerWrapper';
28 |
29 | const App = () => {
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
49 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
68 |
69 |
70 |
71 |
72 |
77 |
82 |
86 |
91 |
96 |
100 |
105 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 | };
118 |
119 | export default App;
120 |
--------------------------------------------------------------------------------
/frontend/src/reducers/productReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | PRODUCT_LIST_REQUEST,
3 | PRODUCT_LIST_SUCCESS,
4 | PRODUCT_LIST_FAILURE,
5 | PRODUCT_DETAILS_FAILURE,
6 | PRODUCT_DETAILS_REQUEST,
7 | PRODUCT_DETAILS_SUCCESS,
8 | PRODUCT_DELETE_FAILURE,
9 | PRODUCT_DELETE_REQUEST,
10 | PRODUCT_DELETE_SUCCESS,
11 | PRODUCT_CREATE_SUCCESS,
12 | PRODUCT_CREATE_FAILURE,
13 | PRODUCT_CREATE_REQUEST,
14 | PRODUCT_CREATE_RESET,
15 | PRODUCT_UPDATE_SUCCESS,
16 | PRODUCT_UPDATE_FAILURE,
17 | PRODUCT_UPDATE_REQUEST,
18 | PRODUCT_UPDATE_RESET,
19 | PRODUCT_CREATE_REVIEW_SUCCESS,
20 | PRODUCT_CREATE_REVIEW_FAILURE,
21 | PRODUCT_CREATE_REVIEW_REQUEST,
22 | PRODUCT_CREATE_REVIEW_RESET,
23 | PRODUCT_TOP_RATED_SUCCESS,
24 | PRODUCT_TOP_RATED_FAILURE,
25 | PRODUCT_TOP_RATED_REQUEST,
26 | } from '../constants/productConstants';
27 |
28 | // list products based on keyword and paginated page number
29 | export const productListReducer = (state = { products: [] }, action) => {
30 | switch (action.type) {
31 | case PRODUCT_LIST_REQUEST:
32 | return { loading: true, products: [] };
33 |
34 | case PRODUCT_LIST_SUCCESS:
35 | return {
36 | loading: false,
37 | products: action.payload.products,
38 | page: action.payload.page,
39 | pages: action.payload.pages,
40 | };
41 |
42 | case PRODUCT_LIST_FAILURE:
43 | return { loading: false, error: action.payload };
44 |
45 | default:
46 | return { state };
47 | }
48 | };
49 |
50 | // details about a particular product
51 | export const productDetailsReducer = (
52 | state = { product: { reviews: [] } },
53 | action
54 | ) => {
55 | switch (action.type) {
56 | case PRODUCT_DETAILS_REQUEST:
57 | return { loading: true, ...state };
58 | case PRODUCT_DETAILS_SUCCESS:
59 | return { loading: false, product: action.payload };
60 | case PRODUCT_DETAILS_FAILURE:
61 | return { loading: false, error: action.payload };
62 | default:
63 | return { ...state };
64 | }
65 | };
66 |
67 | export const productDeleteReducer = (state = {}, action) => {
68 | switch (action.type) {
69 | case PRODUCT_DELETE_REQUEST:
70 | return { loading: true };
71 | case PRODUCT_DELETE_SUCCESS:
72 | return { loading: false, success: true };
73 | case PRODUCT_DELETE_FAILURE:
74 | return { loading: false, error: action.payload };
75 | default:
76 | return { ...state };
77 | }
78 | };
79 |
80 | export const productCreateReducer = (state = {}, action) => {
81 | switch (action.type) {
82 | case PRODUCT_CREATE_REQUEST:
83 | return { loading: true };
84 | case PRODUCT_CREATE_SUCCESS:
85 | return { loading: false, success: true, product: action.payload };
86 | case PRODUCT_CREATE_FAILURE:
87 | return { loading: false, error: action.payload };
88 | case PRODUCT_CREATE_RESET:
89 | return {};
90 | default:
91 | return { ...state };
92 | }
93 | };
94 |
95 | export const productCreateReviewReducer = (state = {}, action) => {
96 | switch (action.type) {
97 | case PRODUCT_CREATE_REVIEW_REQUEST:
98 | return { loading: true };
99 | case PRODUCT_CREATE_REVIEW_SUCCESS:
100 | return { loading: false, success: true };
101 | case PRODUCT_CREATE_REVIEW_FAILURE:
102 | return { loading: false, error: action.payload };
103 | case PRODUCT_CREATE_REVIEW_RESET:
104 | return {};
105 | default:
106 | return { ...state };
107 | }
108 | };
109 |
110 | export const productUpdateReducer = (state = { product: {} }, action) => {
111 | switch (action.type) {
112 | case PRODUCT_UPDATE_REQUEST:
113 | return { loading: true };
114 | case PRODUCT_UPDATE_SUCCESS:
115 | return { loading: false, success: true, product: action.payload };
116 | case PRODUCT_UPDATE_FAILURE:
117 | return { loading: false, error: action.payload };
118 | case PRODUCT_UPDATE_RESET:
119 | return { product: {} };
120 | default:
121 | return { ...state };
122 | }
123 | };
124 |
125 | // fetching top rated products
126 | export const productTopRatedReducer = (state = { products: [] }, action) => {
127 | switch (action.type) {
128 | case PRODUCT_TOP_RATED_REQUEST:
129 | return { loading: true, products: [] };
130 | case PRODUCT_TOP_RATED_SUCCESS:
131 | return { loading: false, products: action.payload };
132 | case PRODUCT_TOP_RATED_FAILURE:
133 | return { loading: false, error: action.payload };
134 | default:
135 | return { ...state };
136 | }
137 | };
138 |
--------------------------------------------------------------------------------
/backend/routes/authRoutes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {
3 | passportLoginSuccess,
4 | passportLoginFailure,
5 | } from '../controllers/authControllers.js';
6 | import passport from 'passport';
7 |
8 | const router = express.Router();
9 |
10 | // @desc login user using the google strategy
11 | // @route GET /api/auth/google
12 | // @access PUBLIC
13 | router.route('/google').get(
14 | passport.authenticate('google', {
15 | scope: ['profile', 'email'],
16 | })
17 | );
18 |
19 | // @desc redirect route for the passport google strategy
20 | // @route GET /api/auth/google/redirect
21 | // @access PUBLIC
22 | router.route('/google/redirect').get(
23 | passport.authenticate('google', {
24 | successRedirect: '/api/auth/google/redirect/success',
25 | failureRedirect: '/api/auth/google/redirect/failure',
26 | failureFlash: true,
27 | })
28 | );
29 |
30 | // @desc redirect route for the passport google strategy
31 | // @route GET /api/auth/google/redirect
32 | // @access PUBLIC
33 | router.route('/google/redirect/success').get(passportLoginSuccess);
34 |
35 | // @desc redirect route for the passport google strategy
36 | // @route GET /api/auth/google/redirect
37 | // @access PUBLIC
38 | router.route('/google/redirect/failure').get(passportLoginFailure);
39 |
40 | // @desc login user using the github strategy
41 | // @route GET /api/auth/github
42 | // @access PUBLIC
43 | router.route('/github').get(
44 | // googleLogin,
45 | passport.authenticate('github', {
46 | scope: ['user:email'],
47 | })
48 | );
49 |
50 | // @desc redirect route for the passport github strategy
51 | // @route GET /api/auth/github/redirect
52 | // @access PUBLIC
53 | router.route('/github/redirect').get(
54 | passport.authenticate('github', {
55 | successRedirect: '/api/auth/github/redirect/success',
56 | failureRedirect: '/api/auth/github/redirect/failure',
57 | failureFlash: true,
58 | })
59 | );
60 |
61 | // @desc redirect route for the passport github strategy
62 | // @route GET /api/auth/github/redirect
63 | // @access PUBLIC
64 | router.route('/github/redirect/success').get(passportLoginSuccess);
65 |
66 | // @desc redirect route for the passport github strategy
67 | // @route GET /api/auth/github/redirect
68 | // @access PUBLIC
69 | router.route('/github/redirect/failure').get(passportLoginFailure);
70 |
71 | // @desc redirect route for the passport twitter strategy
72 | // @route GET /api/auth/twitter
73 | // @access PUBLIC
74 | router.route('/twitter').get(passport.authenticate('twitter'));
75 |
76 | // @desc redirect route for the passport twitter strategy
77 | // @route GET /api/auth/twitter/redirect
78 | // @access PUBLIC
79 | router.route('/twitter/redirect').get(
80 | passport.authenticate('twitter', {
81 | successRedirect: '/api/auth/twitter/redirect/success',
82 | failureRedirect: '/api/auth/twitter/redirect/failure',
83 | failureFlash: true,
84 | })
85 | );
86 |
87 | // @desc redirect route for the passport twitter strategy
88 | // @route GET /api/auth/twitter/redirect
89 | // @access PUBLIC
90 | router.route('/twitter/redirect/success').get(passportLoginSuccess);
91 |
92 | // @desc redirect route for the passport twitter strategy
93 | // @route GET /api/auth/twitter/redirect
94 | // @access PUBLIC
95 | router.route('/twitter/redirect/failure').get(passportLoginFailure);
96 |
97 | // @desc redirect route for the passport linkedin strategy
98 | // @route GET /api/auth/linkedin/
99 | // @access PUBLIC
100 | router.route('/linkedin').get(passport.authenticate('linkedin'));
101 |
102 | // @desc redirect route for the passport linkedin strategy
103 | // @route GET /api/auth/linkedin/redirect
104 | // @access PUBLIC
105 | router.route('/linkedin/redirect').get(
106 | passport.authenticate('linkedin', {
107 | successRedirect: '/api/auth/linkedin/redirect/success',
108 | failureRedirect: '/api/auth/linkedin/redirect/failure',
109 | failureFlash: true,
110 | })
111 | );
112 |
113 | // @desc redirect route for the passport linkedin strategy
114 | // @route GET /api/auth/linkedin/redirect
115 | // @access PUBLIC
116 | router.route('/linkedin/redirect/success').get(passportLoginSuccess);
117 |
118 | // @desc redirect route for the passport linkedin strategy
119 | // @route GET /api/auth/linkedin/redirect
120 | // @access PUBLIC
121 | router.route('/linkedin/redirect/failure').get(passportLoginFailure);
122 |
123 | export default router;
124 |
--------------------------------------------------------------------------------
/frontend/src/reducers/orderReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ORDER_CREATE_REQUEST,
3 | ORDER_CREATE_SUCCESS,
4 | ORDER_CREATE_FAILURE,
5 | ORDER_CREATE_RESET,
6 | ORDER_DETAILS_REQUEST,
7 | ORDER_DETAILS_SUCCESS,
8 | ORDER_DETAILS_FAILURE,
9 | ORDER_PAY_REQUEST,
10 | ORDER_PAY_SUCCESS,
11 | ORDER_PAY_FAILURE,
12 | ORDER_PAY_RESET,
13 | ORDER_DELIVER_REQUEST,
14 | ORDER_DELIVER_SUCCESS,
15 | ORDER_DELIVER_FAILURE,
16 | ORDER_DELIVER_RESET,
17 | ORDER_USER_LIST_REQUEST,
18 | ORDER_USER_LIST_SUCCESS,
19 | ORDER_USER_LIST_FAILURE,
20 | ORDER_USER_LIST_RESET,
21 | ORDER_ALL_LIST_REQUEST,
22 | ORDER_ALL_LIST_SUCCESS,
23 | ORDER_ALL_LIST_FAILURE,
24 | } from '../constants/orderConstants';
25 |
26 | // create an order
27 | export const orderCreateReducer = (state = {}, action) => {
28 | switch (action.type) {
29 | case ORDER_CREATE_REQUEST:
30 | return {
31 | loading: true,
32 | };
33 | case ORDER_CREATE_SUCCESS:
34 | return {
35 | loading: false,
36 | success: true,
37 | order: action.payload,
38 | };
39 | case ORDER_CREATE_FAILURE:
40 | return {
41 | loading: false,
42 | error: action.payload,
43 | };
44 | case ORDER_CREATE_RESET:
45 | return {};
46 | default:
47 | return { ...state };
48 | }
49 | };
50 |
51 | // get order details
52 | export const orderDetailsReducer = (
53 | state = { loading: true, orderItems: [], shippingAddress: {} },
54 | action
55 | ) => {
56 | switch (action.type) {
57 | case ORDER_DETAILS_REQUEST:
58 | return {
59 | ...state,
60 | loading: true,
61 | };
62 | case ORDER_DETAILS_SUCCESS:
63 | return {
64 | loading: false,
65 | order: action.payload,
66 | };
67 | case ORDER_DETAILS_FAILURE:
68 | return {
69 | loading: false,
70 | error: action.payload,
71 | };
72 | default:
73 | return { ...state };
74 | }
75 | };
76 |
77 | // update order payment options
78 | export const orderPayReducer = (state = {}, action) => {
79 | switch (action.type) {
80 | case ORDER_PAY_REQUEST:
81 | return {
82 | ...state,
83 | loading: true,
84 | };
85 | case ORDER_PAY_SUCCESS:
86 | return {
87 | loading: false,
88 | success: true,
89 | };
90 | case ORDER_PAY_FAILURE:
91 | return {
92 | loading: false,
93 | error: action.payload,
94 | };
95 | case ORDER_PAY_RESET:
96 | return {};
97 | default:
98 | return { ...state };
99 | }
100 | };
101 |
102 | // update order to be delivered or not
103 | export const orderDeliverReducer = (state = {}, action) => {
104 | switch (action.type) {
105 | case ORDER_DELIVER_REQUEST:
106 | return {
107 | ...state,
108 | loading: true,
109 | };
110 | case ORDER_DELIVER_SUCCESS:
111 | return {
112 | loading: false,
113 | success: true,
114 | };
115 | case ORDER_DELIVER_FAILURE:
116 | return {
117 | loading: false,
118 | error: action.payload,
119 | };
120 | case ORDER_DELIVER_RESET:
121 | return {};
122 | default:
123 | return { ...state };
124 | }
125 | };
126 |
127 | // reducer to list orders of the particular user
128 | export const orderListUserReducer = (state = { orders: [] }, action) => {
129 | switch (action.type) {
130 | case ORDER_USER_LIST_REQUEST:
131 | return {
132 | ...state,
133 | loading: true,
134 | };
135 | case ORDER_USER_LIST_SUCCESS:
136 | return {
137 | loading: false,
138 | orders: action.payload,
139 | };
140 | case ORDER_USER_LIST_FAILURE:
141 | return {
142 | loading: false,
143 | error: action.payload,
144 | };
145 | case ORDER_USER_LIST_RESET:
146 | return { orders: [] };
147 | default:
148 | return { ...state };
149 | }
150 | };
151 |
152 | // reducer to list all orders for the admin panel view
153 | export const orderListAllReducer = (state = { orders: [] }, action) => {
154 | switch (action.type) {
155 | case ORDER_ALL_LIST_REQUEST:
156 | return {
157 | ...state,
158 | loading: true,
159 | };
160 | case ORDER_ALL_LIST_SUCCESS:
161 | return {
162 | loading: false,
163 | orders: action.payload.orders,
164 | page: action.payload.page,
165 | pages: action.payload.pages,
166 | total: action.payload.total,
167 | };
168 | case ORDER_ALL_LIST_FAILURE:
169 | return {
170 | loading: false,
171 | error: action.payload,
172 | };
173 |
174 | default:
175 | return { ...state };
176 | }
177 | };
178 |
--------------------------------------------------------------------------------
/frontend/src/pages/OrderListPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { LinkContainer } from 'react-router-bootstrap';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { Table, Button, Row, Col } from 'react-bootstrap';
5 | import Loader from '../components/Loader';
6 | import Message from '../components/Message';
7 | import Paginate from '../components/Paginate';
8 | import { refreshLogin } from '../actions/userActions';
9 | import { listAllOrders } from '../actions/orderActions';
10 | import getDateString from '../utils/getDateString';
11 |
12 | const ProductListPage = ({ history, match }) => {
13 | const pageNumber = match.params.pageNumber || 1; // to fetch various pages of orders
14 | const dispatch = useDispatch();
15 | const orderListAll = useSelector((state) => state.orderListAll); // to avoid blank screen display
16 | const { loading, orders, error, page, pages, total } = orderListAll;
17 |
18 | const userLogin = useSelector((state) => state.userLogin);
19 | const { userInfo } = userLogin;
20 |
21 | const userDetails = useSelector((state) => state.userDetails);
22 | const { error: userLoginError } = userDetails;
23 |
24 | // refresh access tokens aif user details are failed
25 | useEffect(() => {
26 | if (userLoginError && userInfo && !userInfo.isSocialLogin) {
27 | const user = JSON.parse(localStorage.getItem('userInfo'));
28 | user && dispatch(refreshLogin(user.email));
29 | }
30 | }, [userLoginError, dispatch, userInfo]);
31 |
32 | // get all orders by pagenumber
33 | useEffect(() => {
34 | if (userInfo && userInfo.isAdmin) dispatch(listAllOrders(pageNumber));
35 | else history.push('/login');
36 | }, [dispatch, history, userInfo, pageNumber]);
37 |
38 | return (
39 | <>
40 |
41 |
42 | All Orders ({`${total || 0}`})
43 |
44 |
45 | {loading ? (
46 |
47 | ) : error ? (
48 |
49 | {error}
50 |
51 | ) : (
52 |
57 |
58 |
59 | ID
60 | USER
61 | TOTAL
62 | DATE
63 | PAID
64 | DELIVERED
65 | ACTION
66 |
67 |
68 |
69 | {orders &&
70 | orders.map((order) => {
71 | return (
72 |
73 | {order._id}
74 | {order.user && order.user.name}
75 |
76 | {order.totalPrice.toLocaleString(
77 | 'en-IN',
78 | {
79 | maximumFractionDigits: 2,
80 | style: 'currency',
81 | currency: 'INR',
82 | }
83 | )}
84 |
85 |
86 | {getDateString(order.createdAt)}
87 |
88 |
89 | {order.isPaid ? (
90 | getDateString(order.paidAt)
91 | ) : (
92 |
98 | )}
99 |
100 |
101 | {order.isDelivered ? (
102 | getDateString(order.deliveredAt)
103 | ) : (
104 |
110 | )}
111 |
112 |
118 |
120 |
123 | View Details
124 |
125 |
126 |
127 |
128 | );
129 | })}
130 |
131 |
132 | )}
133 |
139 | >
140 | );
141 | };
142 |
143 | export default ProductListPage;
144 |
--------------------------------------------------------------------------------
/frontend/src/pages/UserListPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { LinkContainer } from 'react-router-bootstrap';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { Table, Button } from 'react-bootstrap';
5 | import Loader from '../components/Loader';
6 | import Message from '../components/Message';
7 | import { listAllUsers, deleteUser, refreshLogin } from '../actions/userActions';
8 | import Paginate from '../components/Paginate';
9 |
10 | const UserListPage = ({ match, history }) => {
11 | const pageNumber = match.params.pageNumber || 1; // to fetch various pages of orders
12 | const dispatch = useDispatch();
13 | const userList = useSelector((state) => state.userList);
14 | const { loading, users, error, page, pages, total } = userList;
15 |
16 | const userLogin = useSelector((state) => state.userLogin);
17 | const { userInfo } = userLogin;
18 |
19 | const userDelete = useSelector((state) => state.userDelete);
20 | const { success: successDelete } = userDelete;
21 |
22 | const userDetails = useSelector((state) => state.userDetails);
23 | const { error: userLoginError } = userDetails;
24 |
25 | useEffect(() => {
26 | if (userLoginError && userInfo && !userInfo.isSocialLogin) {
27 | const user = JSON.parse(localStorage.getItem('userInfo'));
28 | user && dispatch(refreshLogin(user.email));
29 | }
30 | }, [userLoginError, dispatch, userInfo]);
31 |
32 | useEffect(() => {
33 | if (userInfo && userInfo.isAdmin) dispatch(listAllUsers(pageNumber));
34 | else history.push('/login');
35 | }, [dispatch, history, userInfo, successDelete, pageNumber]);
36 |
37 | const handleDelete = (id) => {
38 | if (window.confirm('Are you sure you wanna delete user?'))
39 | dispatch(deleteUser(id));
40 | };
41 | return (
42 | <>
43 | Users ({`${total || 0}`})
44 | {loading ? (
45 |
46 | ) : error ? (
47 |
48 | {error}
49 |
50 | ) : (
51 |
56 |
57 |
58 | ID
59 | NAME
60 | EMAIL
61 | CONFIRMED
62 | ADMIN
63 | ACTION
64 |
65 |
66 |
67 | {users &&
68 | users.map((user) => {
69 | return (
70 |
71 | {user._id}
72 | {user.name}
73 |
74 |
75 | {user.email}
76 |
77 |
78 |
79 | {user.isConfirmed ? (
80 |
84 | ) : (
85 |
89 | )}
90 |
91 |
92 | {user.isAdmin ? (
93 |
97 | ) : (
98 |
102 | )}
103 |
104 |
105 |
111 |
113 |
116 |
117 |
118 |
119 |
123 | handleDelete(user._id)
124 | }>
125 |
126 |
127 |
128 |
129 | );
130 | })}
131 |
132 |
133 | )}
134 |
140 | >
141 | );
142 | };
143 |
144 | export default UserListPage;
145 |
--------------------------------------------------------------------------------
/frontend/src/pages/ShippingPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { Form, Button, FloatingLabel } from 'react-bootstrap';
4 | import FormContainer from '../components/FormContainer';
5 | import CheckoutStatus from '../components/CheckoutStatus';
6 | import { saveShippingAddress } from '../actions/cartActions';
7 | import { refreshLogin, getUserDetails } from '../actions/userActions';
8 |
9 | const ShippingPage = ({ history }) => {
10 | const dispatch = useDispatch();
11 |
12 | const cart = useSelector((state) => state.cart);
13 | const { cartItems, shippingAddress } = cart;
14 |
15 | const userLogin = useSelector((state) => state.userLogin);
16 | const { userInfo } = userLogin;
17 |
18 | const userDetails = useSelector((state) => state.userDetails);
19 | const { error } = userDetails;
20 |
21 | const [address, setAddress] = useState(shippingAddress.address);
22 | const [city, setCity] = useState(shippingAddress.city);
23 | const [postalCode, setPostalCode] = useState(shippingAddress.postalCode);
24 | const [country, setCountry] = useState(shippingAddress.country);
25 |
26 | // fetch user details from the redux store
27 | useEffect(() => {
28 | userInfo
29 | ? userInfo.isSocialLogin
30 | ? dispatch(getUserDetails(userInfo.id))
31 | : dispatch(getUserDetails('profile'))
32 | : dispatch(getUserDetails('profile'));
33 | }, [userInfo, dispatch]);
34 |
35 | // update access token to a new ine using the refresh tokens
36 | useEffect(() => {
37 | if (error && userInfo && !userInfo.isSocialLogin) {
38 | const user = JSON.parse(localStorage.getItem('userInfo'));
39 | user && dispatch(refreshLogin(user.email));
40 | }
41 | }, [error, dispatch, userInfo]);
42 |
43 | useEffect(() => {
44 | if (!(cartItems.length && userInfo)) {
45 | history.push('/');
46 | }
47 | }, [cartItems, history, userInfo]);
48 |
49 | // save shipping address and move to payment screen
50 | const handleSubmit = (e) => {
51 | e.preventDefault();
52 | dispatch(
53 | saveShippingAddress({
54 | address,
55 | city,
56 | postalCode,
57 | country,
58 | })
59 | );
60 | history.push('/payment');
61 | };
62 |
63 | return (
64 |
65 |
66 | Shipping Address
67 |
69 |
73 | setAddress(e.target.value)}
80 | />
81 |
82 |
83 |
84 |
88 | setCity(e.target.value)}
95 | />
96 |
97 |
98 |
99 |
103 | setPostalCode(e.target.value)}
110 | />
111 |
112 |
113 |
114 |
118 | setCountry(e.target.value)}
125 | />
126 |
127 |
128 |
129 |
136 | Continue
137 |
138 |
139 |
140 |
141 | );
142 | };
143 |
144 | export default ShippingPage;
145 |
--------------------------------------------------------------------------------
/frontend/src/pages/UserEditPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Form, Button, InputGroup } from 'react-bootstrap';
4 | import { Link } from 'react-router-dom';
5 | import Loader from '../components/Loader';
6 | import Message from '../components/Message';
7 | import Meta from '../components/Meta';
8 | import FormContainer from '../components/FormContainer';
9 | import {
10 | getUserDetails,
11 | updateUser,
12 | refreshLogin,
13 | } from '../actions/userActions';
14 | import { USER_UPDATE_RESET } from '../constants/userConstants';
15 |
16 | const UserEditPage = ({ match, history }) => {
17 | const userId = match.params.id;
18 | const [name, setName] = useState('');
19 | const [email, setEmail] = useState('');
20 | const [isAdmin, setIsAdmin] = useState(false);
21 | const dispatch = useDispatch();
22 |
23 | const userDetails = useSelector((state) => state.userDetails);
24 | const { loading, user, error } = userDetails;
25 |
26 | const userUpdate = useSelector((state) => state.userUpdate);
27 | const {
28 | loading: loadingUpdate,
29 | error: errorUpdate,
30 | success: successUpdate,
31 | } = userUpdate;
32 |
33 | const userLogin = useSelector((state) => state.userLogin);
34 | const { userInfo } = userLogin;
35 |
36 | // get new access tokens
37 | useEffect(() => {
38 | if (error && userInfo && !userInfo.isSocialLogin) {
39 | const user = JSON.parse(localStorage.getItem('userInfo'));
40 | user && dispatch(refreshLogin(user.email));
41 | }
42 | }, [error, dispatch, userInfo]);
43 |
44 | // update user details from the admin panel view
45 | useEffect(() => {
46 | if (successUpdate) {
47 | dispatch({ type: USER_UPDATE_RESET });
48 | history.push('/admin/userlist');
49 | } else {
50 | if (!user || !user.name || user._id !== userId) {
51 | dispatch(getUserDetails(userId));
52 | } else {
53 | setName(user.name);
54 | setEmail(user.email);
55 | setIsAdmin(user.isAdmin);
56 | }
57 | }
58 | }, [user, dispatch, userId, successUpdate, history]);
59 |
60 | const handleSubmit = (e) => {
61 | e.preventDefault();
62 | dispatch(updateUser({ _id: userId, name, email, isAdmin }));
63 | };
64 |
65 | return (
66 | <>
67 |
68 |
69 | Go Back
70 |
71 |
72 |
73 | {user && }
74 | Edit User
75 | {loadingUpdate ? (
76 |
77 | ) : errorUpdate ? (
78 |
79 | {errorUpdate}
80 |
81 | ) : (
82 | <>
83 | {loading ? (
84 |
85 | ) : (
86 |
97 | Name
98 |
104 | setName(e.target.value)
105 | }
106 | />
107 |
108 |
109 | Email Address
110 |
116 | setEmail(e.target.value)
117 | }
118 | />
119 |
120 |
123 |
124 |
131 | setIsAdmin(e.target.checked)
132 | }>
133 |
134 |
135 |
139 | Edit
140 |
141 |
142 | )}
143 | >
144 | )}
145 |
146 | >
147 | );
148 | };
149 |
150 | export default UserEditPage;
151 |
--------------------------------------------------------------------------------
/frontend/src/serviceWorkerRegistration.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://cra.link/PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://cra.link/PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then((registration) => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://cra.link/PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch((error) => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then((response) => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null &&
112 | contentType.indexOf('javascript') === -1)
113 | ) {
114 | // No service worker found. Probably a different app. Reload the page.
115 | navigator.serviceWorker.ready.then((registration) => {
116 | registration.unregister().then(() => {
117 | window.location.reload();
118 | });
119 | });
120 | } else {
121 | // Service worker found. Proceed as normal.
122 | registerValidSW(swUrl, config);
123 | }
124 | })
125 | .catch(() => {
126 | console.log(
127 | 'No internet connection found. App is running in offline mode.'
128 | );
129 | });
130 | }
131 |
132 | export function unregister() {
133 | if ('serviceWorker' in navigator) {
134 | navigator.serviceWorker.ready
135 | .then((registration) => {
136 | registration.unregister();
137 | })
138 | .catch((error) => {
139 | console.error(error.message);
140 | });
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/frontend/src/pages/PasswordResetPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Form, Button, InputGroup, FloatingLabel } from 'react-bootstrap';
4 | import Loader from '../components/Loader';
5 | import Message from '../components/Message';
6 | import FormContainer from '../components/FormContainer';
7 | import { resetUserPassword } from '../actions/userActions';
8 |
9 | const PasswordResetPage = ({ match, history }) => {
10 | const [name, setName] = useState('');
11 | const [typePassword, setTypePassword] = useState('password');
12 | const [typeConfirmPassword, setTypeConfirmPassword] = useState('password');
13 | const [message, setMessage] = useState(null);
14 | const [confirmPassword, setConfirmPassword] = useState('');
15 | const [password, setPassword] = useState('');
16 | const dispatch = useDispatch();
17 | const userResetPassword = useSelector((state) => state.userResetPassword);
18 | const { loading, resetPassword, error } = userResetPassword;
19 |
20 | // get the name stored in the local storage and ask that user to reset password
21 | useEffect(() => {
22 | const nameFromLocalStorage = localStorage.getItem('EcommerceUserName');
23 | if (nameFromLocalStorage) {
24 | setName(nameFromLocalStorage);
25 | }
26 | }, []);
27 |
28 | useEffect(() => {
29 | if (resetPassword) {
30 | setTimeout(() => {
31 | history.push('/login');
32 | }, 10000);
33 | }
34 | }, [history, resetPassword]);
35 |
36 | const showHidePassword = (e) => {
37 | e.preventDefault();
38 | e.stopPropagation();
39 | setTypePassword(typePassword === 'password' ? 'text' : 'password');
40 | };
41 | const showHideConfirmPassword = (e) => {
42 | e.preventDefault();
43 | e.stopPropagation();
44 | setTypeConfirmPassword(
45 | typeConfirmPassword === 'password' ? 'text' : 'password'
46 | );
47 | };
48 |
49 | const handleSubmit = (e) => {
50 | e.preventDefault();
51 | if (password !== confirmPassword) {
52 | setMessage('Passwords do not match. Please retry.');
53 | } else {
54 | dispatch(resetUserPassword(match.params.token, password));
55 | }
56 | };
57 |
58 | return (
59 |
60 | {name ? `${name}, reset password` : 'Reset Password'}
61 | {message && (
62 |
63 | {message}
64 |
65 | )}
66 | {resetPassword && (
67 |
68 | Password Changed Successfully.
69 |
70 | )}
71 | {error && (
72 |
73 | {error}
74 |
75 | )}
76 | {loading ? (
77 |
78 | ) : (
79 |
81 |
82 |
86 |
96 | setPassword(e.target.value)
97 | }
98 | />
99 |
100 |
101 |
111 | {typePassword === 'text' ? (
112 |
113 | ) : (
114 |
115 | )}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
126 |
136 | setConfirmPassword(e.target.value)
137 | }
138 | />
139 |
140 |
141 |
151 | {typeConfirmPassword === 'text' ? (
152 |
153 | ) : (
154 |
155 | )}
156 |
157 |
158 |
159 |
160 |
166 | Submit
167 |
168 |
169 | )}
170 |
171 | );
172 | };
173 |
174 | export default PasswordResetPage;
175 |
--------------------------------------------------------------------------------
/frontend/src/pages/HomePage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import Product from '../components/Product';
5 | import Paginate from '../components/Paginate';
6 | import { Row, Col } from 'react-bootstrap';
7 | import ProductCarousel from '../components/ProductCarousel';
8 | import Meta from '../components/Meta';
9 | import { listProducts } from '../actions/productActions';
10 | import { refreshLogin, getUserDetails } from '../actions/userActions';
11 | import Message from '../components/Message';
12 | import SearchBox from '../components/SearchBox';
13 | import ProductSkeleton from '../components/ProductSkeleton';
14 |
15 | const HomePage = ({ match, history }) => {
16 | const keyword = match.params.keyword; // to search for products
17 | const pageNumber = Number(match.params.pageNumber) || 1; // current page number in the paginated display
18 | const [promptVerfication, setPromptVerification] = useState(false); // prompt user to verify email if not yet confirmed
19 | const [products, setProducts] = useState(null);
20 | const [productAvailable, setProductAvailable] = useState(false);
21 | const dispatch = useDispatch();
22 |
23 | // get the products list, userinfo and user details form the redix store
24 | const productList = useSelector((state) => state.productList);
25 | let { loading, error, pages } = productList;
26 |
27 | const userLogin = useSelector((state) => state.userLogin);
28 | const { userInfo } = userLogin;
29 |
30 | const userDetails = useSelector((state) => state.userDetails);
31 | const { error: userInfoError } = userDetails;
32 |
33 | // fetch the user details
34 | useEffect(() => {
35 | userInfo
36 | ? userInfo.isSocialLogin
37 | ? dispatch(getUserDetails(userInfo.id))
38 | : dispatch(getUserDetails('profile'))
39 | : dispatch(getUserDetails('profile'));
40 | }, [userInfo, dispatch]);
41 |
42 | // refresh token to get new access token if error in user details
43 | useEffect(() => {
44 | if (userInfoError && userInfo && !userInfo.isSocialLogin) {
45 | const user = JSON.parse(localStorage.getItem('userInfo'));
46 | dispatch(refreshLogin(user?.email));
47 | }
48 | }, [userInfoError, dispatch, userInfo]);
49 |
50 | // set a state variable to true or false depending on if products is avialable in the state
51 | useEffect(() => {
52 | if (products) {
53 | products.length
54 | ? setProductAvailable(true)
55 | : setProductAvailable(false);
56 | }
57 | }, [products]);
58 |
59 | // fetch products from redux store into local state
60 | useEffect(() => {
61 | if (productList) {
62 | if (productList.products) setProducts([...productList.products]);
63 | }
64 | }, [productList]);
65 |
66 | // list products based on keyword and pagenumber
67 | useEffect(() => {
68 | dispatch(listProducts(keyword, pageNumber));
69 | // eslint-disable-next-line react-hooks/exhaustive-deps
70 | }, [dispatch, keyword, pageNumber]);
71 |
72 | // check if user needs to be promted about email verification on page load
73 | useEffect(() => {
74 | setPromptVerification(
75 | localStorage.getItem('promptEmailVerfication') === 'true'
76 | ? true
77 | : false
78 | );
79 | }, []);
80 |
81 | return (
82 | <>
83 |
84 | {/* display carousel only on larger screens */}
85 | {!keyword ? (
86 | window.innerWidth > 430 &&
87 | ) : (
88 |
91 | Go Back
92 |
93 | )}
94 | {/* display this search bar on home page on mobile screens */}
95 |
96 |
97 |
98 |
99 | {/* if the user needs to be prompted about email verification, show this message */}
100 | {promptVerfication ? (
101 |
102 | Account Created! Please check your email to verify your
103 | account and start shopping.
104 |
105 | ) : null}
106 |
107 | {error ? (
108 |
109 | {error}
110 |
111 | ) : !loading && products ? (
112 | <>
113 |
114 | {products.length
115 | ? products.map((product) => {
116 | return (
117 |
123 |
124 |
125 | );
126 | })
127 | : keyword &&
128 | !productAvailable && (
129 | // show this only if user has searched for some item and it is not available
130 |
131 |
132 | No
133 | items found for this search query
134 |
135 | Go Back to the{' '}
136 | Home Page
137 |
138 | )}
139 |
140 |
146 | >
147 | ) : (
148 | loading &&
149 | products &&
150 | products.length === 0 && (
151 |
152 | {[1, 2, 3, 4].map((ele) => {
153 | return (
154 |
155 |
158 |
159 | );
160 | })}
161 |
162 | )
163 | )}
164 | >
165 | );
166 | };
167 |
168 | export default HomePage;
169 |
--------------------------------------------------------------------------------
/backend/controllers/productControllers.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler';
2 | import Product from '../models/productModel.js';
3 |
4 | // @desc fetch all the products
5 | // @route GET /api/products
6 | // @access PUBLIC
7 | const getAllProducts = asyncHandler(async (req, res) => {
8 | const page = Number(req.query.pageNumber) || 1; // the current page number being fetched
9 | const pageSize = Number(req.query.pageSize) || 10; // the total number of entries on a single page
10 |
11 | // match all products which include the string of chars in the keyword, not necessarily in the given order
12 | const keyword = req.query.keyword
13 | ? {
14 | name: {
15 | $regex: req.query.keyword,
16 | $options: 'si',
17 | },
18 | }
19 | : {};
20 | const count = await Product.countDocuments({ ...keyword }); // total number of products which match with the given key
21 |
22 | // find all products that need to be sent for the current page, by skipping the documents included in the previous pages
23 | // and limiting the number of documents included in this request
24 | const products = await Product.find({ ...keyword })
25 | .limit(pageSize)
26 | .skip(pageSize * (page - 1));
27 |
28 | // send the list of products, current page number, total number of pages available
29 | res.json({ products, page, pages: Math.ceil(count / pageSize) });
30 | });
31 |
32 | // @desc Fetch a single product by id
33 | // @route GET /api/products/:id
34 | // @access PUBLIC
35 | const getProductById = asyncHandler(async (req, res) => {
36 | const product = await Product.findById(req.params.id);
37 | if (product) res.json(product);
38 | else {
39 | // throw a custom error so that our error middleware can catch them and return apt json
40 | res.status(404);
41 | throw new Error('Product not found');
42 | }
43 | });
44 |
45 | // @desc Delete a product
46 | // @route DELETE /api/products/:id
47 | // @access PRIVATE/ADMIN
48 | const deleteProduct = asyncHandler(async (req, res) => {
49 | const product = await Product.findById(req.params.id);
50 | if (product) {
51 | await product.remove();
52 | res.json({ message: 'Product removed from DB' });
53 | } else {
54 | // throw a custom error so that our error middleware can catch them and return apt json
55 | res.status(404);
56 | throw new Error('Product not found');
57 | }
58 | });
59 |
60 | // @desc Create a product
61 | // @route POST /api/products/
62 | // @access PRIVATE/ADMIN
63 | const createProduct = asyncHandler(async (req, res) => {
64 | // create a dummy product which can be edited later
65 | const product = new Product({
66 | name: 'Sample',
67 | brand: 'Sample Brand',
68 | category: 'Sample Category',
69 | numReviews: 0,
70 | countInStock: 0,
71 | price: 0,
72 | user: req.user._id,
73 | image: '/images/alexa.jpg',
74 | description: 'Sample description',
75 | });
76 | const createdProduct = await product.save();
77 | res.status(201).json(createdProduct);
78 | });
79 |
80 | // @desc Update a product
81 | // @route PUT /api/products/:id
82 | // @access PRIVATE/ADMIN
83 | const updateProduct = asyncHandler(async (req, res) => {
84 | const {
85 | name,
86 | price,
87 | brand,
88 | category,
89 | numReviews,
90 | countInStock,
91 | description,
92 | image,
93 | } = req.body;
94 | const product = await Product.findById(req.params.id);
95 |
96 | // update the fields which are sent with the payload
97 | if (product) {
98 | if (name) product.name = name;
99 | if (price) product.price = price;
100 | if (brand) product.brand = brand;
101 | if (category) product.category = category;
102 | if (numReviews) product.numReviews = numReviews;
103 | if (countInStock) product.countInStock = countInStock;
104 | if (description) product.description = description;
105 | if (image) product.image = image;
106 |
107 | const updatedProduct = await product.save();
108 | if (updatedProduct) res.status(201).json(updatedProduct);
109 | } else {
110 | res.status(404);
111 | throw new Error('Product not available');
112 | }
113 | });
114 |
115 | // @desc Create a product review
116 | // @route POST /api/products/:id/reviews
117 | // @access PRIVATE
118 | const createProductReview = asyncHandler(async (req, res) => {
119 | const { rating, review } = req.body;
120 | const product = await Product.findById(req.params.id);
121 | if (product) {
122 | // If the user has already reviewed this product, throw an error
123 | const reviewedAlready = product.reviews.find(
124 | (rev) => rev.user.toString() === req.user._id.toString()
125 | );
126 | if (reviewedAlready) {
127 | res.status(400);
128 | throw new Error('Product Already Reviewed');
129 | }
130 |
131 | const newReview = {
132 | name: req.user.name,
133 | user: req.user._id,
134 | avatar: req.user.avatar,
135 | rating: Number(rating),
136 | review,
137 | };
138 |
139 | // store the new review and update the rating of this product
140 | product.reviews.push(newReview);
141 | product.numReviews = product.reviews.length;
142 | product.rating =
143 | product.reviews.reduce((acc, ele) => acc + ele.rating, 0) /
144 | product.numReviews;
145 | const updatedProduct = await product.save();
146 | if (updatedProduct) res.status(201).json({ message: 'Review Added' });
147 | } else {
148 | res.status(404);
149 | throw new Error('Product not available');
150 | }
151 | });
152 |
153 | // @desc fetch top rated products
154 | // @route GET /api/products/top
155 | // @access PUBLIC
156 | const getTopProducts = asyncHandler(async (req, res) => {
157 | // get top 4 rated products
158 | const topProducts = await Product.find({}).sort({ rating: -1 }).limit(4);
159 | res.json(topProducts);
160 | });
161 |
162 | export {
163 | getProductById,
164 | getAllProducts,
165 | deleteProduct,
166 | createProduct,
167 | updateProduct,
168 | createProductReview,
169 | getTopProducts,
170 | };
171 |
--------------------------------------------------------------------------------
/frontend/src/pages/ProductListPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { LinkContainer } from 'react-router-bootstrap';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { Table, Button, Row, Col } from 'react-bootstrap';
5 | import Loader from '../components/Loader';
6 | import Message from '../components/Message';
7 | import {
8 | listProducts,
9 | deleteProduct,
10 | createProduct,
11 | } from '../actions/productActions';
12 | import { PRODUCT_CREATE_RESET } from '../constants/productConstants';
13 | import Paginate from '../components/Paginate';
14 | import { refreshLogin, getUserDetails } from '../actions/userActions';
15 |
16 | const ProductListPage = ({ history, match }) => {
17 | const pageNumber = match.params.pageNumber || 1;
18 | const dispatch = useDispatch();
19 | const productList = useSelector((state) => state.productList);
20 | const { loading, products, error, pages, page } = productList;
21 |
22 | const productDelete = useSelector((state) => state.productDelete);
23 | const {
24 | loading: loadingDelete,
25 | success: successDelete,
26 | error: errorDelete,
27 | } = productDelete;
28 |
29 | const productCreate = useSelector((state) => state.productCreate);
30 | const {
31 | loading: loadingCreate,
32 | success: successCreate,
33 | error: errorCreate,
34 | product: createdProduct,
35 | } = productCreate;
36 |
37 | const userLogin = useSelector((state) => state.userLogin);
38 | const { userInfo } = userLogin;
39 |
40 | const userDetails = useSelector((state) => state.userDetails);
41 | const { error: userLoginError } = userDetails;
42 |
43 | // fetch user login info
44 | useEffect(() => {
45 | userInfo
46 | ? userInfo.isSocialLogin
47 | ? dispatch(getUserDetails(userInfo.id))
48 | : dispatch(getUserDetails('profile'))
49 | : dispatch(getUserDetails('profile'));
50 | }, [userInfo, dispatch]);
51 |
52 | // refresh token for expired access tokens
53 | useEffect(() => {
54 | if (userLoginError && userInfo && !userInfo.isSocialLogin) {
55 | const user = JSON.parse(localStorage.getItem('userInfo'));
56 | user && dispatch(refreshLogin(user.email));
57 | }
58 | }, [userLoginError, dispatch, userInfo]);
59 |
60 | useEffect(() => {
61 | if (!userInfo.isAdmin) history.push('/login');
62 | dispatch({ type: PRODUCT_CREATE_RESET }); //reset the new product detail
63 | if (successCreate)
64 | history.push(`/admin/product/${createdProduct._id}/edit`);
65 | else dispatch(listProducts('', pageNumber, 10)); // 3rd parameter is the no of products to be listed per page
66 | }, [
67 | dispatch,
68 | history,
69 | userInfo,
70 | successDelete,
71 | successCreate,
72 | createdProduct,
73 | pageNumber,
74 | ]);
75 |
76 | // delete product after confirming
77 | const handleDelete = (id) => {
78 | if (window.confirm('Are you sure you wanna delete this product?'))
79 | dispatch(deleteProduct(id));
80 | };
81 | // create a new dummy product
82 | const handleCreateProduct = () => {
83 | dispatch(createProduct());
84 | };
85 | return (
86 | <>
87 |
88 |
89 | Products
90 |
91 |
92 |
98 | Create Product
99 |
100 |
101 |
102 | {errorDelete && (
103 |
104 | {errorDelete}
105 |
106 | )}
107 | {errorCreate && (
108 |
109 | {errorCreate}
110 |
111 | )}
112 | {loading || loadingCreate || loadingDelete ? (
113 |
114 | ) : error ? (
115 |
116 | {error}
117 |
118 | ) : (
119 | <>
120 |
125 |
126 |
127 | ID
128 | NAME
129 | PRICE
130 | CATEGORY
131 | BRAND
132 | ACTION
133 |
134 |
135 |
136 | {products &&
137 | products.map((product) => {
138 | return (
139 |
140 | {product._id}
141 | {product.name}
142 |
143 | {product.price &&
144 | product.price.toLocaleString(
145 | 'en-IN',
146 | {
147 | maximumFractionDigits: 2,
148 | style: 'currency',
149 | currency: 'INR',
150 | }
151 | )}
152 |
153 | {product.category}
154 | {product.brand}
155 |
156 |
163 |
165 |
168 |
169 |
170 |
171 |
174 | handleDelete(
175 | product._id
176 | )
177 | }
178 | variant='danger'>
179 |
186 |
187 |
188 |
189 | );
190 | })}
191 |
192 |
193 |
194 | >
195 | )}
196 | >
197 | );
198 | };
199 |
200 | export default ProductListPage;
201 |
--------------------------------------------------------------------------------
/backend/controllers/orderControllers.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from 'express-async-handler';
2 | import Order from '../models/orderModel.js';
3 | import Stripe from 'stripe';
4 | import dotenv from 'dotenv';
5 | dotenv.config();
6 |
7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
8 |
9 | // @desc create a new order
10 | // @route GET /api/orders
11 | // @access PRIVATE
12 | const addOrderItems = asyncHandler(async (req, res) => {
13 | const {
14 | orderItems,
15 | shippingAddress,
16 | paymentMethod,
17 | itemsPrice,
18 | shippingPrice,
19 | taxPrice,
20 | totalPrice,
21 | } = req.body;
22 |
23 | if (orderItems && !orderItems.length) {
24 | res.status(401);
25 | throw new Error('No order items');
26 | } else {
27 | const order = new Order({
28 | user: req.user._id,
29 | orderItems,
30 | shippingAddress,
31 | paymentMethod,
32 | itemsPrice,
33 | shippingPrice,
34 | taxPrice,
35 | totalPrice,
36 | });
37 | const createdOrder = await order.save();
38 | res.status(201).json(createdOrder);
39 | }
40 | });
41 |
42 | // @desc get an order by id
43 | // @route GET /api/orders/:id
44 | // @access PRIVATE
45 | const getOrderById = asyncHandler(async (req, res) => {
46 | const reqOrder = await Order.findById(req.params.id).populate(
47 | 'user',
48 | 'name email'
49 | );
50 | if (reqOrder) {
51 | res.status(201).json(reqOrder);
52 | } else {
53 | res.status(401);
54 | throw new Error('Order not found');
55 | }
56 | });
57 |
58 | // @desc update the order object once paid
59 | // @route PUT /api/orders/:id/pay
60 | // @access PRIVATE
61 | const updateOrderToPay = asyncHandler(async (req, res) => {
62 | const order = await Order.findById(req.params.id);
63 | if (order) {
64 | const { paymentMode } = req.body;
65 | order.isPaid = true;
66 | order.paidAt = Date.now();
67 | // update the payment result based on which mode of payment was chosen
68 | if (paymentMode === 'paypal') {
69 | order.paymentResult = {
70 | type: 'paypal',
71 | id: req.body.id,
72 | status: req.body.status,
73 | update_time: req.body.update_time,
74 | email_address: req.body.payer.email_address,
75 | };
76 | } else if (paymentMode === 'stripe') {
77 | order.paymentResult = {
78 | type: 'stripe',
79 | id: req.body.id,
80 | status: req.body.status,
81 | email_address: req.body.receipt_email,
82 | };
83 | }
84 |
85 | const updatedOrder = await order.save();
86 | res.status(201).json(updatedOrder);
87 | } else {
88 | res.status(401);
89 | throw new Error('Order not found');
90 | }
91 | });
92 |
93 | // @desc update the order object once delivered
94 | // @route PUT /api/orders/:id/pay
95 | // @access PRIVATE/ADMIN
96 | const updateOrderToDeliver = asyncHandler(async (req, res) => {
97 | const order = await Order.findById(req.params.id);
98 | if (order) {
99 | order.isDelivered = true;
100 | order.deliveredAt = Date.now();
101 |
102 | const updatedOrder = await order.save();
103 | res.status(201).json(updatedOrder);
104 | } else {
105 | res.status(401);
106 | throw new Error('Order not found');
107 | }
108 | });
109 |
110 | // @desc fetch the orders of the user logged in
111 | // @route GET /api/orders/myorders
112 | // @access PRIVATE
113 | const getMyOrders = asyncHandler(async (req, res) => {
114 | // sort orders in descending order of the date they were created at, hence negetive sign
115 | const allOrders = await Order.find({ user: req.user._id }).sort(
116 | '-createdAt'
117 | );
118 | res.json(allOrders);
119 | });
120 |
121 | // @desc fetch all orders
122 | // @route GET /api/orders
123 | // @access PRIVATE/ADMIN
124 | const getAllOrders = asyncHandler(async (req, res) => {
125 | const page = Number(req.query.pageNumber) || 1; // the current page number in the pagination
126 | const pageSize = 20; // total number of entries on a single page
127 |
128 | const count = await Order.countDocuments({}); // total number of documents available
129 |
130 | // find all orders that need to be sent for the current page, by skipping the documents included in the previous pages
131 | // and limiting the number of documents included in this request
132 | // sort this in desc order that the document was created at
133 | const orders = await Order.find({})
134 | .limit(pageSize)
135 | .skip(pageSize * (page - 1))
136 | .populate('user', 'id name')
137 | .sort('-createdAt');
138 |
139 | // send the list of orders, current page number, total number of pages available
140 | res.json({
141 | orders,
142 | page,
143 | pages: Math.ceil(count / pageSize),
144 | total: count,
145 | });
146 | });
147 |
148 | // @desc create payment intent for stripe payment
149 | // @route POST /api/orders/stripe-payment
150 | // @access PUBLIC
151 | const stripePayment = asyncHandler(async (req, res) => {
152 | const { price, email } = req.body;
153 |
154 | // Need to create a payment intent according to stripe docs
155 | // https://stripe.com/docs/api/payment_intents
156 | const paymentIntent = await stripe.paymentIntents.create({
157 | amount: price,
158 | currency: 'inr',
159 | receipt_email: email,
160 | payment_method_types: ['card'],
161 | });
162 |
163 | // send this payment intent to the client side
164 | res.send({
165 | clientSecret: paymentIntent.client_secret,
166 | });
167 |
168 | // another way to include payments, is to create a new charge for a new customer, each time
169 | // similar to Hitesh's video on accepting stripe payments
170 | // But uses out dated stripe technique, so excluded for the current implementation
171 |
172 | // const { order, token } = req.body;
173 | // const idempotencyKey = nanoid();
174 | // return stripe.customers
175 | // .create({
176 | // email: token.email,
177 | // source: token.id,
178 | // })
179 | // .then((customer) => {
180 | // stripe.charges.create(
181 | // {
182 | // amount: order.totalPrice * 100,
183 | // currency: 'inr',
184 | // customer: customer.id,
185 | // receipt_email: token.email,
186 | // // description: product.name,
187 | // },
188 | // { idempotencyKey }
189 | // );
190 | // })
191 | // .then((result) => res.status(200).json(result))
192 | // .catch((err) => console.log(err));
193 | });
194 |
195 | export {
196 | addOrderItems,
197 | getOrderById,
198 | updateOrderToPay,
199 | updateOrderToDeliver,
200 | getMyOrders,
201 | getAllOrders,
202 | stripePayment,
203 | };
204 |
--------------------------------------------------------------------------------
/frontend/src/reducers/userReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | USER_LOGIN_FAILURE,
3 | USER_LOGIN_REQUEST,
4 | USER_LOGIN_SUCCESS,
5 | USER_LOGOUT,
6 | USER_REGISTER_FAILURE,
7 | USER_REGISTER_SUCCESS,
8 | USER_REGISTER_REQUEST,
9 | USER_RESET_PASSWORD_FAILURE,
10 | USER_RESET_PASSWORD_SUCCESS,
11 | USER_RESET_PASSWORD_REQUEST,
12 | USER_EMAIL_VERIFICATION_REQUEST,
13 | USER_EMAIL_VERIFICATION_SUCCESS,
14 | USER_EMAIL_VERIFICATION_FAILURE,
15 | USER_CONFIRM_REQUEST,
16 | USER_CONFIRM_SUCCESS,
17 | USER_CONFIRM_FAILURE,
18 | USER_DETAILS_FAILURE,
19 | USER_DETAILS_SUCCESS,
20 | USER_DETAILS_REQUEST,
21 | USER_DETAILS_RESET,
22 | USER_PROFILE_UPDATE_REQUEST,
23 | USER_PROFILE_UPDATE_SUCCESS,
24 | USER_PROFILE_UPDATE_FAILURE,
25 | USER_PROFILE_UPDATE_RESET,
26 | USER_LOGIN_REFRESH_REQUEST,
27 | USER_LOGIN_REFRESH_SUCCESS,
28 | USER_LOGIN_REFRESH_FAILURE,
29 | USER_LIST_REQUEST,
30 | USER_LIST_SUCCESS,
31 | USER_LIST_FAILURE,
32 | USER_LIST_RESET,
33 | USER_DELETE_REQUEST,
34 | USER_DELETE_SUCCESS,
35 | USER_DELETE_FAILURE,
36 | USER_UPDATE_REQUEST,
37 | USER_UPDATE_SUCCESS,
38 | USER_UPDATE_FAILURE,
39 | USER_UPDATE_RESET,
40 | } from '../constants/userConstants';
41 |
42 | export const userLoginReducer = (state = {}, action) => {
43 | switch (action.type) {
44 | case USER_LOGIN_REQUEST:
45 | return { ...state, loading: true };
46 | case USER_LOGIN_SUCCESS:
47 | return { loading: false, userInfo: action.payload };
48 | case USER_LOGIN_FAILURE:
49 | return { loading: false, error: action.payload };
50 | case USER_LOGOUT:
51 | return {};
52 | default:
53 | return { ...state };
54 | }
55 | };
56 |
57 | // use refresh token to obtain new access token for the logged in user
58 | export const userLoginRefreshReducer = (state = {}, action) => {
59 | switch (action.type) {
60 | case USER_LOGIN_REFRESH_REQUEST:
61 | return { ...state, loading: true };
62 | case USER_LOGIN_REFRESH_SUCCESS:
63 | return { loading: false, tokenInfo: action.payload };
64 | case USER_LOGIN_REFRESH_FAILURE:
65 | return { loading: false, error: action.payload };
66 | default:
67 | return { ...state };
68 | }
69 | };
70 |
71 | // sending an email for account verification
72 | export const userSendEmailVerficationReducer = (state = {}, action) => {
73 | switch (action.type) {
74 | case USER_EMAIL_VERIFICATION_REQUEST:
75 | return { isLoading: true };
76 | case USER_EMAIL_VERIFICATION_SUCCESS:
77 | return { isLoading: true, emailSent: action.payload };
78 | case USER_EMAIL_VERIFICATION_FAILURE:
79 | return { isLoading: true, hasError: action.payload };
80 | default:
81 | return { ...state };
82 | }
83 | };
84 |
85 | // update user account to confirmed
86 | export const userConfirmReducer = (state = { isConfirmed: false }, action) => {
87 | switch (action.type) {
88 | case USER_CONFIRM_REQUEST:
89 | return { ...state, loading: true };
90 | case USER_CONFIRM_SUCCESS:
91 | return { loading: false, isConfirmed: action.payload };
92 | case USER_CONFIRM_FAILURE:
93 | return { loading: false, error: action.payload };
94 | default:
95 | return { ...state };
96 | }
97 | };
98 |
99 | // reset the user password
100 | export const userResetPasswordReducer = (state = {}, action) => {
101 | switch (action.type) {
102 | case USER_RESET_PASSWORD_REQUEST:
103 | return { ...state, loading: true };
104 | case USER_RESET_PASSWORD_SUCCESS:
105 | return { loading: false, resetPassword: action.payload };
106 | case USER_RESET_PASSWORD_FAILURE:
107 | return { loading: false, error: action.payload };
108 | default: {
109 | return { ...state };
110 | }
111 | }
112 | };
113 |
114 | // register user using email and password
115 | export const userRegisterReducer = (state = {}, action) => {
116 | switch (action.type) {
117 | case USER_REGISTER_REQUEST:
118 | return { ...state, loading: true };
119 | case USER_REGISTER_SUCCESS:
120 | return { loading: false, userInfo: action.payload };
121 | case USER_REGISTER_FAILURE:
122 | return { loading: false, error: action.payload };
123 | default:
124 | return { ...state };
125 | }
126 | };
127 |
128 | // fetch user details
129 | export const userDetailsReducer = (state = { user: {} }, action) => {
130 | switch (action.type) {
131 | case USER_DETAILS_REQUEST:
132 | return { ...state, loading: true };
133 | case USER_DETAILS_SUCCESS:
134 | return { loading: false, user: action.payload };
135 | case USER_DETAILS_FAILURE:
136 | return { loading: false, error: action.payload };
137 | case USER_DETAILS_RESET:
138 | return { user: {} };
139 | default:
140 | return { ...state };
141 | }
142 | };
143 |
144 | // update user profile, not in admin view
145 | export const userProfileUpdateReducer = (state = {}, action) => {
146 | switch (action.type) {
147 | case USER_PROFILE_UPDATE_REQUEST:
148 | return { loading: true };
149 | case USER_PROFILE_UPDATE_SUCCESS:
150 | return { loading: false, success: true, userInfo: action.payload };
151 | case USER_PROFILE_UPDATE_FAILURE:
152 | return { loading: false, error: action.payload };
153 | case USER_PROFILE_UPDATE_RESET:
154 | return {};
155 | default:
156 | return { ...state };
157 | }
158 | };
159 |
160 | // list all users for the admin view
161 | export const userListReducer = (state = { users: [] }, action) => {
162 | switch (action.type) {
163 | case USER_LIST_REQUEST:
164 | return { loading: true };
165 | case USER_LIST_SUCCESS:
166 | return {
167 | loading: false,
168 | users: action.payload.users,
169 | page: action.payload.page,
170 | pages: action.payload.pages,
171 | total: action.payload.total,
172 | };
173 | case USER_LIST_FAILURE:
174 | return { loading: false, error: action.payload };
175 | case USER_LIST_RESET:
176 | return { users: [] };
177 | default:
178 | return { ...state };
179 | }
180 | };
181 |
182 | export const userDeleteReducer = (state = {}, action) => {
183 | switch (action.type) {
184 | case USER_DELETE_REQUEST:
185 | return { loading: true };
186 | case USER_DELETE_SUCCESS:
187 | return { loading: false, success: true };
188 | case USER_DELETE_FAILURE:
189 | return { loading: false, error: action.payload };
190 | default:
191 | return { ...state };
192 | }
193 | };
194 |
195 | // update user from the admin panel view
196 | export const userUpdateReducer = (state = { user: {} }, action) => {
197 | switch (action.type) {
198 | case USER_UPDATE_REQUEST:
199 | return { loading: true };
200 | case USER_UPDATE_SUCCESS:
201 | return { loading: false, success: true };
202 | case USER_UPDATE_FAILURE:
203 | return { loading: false, error: action.payload };
204 | case USER_UPDATE_RESET:
205 | return { user: {} };
206 | default:
207 | return { ...state };
208 | }
209 | };
210 |
--------------------------------------------------------------------------------
/backend/config/passportSetup.js:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import dotenv from 'dotenv';
3 | import User from '../models/userModel.js';
4 | import generateGravatar from '../utils/generateGravatar.js';
5 |
6 | // all passport strategies
7 | import GoogleStrategy from 'passport-google-oauth20';
8 | import GithubStrategy from 'passport-github2';
9 | import TwitterStrategy from 'passport-twitter';
10 | import LinkedInStrategy from 'passport-linkedin-oauth2';
11 |
12 | // to use .env variables in this file
13 | dotenv.config();
14 | const backendURL = process.env.BACKEND_BASE_URL;
15 |
16 | // Funtion to send a flash message depending on which social account the user had originally registered with
17 | const handleAuthError = (err, done) => {
18 | // we get the email from the option the user is currently trying to login with, and find the corresponding User obj
19 | User.findOne({
20 | email: err.keyValue.email, // err obj returned from mongoose has the keyValue key
21 | }).then((user) => {
22 | // check which socialID was stored in this User obj, return the corresponding error in format
23 | // done(null, false, {flash message}) -> which tells passport not to serialise this user
24 | if (user.googleID)
25 | return done(null, false, {
26 | message: 'Registered using google account',
27 | });
28 | if (user.githubID)
29 | return done(null, false, {
30 | message: 'Registered using github account',
31 | });
32 | if (user.twitterID)
33 | return done(null, false, {
34 | message: 'Registered using twitter account',
35 | });
36 | if (user.linkedinID)
37 | return done(null, false, {
38 | message: 'Registered using linkedin account',
39 | });
40 | });
41 | };
42 |
43 | // Include all passport strategies' setup in this function itself
44 | const setupPassport = () => {
45 | // setup a session with the logged in user, by serialising this user is
46 | passport.serializeUser((user, done) => {
47 | done(null, user.id);
48 | });
49 |
50 | // end the current login session after deserialising the user
51 | passport.deserializeUser((id, done) => {
52 | User.findById(id)
53 | .then((user) => done(null, user))
54 | .catch((err) => console.log(`${err}`.bgRed.bold));
55 | });
56 |
57 | // setup for the google strategy
58 | passport.use(
59 | new GoogleStrategy(
60 | {
61 | // options for the google strategy
62 | clientID: process.env.GOOGLE_OAUTH_CLIENT_ID,
63 | clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
64 | callbackURL: `${backendURL}/api/auth/google/redirect`,
65 | },
66 | (accessToken, refreshToken, profile, done) => {
67 | // if a user with this google ID is present, serialise that user, otherwise create a new User
68 | User.findOne({ googleID: profile.id }).then((foundUser) => {
69 | if (!foundUser) {
70 | User.create({
71 | name: profile.displayName,
72 | isAdmin: false,
73 | isConfirmed: profile._json.email_verified,
74 | googleID: profile.id,
75 | email: profile._json.email,
76 | avatar: generateGravatar(profile._json.email), // gravatar is unique for all email IDs
77 | })
78 | .then((user) => {
79 | done(null, user);
80 | })
81 | .catch((err) => {
82 | // In case the User couldn't be created, this means that the email key was duplicate
83 | // Which implies that the current email has already been registered using some different social account
84 | // So throw the corresponding flash message
85 | handleAuthError(err, done);
86 | });
87 | } else {
88 | done(null, foundUser);
89 | }
90 | });
91 | }
92 | )
93 | );
94 |
95 | // setup for the github strategy
96 | passport.use(
97 | new GithubStrategy(
98 | {
99 | // options for the github strategy
100 | clientID: process.env.GITHUB_CLIENT_ID,
101 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
102 | callbackURL: `${backendURL}/api/auth/github/redirect`,
103 | },
104 | (accessToken, refreshToken, profile, done) => {
105 | // if a user with this github ID is present, serialise that user, otherwise create a new User
106 | User.findOne({ githubID: profile.id }).then((foundUser) => {
107 | if (!foundUser) {
108 | User.create({
109 | name: profile.displayName,
110 | isAdmin: false,
111 | isConfirmed: !!profile._json.email,
112 | githubID: profile.id,
113 | avatar: generateGravatar(profile._json.email),
114 | email: profile._json.email,
115 | })
116 | .then((user) => {
117 | done(null, user);
118 | })
119 | .catch((err) => {
120 | handleAuthError(err, done);
121 | });
122 | } else {
123 | done(null, foundUser);
124 | }
125 | });
126 | }
127 | )
128 | );
129 | };
130 |
131 | // setup for the twitter strategy
132 | passport.use(
133 | new TwitterStrategy(
134 | {
135 | consumerKey: process.env.TWITTER_CONSUMER_KEY,
136 | consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
137 | callbackURL: `${backendURL}/api/auth/twitter/redirect`,
138 | includeEmail: true,
139 | },
140 | (accessToken, refreshToken, profile, done) => {
141 | User.findOne({ twitterID: profile.id }).then((foundUser) => {
142 | if (!foundUser) {
143 | User.create({
144 | name: profile.displayName,
145 | isAdmin: false,
146 | isConfirmed: true,
147 | twitterID: profile.id,
148 | avatar: generateGravatar(profile._json.email),
149 | email: profile._json.email,
150 | })
151 | .then((user) => {
152 | done(null, user);
153 | })
154 | .catch((err) => {
155 | handleAuthError(err, done);
156 | });
157 | } else {
158 | done(null, foundUser);
159 | }
160 | });
161 | }
162 | )
163 | );
164 |
165 | // setup for the linkedin strategy
166 | passport.use(
167 | new LinkedInStrategy.Strategy(
168 | {
169 | clientID: process.env.LINKEDIN_CLIENT_ID,
170 | clientSecret: process.env.LINKEDIN_CLIENT_SECRET,
171 | callbackURL: `${backendURL}/api/auth/linkedin/redirect`,
172 | scope: ['r_emailaddress', 'r_liteprofile'],
173 | state: true,
174 | },
175 | (accessToken, refreshToken, profile, done) => {
176 | User.findOne({ linkedinID: profile.id }).then((foundUser) => {
177 | if (!foundUser) {
178 | User.create({
179 | name: profile.displayName,
180 | isAdmin: false,
181 | isConfirmed: true,
182 | linkedinID: profile.id,
183 | email: profile.emails[0].value,
184 | avatar: generateGravatar(profile.emails[0].value),
185 | })
186 | .then((user) => {
187 | done(null, user);
188 | })
189 | .catch((err) => {
190 | handleAuthError(err, done);
191 | });
192 | } else {
193 | done(null, foundUser);
194 | }
195 | });
196 | }
197 | )
198 | );
199 |
200 | export default setupPassport;
201 |
--------------------------------------------------------------------------------
/frontend/src/pages/RegisterPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import {
4 | Form,
5 | Button,
6 | InputGroup,
7 | FloatingLabel,
8 | Row,
9 | Col,
10 | } from 'react-bootstrap';
11 | import Loader from '../components/Loader';
12 | import Message from '../components/Message';
13 | import FormContainer from '../components/FormContainer';
14 | import { registerUser } from '../actions/userActions';
15 | import '../styles/login-register.css';
16 |
17 | const RegisterPage = ({ location, history }) => {
18 | const [typePassword, setTypePassword] = useState('password');
19 | const [typeConfirmPassword, setTypeConfirmPassword] = useState('password');
20 |
21 | const [name, setName] = useState('');
22 | const [email, setEmail] = useState('');
23 | const [password, setPassword] = useState('');
24 | const [confirmPassword, setConfirmPassword] = useState('');
25 | const [message, setMessage] = useState(null);
26 | const dispatch = useDispatch();
27 |
28 | const redirect = location.search ? location.search.split('=')[1] : '';
29 | const userRegister = useSelector((state) => state.userRegister);
30 | const { loading, userInfo, error } = userRegister;
31 |
32 | useEffect(() => {
33 | if (userInfo) {
34 | localStorage.setItem('promptEmailVerfication', 'true');
35 | history.push(redirect);
36 | }
37 | }, [history, redirect, userInfo]);
38 |
39 | const showHidePassword = (e) => {
40 | e.preventDefault();
41 | e.stopPropagation();
42 | setTypePassword(typePassword === 'password' ? 'text' : 'password');
43 | };
44 | const showHideConfirmPassword = (e) => {
45 | e.preventDefault();
46 | e.stopPropagation();
47 | setTypeConfirmPassword(
48 | typeConfirmPassword === 'password' ? 'text' : 'password'
49 | );
50 | };
51 |
52 | const handleSubmit = (e) => {
53 | e.preventDefault();
54 | if (password !== confirmPassword) {
55 | setMessage('Passwords do not match. Please retry.');
56 | } else {
57 | dispatch(registerUser(name, email, password));
58 | }
59 | };
60 |
61 | return (
62 |
63 |
64 |
65 |
history.push('/login')}>Sign In
66 | history.push('/register')}>
75 | Sign Up
76 |
77 |
78 | {message && (
79 |
80 | {message}
81 |
82 | )}
83 | {error && (
84 |
85 | {error}
86 |
87 | )}
88 | {loading ? (
89 |
90 | ) : (
91 |
93 |
97 | setName(e.target.value)}
103 | />
104 |
105 |
106 |
107 |
111 | setEmail(e.target.value)}
117 | />
118 |
119 |
120 |
121 |
122 |
130 |
139 | setPassword(e.target.value)
140 | }
141 | />
142 |
143 |
152 | {typePassword === 'text' ? (
153 |
154 | ) : (
155 |
156 | )}
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
169 |
178 | setConfirmPassword(e.target.value)
179 | }
180 | />
181 |
182 |
191 | {typeConfirmPassword === 'text' ? (
192 |
193 | ) : (
194 |
195 | )}
196 |
197 |
198 |
199 |
200 |
201 |
202 |
206 |
213 | Register
214 |
215 |
216 |
217 |
218 | )}
219 |
220 |
221 | );
222 | };
223 |
224 | export default RegisterPage;
225 |
--------------------------------------------------------------------------------
/frontend/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | PRODUCT_DETAILS_FAILURE,
3 | PRODUCT_DETAILS_REQUEST,
4 | PRODUCT_DETAILS_SUCCESS,
5 | PRODUCT_LIST_FAILURE,
6 | PRODUCT_LIST_REQUEST,
7 | PRODUCT_LIST_SUCCESS,
8 | PRODUCT_DELETE_FAILURE,
9 | PRODUCT_DELETE_REQUEST,
10 | PRODUCT_DELETE_SUCCESS,
11 | PRODUCT_CREATE_REQUEST,
12 | PRODUCT_CREATE_SUCCESS,
13 | PRODUCT_CREATE_FAILURE,
14 | PRODUCT_UPDATE_REQUEST,
15 | PRODUCT_UPDATE_SUCCESS,
16 | PRODUCT_UPDATE_FAILURE,
17 | PRODUCT_CREATE_REVIEW_REQUEST,
18 | PRODUCT_CREATE_REVIEW_SUCCESS,
19 | PRODUCT_CREATE_REVIEW_FAILURE,
20 | PRODUCT_TOP_RATED_REQUEST,
21 | PRODUCT_TOP_RATED_SUCCESS,
22 | PRODUCT_TOP_RATED_FAILURE,
23 | } from '../constants/productConstants';
24 | import axios from 'axios';
25 |
26 | // list orders based on keyword and page number when paginated
27 | export const listProducts =
28 | (keyword = '', pageNumber = '', pageSize = '') =>
29 | async (dispatch) => {
30 | try {
31 | dispatch({ type: PRODUCT_LIST_REQUEST });
32 |
33 | const { data } = await axios.get(
34 | `/api/products?keyword=${keyword}&pageNumber=${pageNumber}&pageSize=${pageSize}`
35 | );
36 |
37 | dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data });
38 | } catch (error) {
39 | dispatch({
40 | type: PRODUCT_LIST_FAILURE,
41 | payload:
42 | error.response && error.response.data.message
43 | ? error.response.data.message
44 | : error.message,
45 | });
46 | }
47 | };
48 |
49 | // fetch details of a particular product
50 | export const listProductDetails = (id) => async (dispatch) => {
51 | try {
52 | dispatch({ type: PRODUCT_DETAILS_REQUEST });
53 |
54 | const { data } = await axios.get(`/api/products/${id}`);
55 |
56 | dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data });
57 | } catch (error) {
58 | dispatch({
59 | type: PRODUCT_DETAILS_FAILURE,
60 | payload:
61 | error.response && error.response.data.message
62 | ? error.response.data.message
63 | : error.message,
64 | });
65 | }
66 | };
67 |
68 | // delete a particular product by taking an id
69 | export const deleteProduct = (id) => async (dispatch, getState) => {
70 | try {
71 | dispatch({ type: PRODUCT_DELETE_REQUEST });
72 |
73 | const {
74 | userLogin: { userInfo },
75 | } = getState();
76 |
77 | // different headers are used when it is a social login, and when it is a std email login
78 | const config = userInfo.isSocialLogin
79 | ? {
80 | headers: {
81 | Authorization: `SocialLogin ${userInfo.id}`,
82 | },
83 | }
84 | : {
85 | headers: {
86 | Authorization: `Bearer ${userInfo.accessToken}`,
87 | },
88 | };
89 |
90 | const { data } = await axios.delete(`/api/products/${id}`, config);
91 |
92 | data && dispatch({ type: PRODUCT_DELETE_SUCCESS });
93 | } catch (error) {
94 | dispatch({
95 | type: PRODUCT_DELETE_FAILURE,
96 | payload:
97 | error.response && error.response.data.message
98 | ? error.response.data.message
99 | : error.message,
100 | });
101 | }
102 | };
103 |
104 | // create a product, when the user is an admin
105 | export const createProduct = () => async (dispatch, getState) => {
106 | try {
107 | dispatch({ type: PRODUCT_CREATE_REQUEST });
108 |
109 | const {
110 | userLogin: { userInfo },
111 | } = getState();
112 |
113 | // different headers are used when it is a social login, and when it is a std email login
114 | const config = userInfo.isSocialLogin
115 | ? {
116 | headers: {
117 | 'Content-Type': 'application/json',
118 | Authorization: `SocialLogin ${userInfo.id}`,
119 | },
120 | }
121 | : {
122 | headers: {
123 | 'Content-Type': 'application/json',
124 | Authorization: `Bearer ${userInfo.accessToken}`,
125 | },
126 | };
127 |
128 | const { data } = await axios.post(`/api/products/`, {}, config);
129 |
130 | dispatch({ type: PRODUCT_CREATE_SUCCESS, payload: data });
131 | } catch (error) {
132 | dispatch({
133 | type: PRODUCT_CREATE_FAILURE,
134 | payload:
135 | error.response && error.response.data.message
136 | ? error.response.data.message
137 | : error.message,
138 | });
139 | }
140 | };
141 |
142 | // update the product details from the admin panel view
143 | export const updateProduct = (product) => async (dispatch, getState) => {
144 | try {
145 | dispatch({ type: PRODUCT_UPDATE_REQUEST });
146 |
147 | const {
148 | userLogin: { userInfo },
149 | } = getState();
150 |
151 | // different headers are used when it is a social login, and when it is a std email login
152 | const config = userInfo.isSocialLogin
153 | ? {
154 | headers: {
155 | 'Content-Type': 'application/json',
156 | Authorization: `SocialLogin ${userInfo.id}`,
157 | },
158 | }
159 | : {
160 | headers: {
161 | 'Content-Type': 'application/json',
162 | Authorization: `Bearer ${userInfo.accessToken}`,
163 | },
164 | };
165 |
166 | const { data } = await axios.put(
167 | `/api/products/${product._id}`,
168 | product,
169 | config
170 | );
171 |
172 | dispatch({ type: PRODUCT_UPDATE_SUCCESS, payload: data });
173 | } catch (error) {
174 | dispatch({
175 | type: PRODUCT_UPDATE_FAILURE,
176 | payload:
177 | error.response && error.response.data.message
178 | ? error.response.data.message
179 | : error.message,
180 | });
181 | }
182 | };
183 |
184 | // create a new product review for particular product
185 | export const createProductReview =
186 | (productID, review) => async (dispatch, getState) => {
187 | try {
188 | dispatch({ type: PRODUCT_CREATE_REVIEW_REQUEST });
189 |
190 | const {
191 | userLogin: { userInfo },
192 | } = getState();
193 |
194 | // different headers are used when it is a social login, and when it is a std email login
195 | const config = userInfo.isSocialLogin
196 | ? {
197 | headers: {
198 | 'Content-Type': 'application/json',
199 | Authorization: `SocialLogin ${userInfo.id}`,
200 | },
201 | }
202 | : {
203 | headers: {
204 | 'Content-Type': 'application/json',
205 | Authorization: `Bearer ${userInfo.accessToken}`,
206 | },
207 | };
208 |
209 | await axios.post(
210 | `/api/products/${productID}/reviews`,
211 | review,
212 | config
213 | );
214 |
215 | dispatch({ type: PRODUCT_CREATE_REVIEW_SUCCESS });
216 | } catch (error) {
217 | dispatch({
218 | type: PRODUCT_CREATE_REVIEW_FAILURE,
219 | payload:
220 | error.response && error.response.data.message
221 | ? error.response.data.message
222 | : error.message,
223 | });
224 | }
225 | };
226 |
227 | // fetch the top rated products for the carousel
228 | export const getTopRatedProducts = () => async (dispatch) => {
229 | try {
230 | dispatch({ type: PRODUCT_TOP_RATED_REQUEST });
231 |
232 | const { data } = await axios.get('/api/products/top');
233 |
234 | dispatch({ type: PRODUCT_TOP_RATED_SUCCESS, payload: data });
235 | } catch (error) {
236 | dispatch({
237 | type: PRODUCT_TOP_RATED_FAILURE,
238 | payload:
239 | error.response && error.response.data.message
240 | ? error.response.data.message
241 | : error.message,
242 | });
243 | }
244 | };
245 |
--------------------------------------------------------------------------------
/frontend/src/actions/orderActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | ORDER_CREATE_REQUEST,
3 | ORDER_CREATE_SUCCESS,
4 | ORDER_CREATE_FAILURE,
5 | ORDER_DETAILS_REQUEST,
6 | ORDER_DETAILS_SUCCESS,
7 | ORDER_DETAILS_FAILURE,
8 | ORDER_PAY_REQUEST,
9 | ORDER_PAY_SUCCESS,
10 | ORDER_PAY_FAILURE,
11 | ORDER_DELIVER_REQUEST,
12 | ORDER_DELIVER_SUCCESS,
13 | ORDER_DELIVER_FAILURE,
14 | ORDER_USER_LIST_REQUEST,
15 | ORDER_USER_LIST_SUCCESS,
16 | ORDER_USER_LIST_FAILURE,
17 | ORDER_ALL_LIST_REQUEST,
18 | ORDER_ALL_LIST_SUCCESS,
19 | ORDER_ALL_LIST_FAILURE,
20 | } from '../constants/orderConstants';
21 |
22 | import axios from 'axios';
23 |
24 | // get all the details about the order and dispatch only of currently logged in
25 | export const createOrder = (order) => async (dispatch, getState) => {
26 | try {
27 | dispatch({ type: ORDER_CREATE_REQUEST });
28 |
29 | const {
30 | userLogin: { userInfo },
31 | } = getState();
32 |
33 | // different headers are used when it is a social login, and when it is a std email login
34 | const config = userInfo.isSocialLogin
35 | ? {
36 | headers: {
37 | 'Content-Type': 'application/json',
38 | Authorization: `SocialLogin ${userInfo.id}`,
39 | },
40 | }
41 | : {
42 | headers: {
43 | 'Content-Type': 'application/json',
44 | Authorization: `Bearer ${userInfo.accessToken}`,
45 | },
46 | };
47 |
48 | const { data } = await axios.post('/api/orders/', order, config);
49 |
50 | dispatch({ type: ORDER_CREATE_SUCCESS, payload: data });
51 | } catch (error) {
52 | dispatch({
53 | type: ORDER_CREATE_FAILURE,
54 | payload:
55 | error.response && error.response.data.message
56 | ? error.response.data.message
57 | : error.message,
58 | });
59 | }
60 | };
61 |
62 | // get details about a particular order
63 | export const getOrderDetails = (orderID) => async (dispatch, getState) => {
64 | try {
65 | dispatch({ type: ORDER_DETAILS_REQUEST });
66 |
67 | const {
68 | userLogin: { userInfo },
69 | } = getState();
70 |
71 | // different headers are used when it is a social login, and when it is a std email login
72 | const config = userInfo.isSocialLogin
73 | ? {
74 | headers: {
75 | Authorization: `SocialLogin ${userInfo.id}`,
76 | },
77 | }
78 | : {
79 | headers: {
80 | Authorization: `Bearer ${userInfo.accessToken}`,
81 | },
82 | };
83 |
84 | const { data } = await axios.get(`/api/orders/${orderID}`, config);
85 |
86 | dispatch({ type: ORDER_DETAILS_SUCCESS, payload: data });
87 | } catch (error) {
88 | dispatch({
89 | type: ORDER_DETAILS_FAILURE,
90 | payload:
91 | error.response && error.response.data.message
92 | ? error.response.data.message
93 | : error.message,
94 | });
95 | }
96 | };
97 |
98 | // update the current order to that of a paid one, and store the correct payment result
99 | export const payOrder =
100 | (orderID, paymentResult) => async (dispatch, getState) => {
101 | try {
102 | dispatch({ type: ORDER_PAY_REQUEST });
103 |
104 | const {
105 | userLogin: { userInfo },
106 | } = getState();
107 |
108 | // different headers are used when it is a social login, and when it is a std email login
109 | const config = userInfo.isSocialLogin
110 | ? {
111 | headers: {
112 | 'Content-Type': 'application/json',
113 | Authorization: `SocialLogin ${userInfo.id}`,
114 | },
115 | }
116 | : {
117 | headers: {
118 | 'Content-Type': 'application/json',
119 | Authorization: `Bearer ${userInfo.accessToken}`,
120 | },
121 | };
122 |
123 | const { data } = await axios.put(
124 | `/api/orders/${orderID}/pay`,
125 | paymentResult,
126 | config
127 | );
128 |
129 | dispatch({ type: ORDER_PAY_SUCCESS, payload: data });
130 | } catch (error) {
131 | dispatch({
132 | type: ORDER_PAY_FAILURE,
133 | payload:
134 | error.response && error.response.data.message
135 | ? error.response.data.message
136 | : error.message,
137 | });
138 | }
139 | };
140 |
141 | // Set the current order as delivered, only when logged in user is an admin
142 | export const deliverOrder = (orderID) => async (dispatch, getState) => {
143 | try {
144 | dispatch({ type: ORDER_DELIVER_REQUEST });
145 |
146 | const {
147 | userLogin: { userInfo },
148 | } = getState();
149 |
150 | const config = userInfo.isSocialLogin
151 | ? {
152 | headers: {
153 | Authorization: `SocialLogin ${userInfo.id}`,
154 | },
155 | }
156 | : {
157 | headers: {
158 | Authorization: `Bearer ${userInfo.accessToken}`,
159 | },
160 | };
161 |
162 | const { data } = await axios.put(
163 | `/api/orders/${orderID}/deliver`,
164 | {},
165 | config
166 | );
167 |
168 | dispatch({ type: ORDER_DELIVER_SUCCESS, payload: data });
169 | } catch (error) {
170 | dispatch({
171 | type: ORDER_DELIVER_FAILURE,
172 | payload:
173 | error.response && error.response.data.message
174 | ? error.response.data.message
175 | : error.message,
176 | });
177 | }
178 | };
179 |
180 | // list all the orders of a particular user
181 | export const listMyOrders = () => async (dispatch, getState) => {
182 | try {
183 | dispatch({ type: ORDER_USER_LIST_REQUEST });
184 |
185 | const {
186 | userLogin: { userInfo },
187 | } = getState();
188 |
189 | // different headers are used when it is a social login, and when it is a std email login
190 | const config = userInfo.isSocialLogin
191 | ? {
192 | headers: {
193 | Authorization: `SocialLogin ${userInfo.id}`,
194 | },
195 | }
196 | : {
197 | headers: {
198 | Authorization: `Bearer ${userInfo.accessToken}`,
199 | },
200 | };
201 |
202 | const { data } = await axios.get(`/api/orders/myorders`, config);
203 |
204 | dispatch({ type: ORDER_USER_LIST_SUCCESS, payload: data });
205 | } catch (error) {
206 | dispatch({
207 | type: ORDER_USER_LIST_FAILURE,
208 | payload:
209 | error.response && error.response.data.message
210 | ? error.response.data.message
211 | : error.message,
212 | });
213 | }
214 | };
215 |
216 | // list all orders for the admin panel view, include the pagenumber being fetched
217 | export const listAllOrders =
218 | (pageNumber = '') =>
219 | async (dispatch, getState) => {
220 | try {
221 | dispatch({ type: ORDER_ALL_LIST_REQUEST });
222 |
223 | const {
224 | userLogin: { userInfo },
225 | } = getState();
226 |
227 | // different headers are used when it is a social login, and when it is a std email login
228 | const config = userInfo.isSocialLogin
229 | ? {
230 | headers: {
231 | Authorization: `SocialLogin ${userInfo.id}`,
232 | },
233 | }
234 | : {
235 | headers: {
236 | Authorization: `Bearer ${userInfo.accessToken}`,
237 | },
238 | };
239 |
240 | const { data } = await axios.get(
241 | `/api/orders?pageNumber=${pageNumber}`,
242 | config
243 | );
244 |
245 | dispatch({ type: ORDER_ALL_LIST_SUCCESS, payload: data });
246 | } catch (error) {
247 | dispatch({
248 | type: ORDER_ALL_LIST_FAILURE,
249 | payload:
250 | error.response && error.response.data.message
251 | ? error.response.data.message
252 | : error.message,
253 | });
254 | }
255 | };
256 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { LinkContainer } from 'react-router-bootstrap';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { Navbar, Nav, Container, NavDropdown } from 'react-bootstrap';
5 | import { logoutUser } from '../actions/userActions';
6 | import { Route } from 'react-router-dom';
7 | import SearchBox from './SearchBox';
8 | import '../styles/header.css';
9 |
10 | const Header = () => {
11 | const dispatch = useDispatch();
12 | const userLogin = useSelector((state) => state.userLogin);
13 | const cart = useSelector((state) => state.cart);
14 | const { userInfo } = userLogin;
15 | const { cartItems } = cart;
16 |
17 | const [show1, setShow1] = useState(false); // to close dropdown when clicking anywhere outside
18 | const [show2, setShow2] = useState(false); // to close dropdown when clicking anywhere outside
19 | const [count, setCount] = useState(0);
20 |
21 | // update count when new cart changes
22 | useEffect(() => {
23 | setCount(cartItems.reduce((acc, item) => acc + item.qty, 0));
24 | }, [cartItems]);
25 |
26 | // close the second dropdown when it is open and user clicks anywhere else
27 | const handleDropdown2 = (e) => {
28 | if (show2) {
29 | setShow2(false);
30 | } else {
31 | setShow2(true);
32 | setShow1(false);
33 | }
34 | };
35 |
36 | // close the first dropdown when it is open and user clicks anywhere else
37 | const handleDropdown1 = (e) => {
38 | if (show1) {
39 | setShow1(false);
40 | } else {
41 | setShow1(true);
42 | setShow2(false);
43 | }
44 | };
45 |
46 | // dispatch action to logout user
47 | const handleLogout = () => {
48 | dispatch(logoutUser());
49 | window.location.href = '/';
50 | };
51 |
52 | // render different navbars for large and small screens without navbar toggle
53 | return (
54 |
55 | {/* this section covers entire screen except the dropdown, to handle onclicks */}
56 | 430 && (show1 || show2)
61 | ? 'block'
62 | : 'none',
63 | minWidth: '100%',
64 | height: '100%',
65 | zIndex: '100',
66 | position: 'absolute',
67 | }}
68 | onClick={() => {
69 | setShow1(false);
70 | setShow2(false);
71 | }}
72 | />
73 |
74 | {/* conditionally render different navbars for the mobile sreens */}
75 |
76 |
77 |
78 |
79 |
84 | Kosells
85 |
86 |
87 |
88 | {/* history is available only inside Route, so this is used */}
89 | {/* display searchbar inside navbar in large screens only */}
90 | (
92 |
93 |
94 |
95 | )}
96 | />
97 |
98 |
109 | {userInfo && userInfo.isAdmin && (
110 | <>
111 | {/* display this only on mobile screens */}
112 |
115 |
116 |
117 |
118 |
119 |
122 |
123 |
124 |
125 |
126 |
129 |
130 |
131 |
132 |
133 | >
134 | )}
135 |
136 |
137 | {/* indicate cart size */}
138 | {count ? (
139 |
140 | 10
143 | ? { fontSize: '0.6em' }
144 | : { fontSize: '0.7em' }
145 | }>
146 | {count}
147 |
148 |
149 | ) : (
150 | ''
151 | )}
152 | {' '}
153 | {!(userInfo && userInfo.isAdmin) ||
154 | window.innerWidth >= 430
155 | ? 'Cart'
156 | : ''}
157 |
158 |
159 | {userInfo && userInfo.isAdmin && (
160 | // show this only on md screens and above
161 |
179 | )}
180 |
181 | {userInfo && (
182 | // show this only on mobile screens
183 |
186 | {' '}
187 | {!(userInfo && userInfo.isAdmin) && 'Logout'}
188 |
189 | )}
190 |
191 | {userInfo ? (
192 |
193 | {/* show this container only on mobile screens */}
194 |
197 |
198 |
203 |
204 |
205 |
210 |
211 | {/* show this dropdown only on large screens */}
212 |
218 |
219 |
220 | Profile
221 |
222 |
223 |
224 | Logout
225 |
226 |
227 |
228 | ) : (
229 |
230 |
231 | {' '}
232 | Sign In
233 |
234 |
235 | )}
236 |
237 |
238 |
239 |
240 | );
241 | };
242 |
243 | export default Header;
244 |
--------------------------------------------------------------------------------
Or Connect With
22 | 23 |