├── 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 |
6 |
7 |
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 | error 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 | 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 | 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 |
19 | {/* display searchbar inside navbar in large screens only */} 20 | 21 | setKeyword(e.target.value)} 30 | placeholder='Search Products...' 31 | value={keyword} 32 | /> 33 | 39 | 54 | 55 | 56 | 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 | {product.name} 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 |
19 |
20 |
21 |

Or Connect With

22 |
23 |
24 | 33 | 34 | 43 | 44 | 45 | 54 | 55 | 56 | 65 | 66 | 67 | 76 | 77 | 78 |
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 | 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 |
68 | 69 | 70 | 80 | 90 | 91 | 92 |
93 | 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 |
76 | {error && ( 77 | 78 | {error} 79 | 80 | )} 81 | 86 | 103 | 104 |
105 | 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 |