├── uploads └── .gitkeep ├── frontend ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── images │ │ ├── airpods.jpg │ │ ├── alexa.jpg │ │ ├── camera.jpg │ │ ├── mouse.jpg │ │ ├── phone.jpg │ │ ├── sample.jpg │ │ ├── screens.png │ │ └── playstation.jpg │ ├── manifest.json │ └── index.html ├── src │ ├── assets │ │ ├── logo.png │ │ └── styles │ │ │ └── index.css │ ├── setupTests.js │ ├── components │ │ ├── Message.jsx │ │ ├── PrivateRoute.jsx │ │ ├── FormContainer.jsx │ │ ├── Loader.jsx │ │ ├── AdminRoute.jsx │ │ ├── Footer.jsx │ │ ├── Meta.jsx │ │ ├── Paginate.jsx │ │ ├── Product.jsx │ │ ├── ProductCarousel.jsx │ │ ├── SearchBox.jsx │ │ ├── Rating.jsx │ │ ├── CheckoutSteps.jsx │ │ └── Header.jsx │ ├── constants.js │ ├── reportWebVitals.js │ ├── store.js │ ├── slices │ │ ├── authSlice.js │ │ ├── apiSlice.js │ │ ├── ordersApiSlice.js │ │ ├── usersApiSlice.js │ │ ├── cartSlice.js │ │ └── productsApiSlice.js │ ├── App.js │ ├── utils │ │ └── cartUtils.js │ ├── screens │ │ ├── HomeScreen.jsx │ │ ├── PaymentScreen.jsx │ │ ├── admin │ │ │ ├── OrderListScreen.jsx │ │ │ ├── UserListScreen.jsx │ │ │ ├── UserEditScreen.jsx │ │ │ ├── ProductListScreen.jsx │ │ │ └── ProductEditScreen.jsx │ │ ├── LoginScreen.jsx │ │ ├── ShippingScreen.jsx │ │ ├── RegisterScreen.jsx │ │ ├── CartScreen.jsx │ │ ├── PlaceOrderScreen.jsx │ │ ├── ProfileScreen.jsx │ │ ├── ProductScreen.jsx │ │ └── OrderScreen.jsx │ └── index.js ├── package.json └── README.md ├── backend ├── middleware │ ├── asyncHandler.js │ ├── errorMiddleware.js │ ├── checkObjectId.js │ └── authMiddleware.js ├── config │ └── db.js ├── data │ ├── users.js │ └── products.js ├── utils │ ├── generateToken.js │ ├── calcPrices.js │ └── paypal.js ├── routes │ ├── orderRoutes.js │ ├── userRoutes.js │ ├── productRoutes.js │ └── uploadRoutes.js ├── models │ ├── userModel.js │ ├── productModel.js │ └── orderModel.js ├── seeder.js ├── server.js └── controllers │ ├── productController.js │ ├── orderController.js │ └── userController.js ├── .prettierrc.yaml ├── .env.example ├── .gitignore ├── package.json └── readme.md /uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/Vector-6/React-Shop/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/public/images/airpods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/airpods.jpg -------------------------------------------------------------------------------- /frontend/public/images/alexa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/alexa.jpg -------------------------------------------------------------------------------- /frontend/public/images/camera.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/camera.jpg -------------------------------------------------------------------------------- /frontend/public/images/mouse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/mouse.jpg -------------------------------------------------------------------------------- /frontend/public/images/phone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/phone.jpg -------------------------------------------------------------------------------- /frontend/public/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/sample.jpg -------------------------------------------------------------------------------- /frontend/public/images/screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/screens.png -------------------------------------------------------------------------------- /frontend/public/images/playstation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vector-6/React-Shop/HEAD/frontend/public/images/playstation.jpg -------------------------------------------------------------------------------- /backend/middleware/asyncHandler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (fn) => (req, res, next) => 2 | Promise.resolve(fn(req, res, next)).catch(next); 3 | 4 | export default asyncHandler; 5 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | tabWidth: 2 3 | useTabs: false 4 | semi: true 5 | singleQuote: true 6 | bracketSpacing: true 7 | jsxBracketSameLine: false 8 | jsxSingleQuote: true 9 | trailingComma: es5 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | MONGO_URI= 3 | JWT_SECRET= 4 | PAYPAL_CLIENT_ID= 5 | PAYPAL_APP_SECRET= 6 | PAYPAL_API_URL=https://api-m.sandbox.paypal.com 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'react-bootstrap'; 2 | 3 | const Message = ({ variant, children }) => { 4 | return {children}; 5 | }; 6 | 7 | Message.defaultProps = { 8 | variant: 'info', 9 | }; 10 | 11 | export default Message; 12 | -------------------------------------------------------------------------------- /frontend/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const PrivateRoute = () => { 5 | const { userInfo } = useSelector((state) => state.auth); 6 | return userInfo ? : ; 7 | }; 8 | export default PrivateRoute; 9 | -------------------------------------------------------------------------------- /frontend/src/constants.js: -------------------------------------------------------------------------------- 1 | // export const BASE_URL = 2 | // process.env.NODE_ENV === 'develeopment' ? 'http://localhost:5000' : ''; 3 | export const BASE_URL = ''; // If using proxy 4 | export const PRODUCTS_URL = '/api/products'; 5 | export const USERS_URL = '/api/users'; 6 | export const ORDERS_URL = '/api/orders'; 7 | export const PAYPAL_URL = '/api/config/paypal'; 8 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const connectDB = async () => { 4 | try { 5 | const conn = await mongoose.connect(process.env.MONGO_URI); 6 | console.log(`MongoDB Connected: ${conn.connection.host}`); 7 | } catch (error) { 8 | console.error(`Error: ${error.message}`); 9 | process.exit(1); 10 | } 11 | }; 12 | 13 | export default connectDB; 14 | -------------------------------------------------------------------------------- /frontend/src/components/FormContainer.jsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from 'react-bootstrap'; 2 | 3 | const FormContainer = ({ children }) => { 4 | return ( 5 | 6 | 7 | 8 | {children} 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default FormContainer; 16 | -------------------------------------------------------------------------------- /frontend/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from 'react-bootstrap'; 2 | 3 | const Loader = () => { 4 | return ( 5 | 15 | ); 16 | }; 17 | 18 | export default Loader; 19 | -------------------------------------------------------------------------------- /frontend/src/components/AdminRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const AdminRoute = () => { 5 | const { userInfo } = useSelector((state) => state.auth); 6 | return userInfo && userInfo.isAdmin ? ( 7 | 8 | ) : ( 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default AdminRoute; 17 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from 'react-bootstrap'; 2 | 3 | const Footer = () => { 4 | const currentYear = new Date().getFullYear(); 5 | 6 | return ( 7 |
8 | 9 | 10 | 11 |

ProShop © {currentYear}

12 | 13 |
14 |
15 |
16 | ); 17 | }; 18 | export default Footer; 19 | -------------------------------------------------------------------------------- /backend/data/users.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | const users = [ 4 | { 5 | name: 'Admin User', 6 | email: 'admin@email.com', 7 | password: bcrypt.hashSync('123456', 10), 8 | isAdmin: true, 9 | }, 10 | { 11 | name: 'John Doe', 12 | email: 'john@email.com', 13 | password: bcrypt.hashSync('123456', 10), 14 | }, 15 | { 16 | name: 'Jane Doe', 17 | email: 'jane@email.com', 18 | password: bcrypt.hashSync('123456', 10), 19 | }, 20 | ]; 21 | 22 | export default users; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | frontend/build 27 | uploads/*png 28 | uploads/*jpg 29 | 30 | # API tests 31 | http 32 | -------------------------------------------------------------------------------- /backend/utils/generateToken.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | const generateToken = (res, userId) => { 4 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, { 5 | expiresIn: '30d', 6 | }); 7 | 8 | // Set JWT as an HTTP-Only cookie 9 | res.cookie('jwt', token, { 10 | httpOnly: true, 11 | secure: process.env.NODE_ENV !== 'development', // Use secure cookies in production 12 | sameSite: 'strict', // Prevent CSRF attacks 13 | maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days 14 | }); 15 | }; 16 | 17 | export default generateToken; 18 | -------------------------------------------------------------------------------- /frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { apiSlice } from './slices/apiSlice'; 3 | import cartSliceReducer from './slices/cartSlice'; 4 | import authReducer from './slices/authSlice'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | [apiSlice.reducerPath]: apiSlice.reducer, 9 | cart: cartSliceReducer, 10 | auth: authReducer, 11 | }, 12 | middleware: (getDefaultMiddleware) => 13 | getDefaultMiddleware().concat(apiSlice.middleware), 14 | devTools: true, 15 | }); 16 | 17 | export default store; 18 | -------------------------------------------------------------------------------- /frontend/src/components/Meta.jsx: -------------------------------------------------------------------------------- 1 | import { Helmet } from 'react-helmet-async'; 2 | 3 | const Meta = ({ title, description, keywords }) => { 4 | return ( 5 | 6 | {title} 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | Meta.defaultProps = { 14 | title: 'Welcome To ProShop', 15 | description: 'We sell the best products for cheap', 16 | keywords: 'electronics, buy electronics, cheap electroincs', 17 | }; 18 | 19 | export default Meta; 20 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Welcome To ProShop! 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/middleware/errorMiddleware.js: -------------------------------------------------------------------------------- 1 | const notFound = (req, res, next) => { 2 | const error = new Error(`Not Found - ${req.originalUrl}`); 3 | res.status(404); 4 | next(error); 5 | }; 6 | 7 | const errorHandler = (err, req, res, next) => { 8 | let statusCode = res.statusCode === 200 ? 500 : res.statusCode; 9 | let message = err.message; 10 | 11 | // NOTE: checking for invalid ObjectId moved to it's own middleware 12 | // See README for further info. 13 | 14 | res.status(statusCode).json({ 15 | message: message, 16 | stack: process.env.NODE_ENV === 'production' ? null : err.stack, 17 | }); 18 | }; 19 | 20 | export { notFound, errorHandler }; 21 | -------------------------------------------------------------------------------- /backend/routes/orderRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import { 4 | addOrderItems, 5 | getMyOrders, 6 | getOrderById, 7 | updateOrderToPaid, 8 | updateOrderToDelivered, 9 | getOrders, 10 | } from '../controllers/orderController.js'; 11 | import { protect, admin } from '../middleware/authMiddleware.js'; 12 | 13 | router.route('/').post(protect, addOrderItems).get(protect, admin, getOrders); 14 | router.route('/mine').get(protect, getMyOrders); 15 | router.route('/:id').get(protect, getOrderById); 16 | router.route('/:id/pay').put(protect, updateOrderToPaid); 17 | router.route('/:id/deliver').put(protect, admin, updateOrderToDelivered); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /backend/middleware/checkObjectId.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { isValidObjectId } from 'mongoose'; 3 | 4 | /** 5 | * Checks if the req.params.id is a valid Mongoose ObjectId. 6 | * 7 | * @param {import('express').Request} req - The Express request object. 8 | * @param {import('express').Response} res - The Express response object. 9 | * @param {import('express').NextFunction} next - The Express next middleware function. 10 | * @throws {Error} Throws an error if the ObjectId is invalid. 11 | */ 12 | 13 | function checkObjectId(req, res, next) { 14 | if (!isValidObjectId(req.params.id)) { 15 | res.status(404); 16 | throw new Error(`Invalid ObjectId of: ${req.params.id}`); 17 | } 18 | next(); 19 | } 20 | 21 | export default checkObjectId; 22 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { 3 | authUser, 4 | registerUser, 5 | logoutUser, 6 | getUserProfile, 7 | updateUserProfile, 8 | getUsers, 9 | deleteUser, 10 | getUserById, 11 | updateUser, 12 | } from '../controllers/userController.js'; 13 | import { protect, admin } from '../middleware/authMiddleware.js'; 14 | 15 | const router = express.Router(); 16 | 17 | router.route('/').post(registerUser).get(protect, admin, getUsers); 18 | router.post('/auth', authUser); 19 | router.post('/logout', logoutUser); 20 | router 21 | .route('/profile') 22 | .get(protect, getUserProfile) 23 | .put(protect, updateUserProfile); 24 | router 25 | .route('/:id') 26 | .delete(protect, admin, deleteUser) 27 | .get(protect, admin, getUserById) 28 | .put(protect, admin, updateUser); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Paginate.jsx: -------------------------------------------------------------------------------- 1 | import { Pagination } from 'react-bootstrap'; 2 | import { LinkContainer } from 'react-router-bootstrap'; 3 | 4 | const Paginate = ({ pages, page, isAdmin = false, keyword = '' }) => { 5 | return ( 6 | pages > 1 && ( 7 | 8 | {[...Array(pages).keys()].map((x) => ( 9 | 19 | {x + 1} 20 | 21 | ))} 22 | 23 | ) 24 | ); 25 | }; 26 | 27 | export default Paginate; 28 | -------------------------------------------------------------------------------- /backend/routes/productRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import { 4 | getProducts, 5 | getProductById, 6 | createProduct, 7 | updateProduct, 8 | deleteProduct, 9 | createProductReview, 10 | getTopProducts, 11 | } from '../controllers/productController.js'; 12 | import { protect, admin } from '../middleware/authMiddleware.js'; 13 | import checkObjectId from '../middleware/checkObjectId.js'; 14 | 15 | router.route('/').get(getProducts).post(protect, admin, createProduct); 16 | router.route('/:id/reviews').post(protect, checkObjectId, createProductReview); 17 | router.get('/top', getTopProducts); 18 | router 19 | .route('/:id') 20 | .get(checkObjectId, getProductById) 21 | .put(protect, admin, checkObjectId, updateProduct) 22 | .delete(protect, admin, checkObjectId, deleteProduct); 23 | 24 | export default router; 25 | -------------------------------------------------------------------------------- /frontend/src/slices/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | userInfo: localStorage.getItem('userInfo') 5 | ? JSON.parse(localStorage.getItem('userInfo')) 6 | : null, 7 | }; 8 | 9 | const authSlice = createSlice({ 10 | name: 'auth', 11 | initialState, 12 | reducers: { 13 | setCredentials: (state, action) => { 14 | state.userInfo = action.payload; 15 | localStorage.setItem('userInfo', JSON.stringify(action.payload)); 16 | }, 17 | logout: (state, action) => { 18 | state.userInfo = null; 19 | // NOTE: here we need to also remove the cart from storage so the next 20 | // logged in user doesn't inherit the previous users cart and shipping 21 | localStorage.clear(); 22 | }, 23 | }, 24 | }); 25 | 26 | export const { setCredentials, logout } = authSlice.actions; 27 | 28 | export default authSlice.reducer; 29 | -------------------------------------------------------------------------------- /frontend/src/components/Product.jsx: -------------------------------------------------------------------------------- 1 | import { Card } from 'react-bootstrap'; 2 | import { Link } from 'react-router-dom'; 3 | import Rating from './Rating'; 4 | 5 | const Product = ({ product }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {product.name} 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | ${product.price} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Product; 33 | -------------------------------------------------------------------------------- /frontend/src/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | main { 2 | min-height: 80vh; 3 | } 4 | 5 | .rating span { 6 | margin: 0.1rem; 7 | } 8 | 9 | .rating svg { 10 | color: #f8e825; 11 | } 12 | 13 | .rating-text { 14 | font-size: 0.8rem; 15 | font-weight: 600; 16 | padding-left: 0.5rem; 17 | } 18 | 19 | .product-title { 20 | height: 2.5em; /* Set a fixed height */ 21 | overflow: hidden; /* Hide overflow content */ 22 | text-overflow: ellipsis; /* Add ellipsis for long text */ 23 | white-space: nowrap; /* Prevent wrapping */ 24 | } 25 | 26 | table td, 27 | table th { 28 | text-align: center; 29 | } 30 | 31 | .review { 32 | margin-top: 30px; 33 | } 34 | 35 | .review h2 { 36 | font-size: 24px; 37 | background: #f4f4f4; 38 | padding: 10px; 39 | border: 1px solid #ddd; 40 | } 41 | 42 | .review button { 43 | margin-top: 10px; 44 | } 45 | 46 | .carousel-caption { 47 | position: absolute; 48 | width: 100%; 49 | left: 0; 50 | right: 0; 51 | bottom: 0; 52 | background: rgba(0, 0, 0, 0.5); 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/slices/apiSlice.js: -------------------------------------------------------------------------------- 1 | import { fetchBaseQuery, createApi } from '@reduxjs/toolkit/query/react'; 2 | import { BASE_URL } from '../constants'; 3 | 4 | import { logout } from './authSlice'; // Import the logout action 5 | 6 | // NOTE: code here has changed to handle when our JWT and Cookie expire. 7 | // We need to customize the baseQuery to be able to intercept any 401 responses 8 | // and log the user out 9 | // https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#customizing-queries-with-basequery 10 | 11 | const baseQuery = fetchBaseQuery({ 12 | baseUrl: BASE_URL, 13 | }); 14 | 15 | async function baseQueryWithAuth(args, api, extra) { 16 | const result = await baseQuery(args, api, extra); 17 | // Dispatch the logout action on 401. 18 | if (result.error && result.error.status === 401) { 19 | api.dispatch(logout()); 20 | } 21 | return result; 22 | } 23 | 24 | export const apiSlice = createApi({ 25 | baseQuery: baseQueryWithAuth, // Use the customized baseQuery 26 | tagTypes: ['Product', 'Order', 'User'], 27 | endpoints: (builder) => ({}), 28 | }); 29 | -------------------------------------------------------------------------------- /backend/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import asyncHandler from './asyncHandler.js'; 3 | import User from '../models/userModel.js'; 4 | 5 | // User must be authenticated 6 | const protect = asyncHandler(async (req, res, next) => { 7 | let token; 8 | 9 | // Read JWT from the 'jwt' cookie 10 | token = req.cookies.jwt; 11 | 12 | if (token) { 13 | try { 14 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 15 | 16 | req.user = await User.findById(decoded.userId).select('-password'); 17 | 18 | next(); 19 | } catch (error) { 20 | console.error(error); 21 | res.status(401); 22 | throw new Error('Not authorized, token failed'); 23 | } 24 | } else { 25 | res.status(401); 26 | throw new Error('Not authorized, no token'); 27 | } 28 | }); 29 | 30 | // User must be an admin 31 | const admin = (req, res, next) => { 32 | if (req.user && req.user.isAdmin) { 33 | next(); 34 | } else { 35 | res.status(401); 36 | throw new Error('Not authorized as an admin'); 37 | } 38 | }; 39 | 40 | export { protect, admin }; 41 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Container } from 'react-bootstrap'; 4 | import { Outlet } from 'react-router-dom'; 5 | import Header from './components/Header'; 6 | import Footer from './components/Footer'; 7 | import { logout } from './slices/authSlice'; 8 | 9 | import { ToastContainer } from 'react-toastify'; 10 | import 'react-toastify/dist/ReactToastify.css'; 11 | 12 | const App = () => { 13 | const dispatch = useDispatch(); 14 | 15 | useEffect(() => { 16 | const expirationTime = localStorage.getItem('expirationTime'); 17 | if (expirationTime) { 18 | const currentTime = new Date().getTime(); 19 | 20 | if (currentTime > expirationTime) { 21 | dispatch(logout()); 22 | } 23 | } 24 | }, [dispatch]); 25 | 26 | return ( 27 | <> 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |