├── Deployment
└── Terraform
│ ├── scripts
│ └── deploy.sh
│ ├── modules
│ ├── artifacts
│ │ ├── output.tf
│ │ ├── variable.tf
│ │ └── main.tf
│ ├── compute
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── database
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── monitoring
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── load_balancers
│ │ ├── outputs.tf
│ │ ├── variables.tf
│ │ └── main.tf
│ ├── cicd
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── security
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── network
│ │ ├── outputs.tf
│ │ └── main.tf
│ ├── terraform-dev.tfvars
│ ├── terraform-prod.tfvars
│ ├── terraform-stage.tfvars
│ ├── provider.tf
│ ├── Terraform.tf
│ ├── outputs.tf
│ ├── variables.tf
│ ├── beckend.tf
│ ├── file_structure.txt
│ └── main.tf
├── server
├── Procfile
├── __tests__
│ └── controllers
│ │ ├── product.test.js
│ │ └── auth.test.js
├── .dockerignore
├── .gitignore
├── .prettierrc
├── Dockerfile
├── Dockerfile.dev
├── routes
│ ├── payment.js
│ ├── order.js
│ ├── users.js
│ ├── index.js
│ ├── auth.js
│ ├── cart.js
│ └── product.js
├── middleware
│ ├── unKnownEndpoint.js
│ ├── verifyAdmin.js
│ └── verifyToken.js
├── utils
│ └── logger.js
├── docs
│ ├── orders
│ │ ├── index.js
│ │ ├── getOrders.js
│ │ ├── createOrder.js
│ │ └── getOrder.js
│ ├── servers.js
│ ├── index.js
│ ├── paths.js
│ ├── users
│ │ ├── index.js
│ │ ├── delete-user.js
│ │ ├── create-user.js
│ │ ├── get-users.js
│ │ ├── get-user.js
│ │ └── update-user.js
│ ├── basicInfo.js
│ ├── products
│ │ ├── index.js
│ │ ├── updateProduct.js
│ │ ├── getProducts.js
│ │ ├── deleteProduct.js
│ │ ├── getProduct.js
│ │ └── createProduct.js
│ ├── tags.js
│ ├── cart
│ │ ├── index.js
│ │ ├── getCart.js
│ │ ├── addItem.js
│ │ ├── removeItem.js
│ │ ├── decrease.js
│ │ └── increase.js
│ └── auth
│ │ ├── index.js
│ │ ├── signup.js
│ │ ├── forgotPassword.js
│ │ ├── check-token.js
│ │ ├── login.js
│ │ ├── reset-password.js
│ │ ├── refresh-token.js
│ │ └── googleLogin.js
├── helpers
│ ├── validateUser.js
│ ├── test_helper.js
│ ├── hashPassword.js
│ └── error.js
├── controllers
│ ├── payment.controller.js
│ ├── orders.controller.js
│ ├── cart.controller.js
│ ├── users.controller.js
│ ├── auth.controller.js
│ └── products.controller.js
├── index.js
├── .eslintrc.js
├── services
│ ├── payment.service.js
│ ├── order.service.js
│ ├── cart.service.js
│ ├── product.service.js
│ └── user.service.js
├── config
│ └── index.js
├── app.js
├── .env.example
├── db
│ ├── auth.db.js
│ ├── review.db.js
│ ├── orders.db.js
│ ├── product.db.js
│ ├── user.db.js
│ └── cart.db.js
└── package.json
├── client
├── public
│ ├── _redirects
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── site.webmanifest
│ ├── manifest.json
│ └── sitemap.xml
├── .eslintignore
├── .dockerignore
├── src
│ ├── components
│ │ ├── index.jsx
│ │ ├── GlobalHistory.jsx
│ │ ├── Spinner.jsx
│ │ ├── OrderItem.jsx
│ │ ├── OrderSummary.jsx
│ │ ├── ReviewCard.jsx
│ │ ├── CartItem.jsx
│ │ ├── PaystackBtn.jsx
│ │ ├── Product.jsx
│ │ ├── ForgotPasswordModal.jsx
│ │ ├── PaymentForm.jsx
│ │ └── ReviewModal.jsx
│ ├── index.css
│ ├── helpers
│ │ ├── history.js
│ │ ├── formatCurrency.js
│ │ ├── WithAxios.js
│ │ └── localStorage.js
│ ├── services
│ │ ├── product.service.js
│ │ ├── order.service.js
│ │ ├── cart.service.js
│ │ ├── review.service.js
│ │ └── auth.service.js
│ ├── routes
│ │ └── protected.route.jsx
│ ├── api
│ │ └── axios.config.js
│ ├── context
│ │ ├── OrderContext.jsx
│ │ ├── ReviewContext.jsx
│ │ ├── ProductContext.jsx
│ │ ├── UserContext.jsx
│ │ └── CartContext.jsx
│ ├── pages
│ │ ├── index.jsx
│ │ ├── 404.jsx
│ │ ├── ProductList.jsx
│ │ ├── Checkout.jsx
│ │ ├── Confirmation.jsx
│ │ ├── OrderDetails.jsx
│ │ ├── Cart.jsx
│ │ ├── Orders.jsx
│ │ └── ProductDetails.jsx
│ ├── index.jsx
│ ├── App.jsx
│ └── layout
│ │ └── Layout.jsx
├── postcss.config.js
├── jsconfig.json
├── Dockerfile.dev
├── .prettierrc
├── Dockerfile
├── .env.example
├── sonar.properties
├── tailwind.config.js
├── .gitignore
├── .eslintrc.json
├── vite.config.js
├── index.html
├── package.json
├── jenkinsfile
└── README.md
├── .github
└── dependabot.yml
├── .gitignore
├── package.json
├── sonar.properties
├── docker-compose.yml
└── docker-compose.dev.yml
/Deployment/Terraform/scripts/deploy.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
2 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/artifacts/output.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/compute/main.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/compute/outputs.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/database/main.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/database/outputs.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/monitoring/main.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/__tests__/controllers/product.test.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/compute/variables.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/database/variables.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/monitoring/outputs.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/monitoring/variables.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public
3 | dist
4 | **/*.output.*
5 |
--------------------------------------------------------------------------------
/client/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .gitignore
3 | node_modules
4 | Dockerfile
5 |
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .gitignore
3 | node_modules
4 | Dockerfile
5 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | .vscode
4 | request.http
5 | *.rest
6 |
--------------------------------------------------------------------------------
/client/src/components/index.jsx:
--------------------------------------------------------------------------------
1 | export { AccountForm as default } from "./AccountForm";
2 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/logo512.png
--------------------------------------------------------------------------------
/client/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/favicon-16x16.png
--------------------------------------------------------------------------------
/client/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/favicon-32x32.png
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/load_balancers/outputs.tf:
--------------------------------------------------------------------------------
1 | output "sonar_lb_dns" {
2 | value = aws_lb.sonar-lb.dns_name
3 | }
--------------------------------------------------------------------------------
/client/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/client/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/client/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhatGuy/PERN-Store/HEAD/client/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/Deployment/Terraform/terraform-dev.tfvars:
--------------------------------------------------------------------------------
1 | environment = "dev"
2 | ami="ami-0866a3c8686eaeeba"
3 | key_name = "rb"
4 | instance_type = "t2.micro"
5 |
--------------------------------------------------------------------------------
/Deployment/Terraform/terraform-prod.tfvars:
--------------------------------------------------------------------------------
1 | environment = "prod"
2 | ami="ami-0866a3c8686eaeeba"
3 | key_name = "rb"
4 | instance_type = "t2.micro"
5 |
--------------------------------------------------------------------------------
/Deployment/Terraform/terraform-stage.tfvars:
--------------------------------------------------------------------------------
1 | environment = "stage"
2 | ami="ami-0866a3c8686eaeeba"
3 | key_name = "rb"
4 | instance_type = "t2.micro"
5 |
--------------------------------------------------------------------------------
/client/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"],
6 | "exclude": ["node_modules"]
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/helpers/history.js:
--------------------------------------------------------------------------------
1 | const history = {
2 | navigate: null,
3 | push: (page, ...rest) => History.navigate(page, ...rest),
4 | };
5 |
6 | export default history;
7 |
--------------------------------------------------------------------------------
/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "printWidth": 80,
5 | "singleQuote": false,
6 | "trailingComma": "es5",
7 | "endOfLine": "lf"
8 | }
9 |
--------------------------------------------------------------------------------
/Deployment/Terraform/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = "5.76.0"
6 | }
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/client/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY . .
6 |
7 | RUN npm i --no-audit || npm i --no-audit --maxsockets 1
8 |
9 | EXPOSE 3000
10 |
11 | CMD [ "yarn", "start" ]
12 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY . .
6 |
7 | RUN npm ci --no-audit || npm ci --no-audit --maxsockets 1
8 |
9 | EXPOSE 9000
10 |
11 | CMD [ "npm", "start" ]
12 |
--------------------------------------------------------------------------------
/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "printWidth": 100,
5 | "singleQuote": false,
6 | "trailingComma": "es5",
7 | "endOfLine": "auto",
8 | "bracketSameLine": false
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/helpers/formatCurrency.js:
--------------------------------------------------------------------------------
1 | export const formatCurrency = (amount) => {
2 | return new Intl.NumberFormat("en-NG", {
3 | style: "currency",
4 | currency: "NGN",
5 | }).format(amount);
6 | };
7 |
--------------------------------------------------------------------------------
/server/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY . .
6 |
7 | RUN npm i --no-audit || npm i --no-audit --maxsockets 1
8 |
9 | EXPOSE 9000
10 |
11 | CMD [ "npm", "run", "dev" ]
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.env
3 | *.vscode
4 | client/.eslintcache
5 |
6 | # Local Netlify folder
7 | .netlify
8 | db
9 | dist
10 | client/public/sitemap.xml
11 | .terraform/
12 | .terraform.lock.hcl
13 |
--------------------------------------------------------------------------------
/server/routes/payment.js:
--------------------------------------------------------------------------------
1 | const { makePayment } = require("../controllers/payment.controller");
2 |
3 | const router = require("express").Router();
4 |
5 | router.route("/").post(makePayment);
6 |
7 | module.exports = router;
8 |
--------------------------------------------------------------------------------
/client/src/components/GlobalHistory.jsx:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/70002872/11885780
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export const GlobalHistory = () => {
5 | History.navigate = useNavigate();
6 |
7 | return null;
8 | };
9 |
--------------------------------------------------------------------------------
/Deployment/Terraform/Terraform.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | bucket = "pern-store-s3-bucket-777"
4 | dynamodb_table = "state-lock"
5 | key = "Terraform/dev/terraform.tfstate"
6 | region = "us-east-1"
7 | encrypt = true
8 | }
9 | }
--------------------------------------------------------------------------------
/server/middleware/unKnownEndpoint.js:
--------------------------------------------------------------------------------
1 | const { ErrorHandler } = require("../helpers/error");
2 |
3 | // eslint-disable-next-line no-unused-vars
4 | const unknownEndpoint = (request, response) => {
5 | throw new ErrorHandler(401, "unknown endpoint");
6 | };
7 |
8 | module.exports = unknownEndpoint;
9 |
--------------------------------------------------------------------------------
/server/utils/logger.js:
--------------------------------------------------------------------------------
1 | const pino = require("pino");
2 |
3 | // Create a logging instance
4 | const logger = pino({
5 | level: process.env.NODE_ENV === "production" ? "info" : "debug",
6 | prettyPrint: process.env.NODE_ENV !== "production",
7 | });
8 |
9 | module.exports.logger = logger;
10 |
--------------------------------------------------------------------------------
/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY . .
6 |
7 | RUN npm ci --no-audit --legacy-peer-deps || npm ci --no-audit --legacy-peer-deps --maxsockets 1
8 |
9 | RUN npm run build
10 |
11 | RUN npm install -g serve
12 |
13 | EXPOSE 3000
14 |
15 | CMD ["serve", "dist"]
16 |
--------------------------------------------------------------------------------
/Deployment/Terraform/outputs.tf:
--------------------------------------------------------------------------------
1 | output "public_ip_Basiton_host" {
2 | value = module.network.public_ip_Basiton_host
3 | }
4 |
5 | output "Basiton_Instance_Id" {
6 | value = module.network.Basiton_Instance_Id
7 | }
8 |
9 | output "sonar_lb_dns" {
10 | value = module.load_balancer.sonar_lb_dns
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/client/.env.example:
--------------------------------------------------------------------------------
1 | VITE_API_URL=pern-store-server:9000
2 | VITE_GOOGLE_CLIENT_ID=893689195365-q5bgpgnofu5184jq0r4nu5869f53j6i4.apps.googleusercontent.com
3 | VITE_GOOGLE_CLIENT_SECRET=43EJjaP7mnyXWH8wy3ZFCR2i
4 | VITE_STRIPE_PUB_KEY=pk_test_uuiduw984x4h4xx41489j94n
5 | VITE_PAYSTACK_PUB_KEY=pk_test_uuiduw984x4h4xx41489j94n
--------------------------------------------------------------------------------
/server/docs/orders/index.js:
--------------------------------------------------------------------------------
1 | const getOrder = require("./getOrder");
2 | const getOrders = require("./getOrders");
3 | const createOrder = require("./createOrder");
4 |
5 | module.exports = {
6 | "/orders": {
7 | ...getOrders,
8 | ...createOrder,
9 | },
10 | "/orders/{id}": {
11 | ...getOrder,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/server/docs/servers.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | servers: [
3 | {
4 | url: "https://nameless-journey-88760.herokuapp.com/api", // url
5 | description: "Production server", // name
6 | },
7 | {
8 | url: "http://localhost:9000/api", // url
9 | description: "Local server", // name
10 | },
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/server/helpers/validateUser.js:
--------------------------------------------------------------------------------
1 | const validateUser = (email, password) => {
2 | const validEmail = typeof email === "string" && email.trim() !== "";
3 | const validPassword =
4 | typeof password === "string" && password.trim().length >= 6;
5 |
6 | return validEmail && validPassword;
7 | };
8 |
9 | module.exports = validateUser;
10 |
--------------------------------------------------------------------------------
/server/controllers/payment.controller.js:
--------------------------------------------------------------------------------
1 | const paymentService = require("../services/payment.service");
2 |
3 | const makePayment = async (req, res) => {
4 | const { email, amount } = req.body;
5 |
6 | const result = await paymentService.payment(amount, email);
7 | res.json(result);
8 | };
9 |
10 | module.exports = {
11 | makePayment,
12 | };
13 |
--------------------------------------------------------------------------------
/server/docs/index.js:
--------------------------------------------------------------------------------
1 | const basicInfo = require("./basicInfo");
2 | const servers = require("./servers");
3 | const components = require("./components");
4 | const tags = require("./tags");
5 | const paths = require("./paths");
6 |
7 | module.exports = {
8 | ...basicInfo,
9 | ...servers,
10 | ...components,
11 | ...tags,
12 | ...paths,
13 | };
14 |
--------------------------------------------------------------------------------
/server/docs/paths.js:
--------------------------------------------------------------------------------
1 | const auth = require("./auth");
2 | const users = require("./users");
3 | const products = require("./products");
4 | const orders = require("./orders");
5 | const cart = require("./cart");
6 |
7 | module.exports = {
8 | paths: {
9 | ...auth,
10 | ...users,
11 | ...products,
12 | ...orders,
13 | ...cart,
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config({ path: __dirname + "/.env" });
2 | const http = require("http");
3 | const app = require("./app");
4 | const { logger } = require("./utils/logger");
5 |
6 | const server = http.createServer(app);
7 |
8 | const PORT = process.env.PORT || 8080;
9 |
10 | server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`));
11 |
--------------------------------------------------------------------------------
/client/sonar.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=PERN_Store
2 | sonar.projectName=PERN_STORE
3 | sonar.projectVersion=1.0
4 | sonar.sources=.
5 | sonar.sourceEncoding=UTF-8
6 | sonar.host.url=http://3.84.152.110:9000
7 | sonar.login=${squ_a7a6a41ed1e37ae89ad8165661a5d65ad9502558}
8 | sonar.exclusions=**/node_modules/**,coverage/lcov-report/*,test/*.js
9 | sonar.javascript.lcov.reportPaths=coverage/lcov.info
10 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: "class",
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{js,ts,jsx,tsx}",
7 | "node_modules/@windmill/react-ui/lib/defaultTheme.js",
8 | "node_modules/@windmill/react-ui/dist/index.js",
9 | ],
10 | theme: {
11 | extend: {},
12 | },
13 | plugins: [],
14 | };
15 |
--------------------------------------------------------------------------------
/server/helpers/test_helper.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 |
3 | const usersInDb = async () => {
4 | const users = await pool.query("SELECT * FROM USERS");
5 | return users.rows;
6 | };
7 |
8 | const productsInDb = async () => {
9 | const products = await pool.query("SELECT * FROM products");
10 | return products.rows;
11 | };
12 |
13 | module.exports = { usersInDb, productsInDb };
14 |
--------------------------------------------------------------------------------
/server/middleware/verifyAdmin.js:
--------------------------------------------------------------------------------
1 | const { ErrorHandler } = require("../helpers/error");
2 |
3 | module.exports = (req, res, next) => {
4 | const { roles } = req.user;
5 | if (roles && roles.includes("admin")) {
6 | req.user = {
7 | ...req.user,
8 | roles,
9 | };
10 | return next();
11 | } else {
12 | throw new ErrorHandler(401, "require admin role");
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/components/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import ClipLoader from "react-spinners/ClipLoader";
2 |
3 | const Spinner = ({ css, size, loading }) => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default Spinner;
12 |
--------------------------------------------------------------------------------
/client/src/services/product.service.js:
--------------------------------------------------------------------------------
1 | import API from "api/axios.config";
2 |
3 | class ProductService {
4 | getProducts(page) {
5 | return API.get(`/products/?page=${page}`);
6 | }
7 | getProduct(id) {
8 | return API.get(`/products/${id}`);
9 | }
10 | getProductByName(name) {
11 | return API.get(`/products/${name}`);
12 | }
13 | }
14 |
15 | export default new ProductService();
16 |
--------------------------------------------------------------------------------
/server/helpers/hashPassword.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcrypt");
2 |
3 | const hashPassword = async (password) => {
4 | const salt = await bcrypt.genSalt();
5 | const hashedPassword = await bcrypt.hash(password, salt);
6 | return hashedPassword;
7 | };
8 |
9 | const comparePassword = async (password, passwordHash) =>
10 | await bcrypt.compare(password, passwordHash);
11 |
12 | module.exports = { hashPassword, comparePassword };
13 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/artifacts/variable.tf:
--------------------------------------------------------------------------------
1 | variable "ami" {
2 | type = string
3 | }
4 |
5 | variable "instance_type" {
6 | type = string
7 | default = "t2.medium"
8 | }
9 |
10 | variable "enviroment" {
11 | type = string
12 |
13 | }
14 |
15 | variable "nexus_subnet" {
16 | type = string
17 | }
18 |
19 | variable "nexus_sg" {
20 | type = string
21 | }
22 |
23 |
24 | variable "key_name" {
25 | type = string
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/cicd/outputs.tf:
--------------------------------------------------------------------------------
1 | output "sonar_public_ip" {
2 | value = aws_instance.sonarqube_instance.public_ip
3 | description = "it will be used for the ui access of sonarqube "
4 | }
5 |
6 | output "sonar_id" {
7 | value = aws_instance.sonarqube_instance.id
8 | description = "it will be used while creating the ssh keys "
9 | }
10 |
11 |
12 | output "sonar_target_instance" {
13 | value = aws_instance.sonarqube_instance.id
14 | }
15 |
--------------------------------------------------------------------------------
/server/docs/users/index.js:
--------------------------------------------------------------------------------
1 | const getUser = require("./get-user");
2 | const updateUser = require("./update-user");
3 | const deleteUser = require("./delete-user");
4 | const createUser = require("./create-user");
5 | const getUsers = require("./get-users");
6 |
7 | module.exports = {
8 | "/users": {
9 | ...getUsers,
10 | ...createUser,
11 | },
12 | "/users/{id}": {
13 | ...getUser,
14 | ...updateUser,
15 | ...deleteUser,
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | src/tailwind.output.css
27 |
--------------------------------------------------------------------------------
/server/routes/order.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const {
3 | getOrder,
4 | getAllOrders,
5 | createOrder,
6 | } = require("../controllers/orders.controller");
7 | const verifyToken = require("../middleware/verifyToken");
8 |
9 | router.route("/create").post(verifyToken, createOrder);
10 |
11 | router.route("/").get(verifyToken, getAllOrders);
12 |
13 | router.route("/:id").get(verifyToken, getOrder);
14 |
15 | module.exports = router;
16 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/load_balancers/variables.tf:
--------------------------------------------------------------------------------
1 | variable "enviroment" {
2 | description = "env name"
3 | type = string
4 | }
5 |
6 | variable "sonar_alb_sg" {
7 | description = "security group for sonar alb "
8 | type = string
9 | }
10 |
11 | variable "sonar_subnet_ids" {
12 | type = list(string)
13 | }
14 |
15 | variable "igw_id" {
16 | type = string
17 | }
18 |
19 | variable "vpc_id" {
20 | type = string
21 | }
22 |
23 | variable "sonar_target_instance" {
24 | type = string
25 | }
--------------------------------------------------------------------------------
/client/src/routes/protected.route.jsx:
--------------------------------------------------------------------------------
1 | import { useUser } from "context/UserContext";
2 | import { Navigate, Outlet, useLocation } from "react-router-dom";
3 |
4 | export const ProtectedRoute = ({ redirectPath = "/login", children }) => {
5 | const { isLoggedIn } = useUser();
6 | const location = useLocation();
7 |
8 | if (!isLoggedIn) {
9 | return ;
10 | }
11 |
12 | return children ? children : ;
13 | };
14 |
--------------------------------------------------------------------------------
/server/docs/basicInfo.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | openapi: "3.0.3", // present supported openapi version
3 | info: {
4 | title: "PERN-Store", // short title.
5 | description: "A REST E-commerce API made with Express and Postgresql.", // desc.
6 | version: "1.0.0", // version number
7 | contact: {
8 | name: "Joseph Odunsi", // your name
9 | email: "odunsiolakunbi@gmail.com", // your email
10 | url: "https://github.com/dhatguy", // your website
11 | },
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/server/docs/products/index.js:
--------------------------------------------------------------------------------
1 | const getProduct = require("./getProduct");
2 | const getProducts = require("./getProducts");
3 | const createProduct = require("./createProduct");
4 | const updateProduct = require("./updateProduct");
5 | const deleteProduct = require("./deleteProduct");
6 |
7 | module.exports = {
8 | "/products": {
9 | ...getProducts,
10 | ...createProduct,
11 | },
12 | "/products/{id}": {
13 | ...getProduct,
14 | ...updateProduct,
15 | ...deleteProduct,
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/client/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PERN Store",
3 | "short_name": "PERN Store E-Commerce App",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/server/docs/tags.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tags: [
3 | {
4 | name: "Auth",
5 | description: "API for authentication",
6 | },
7 | {
8 | name: "Users",
9 | description: "API for users",
10 | },
11 | {
12 | name: "Products",
13 | description: "API for products",
14 | },
15 | {
16 | name: "Orders",
17 | description: "API for orders",
18 | },
19 | {
20 | name: "Cart",
21 | description: "API for cart",
22 | },
23 | ],
24 | };
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pern-store",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "dependencies": {
6 | "concurrently": "^7.6.0"
7 | },
8 | "devDependencies": {},
9 | "scripts": {
10 | "start": "node server",
11 | "server": "cd server && nodemon",
12 | "client": "npm start --prefix client",
13 | "dev": "concurrently \"npm run server\" \"npm run client\""
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "description": ""
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/services/order.service.js:
--------------------------------------------------------------------------------
1 | import API from "api/axios.config";
2 |
3 | class OrderService {
4 | createOrder(amount, itemTotal, ref, paymentMethod) {
5 | return API.post("/orders/create", {
6 | amount,
7 | itemTotal,
8 | ref,
9 | paymentMethod,
10 | });
11 | }
12 | getAllOrders(page) {
13 | return API.get(`/orders/?page=${page}`);
14 | }
15 | getOrder(id) {
16 | return API.get(`/orders/${id}`);
17 | }
18 | }
19 |
20 | export default new OrderService();
21 |
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commonjs: true,
4 | es2021: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: ["eslint:recommended"],
9 | parserOptions: {
10 | ecmaVersion: 12,
11 | },
12 | parser: "babel-eslint",
13 | plugins: ["babel", "prettier"],
14 | rules: {
15 | "no-console": "warn",
16 | eqeqeq: "error",
17 | // "object-curly-spacing": ["error", "always"],
18 | // "arrow-spacing": ["error", { before: true, after: true }],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/server/docs/cart/index.js:
--------------------------------------------------------------------------------
1 | const getCart = require("./getCart");
2 | const addItem = require("./addItem");
3 | const decrease = require("./decrease");
4 | const increase = require("./increase");
5 | const removeItem = require("./removeItem");
6 |
7 | module.exports = {
8 | "/cart": {
9 | ...getCart,
10 | },
11 | "/cart/add": {
12 | ...addItem,
13 | },
14 | "cart/increment": {
15 | ...increase,
16 | },
17 | "cart/decrement": {
18 | ...decrease,
19 | },
20 | "/cart/delete": {
21 | ...removeItem,
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/security/outputs.tf:
--------------------------------------------------------------------------------
1 | output "bastion_sg_id" {
2 | value = aws_security_group.bastion_sg.id
3 | }
4 |
5 | output "bastion_ssm_role_name" {
6 | value = aws_iam_role.bastion_ssm_role.name
7 | }
8 | output "jenkins_sg" {
9 | value = aws_security_group.jenkins_sg.id
10 | }
11 |
12 | output "sonar_sg_id" {
13 | value = aws_security_group.sonar_sg.id
14 | }
15 |
16 | output "sonar_alb_sg" {
17 | value = aws_security_group.sonar_alb_sg.id
18 | }
19 |
20 |
21 | output "nexus_sg" {
22 | value = aws_security_group.nexus_sg.id
23 | }
24 |
--------------------------------------------------------------------------------
/sonar.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=Pern-Store
2 | sonar.projectName=Pern-Store
3 | sonar.projectVersion=1.0
4 | sonar.sources=./client
5 | sonar.sourceEncoding=UTF-8
6 | sonar.host.url=http://3.84.152.110:9000/
7 | sonar.login=squ_2d0a20cec794ceec96af6bd63d29d505add0c229
8 | # Exclude non-Node.js files, such as tests, build directories, node_modules, CSS, HTML, etc.
9 | sonar.exclusions=**/node_modules/**,**/test/**
10 | # Enable JavaScript analysis (for Node.js)
11 | sonar.language=js # or 'ts' for TypeScript projects
12 |
13 | sonar.javascript.lcov.reportPaths=coverage/lcov.info
--------------------------------------------------------------------------------
/server/middleware/verifyToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const { ErrorHandler } = require("../helpers/error");
3 |
4 | const verifyToken = (req, res, next) => {
5 | const token = req.header("auth-token");
6 | if (!token) {
7 | throw new ErrorHandler(401, "Token missing");
8 | }
9 |
10 | try {
11 | const verified = jwt.verify(token, process.env.SECRET);
12 | req.user = verified;
13 | next();
14 | } catch (error) {
15 | throw new ErrorHandler(401, error.message || "Invalid Token");
16 | }
17 | };
18 |
19 | module.exports = verifyToken;
20 |
--------------------------------------------------------------------------------
/client/src/api/axios.config.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const baseURL = import.meta.env.PROD ? import.meta.env.VITE_API_URL : "http://localhost:9000/api";
4 |
5 | const API = axios.create({
6 | baseURL,
7 | withCredentials: true,
8 | });
9 |
10 | API.interceptors.request.use(
11 | function (req) {
12 | const token = JSON.parse(localStorage.getItem("token"));
13 | if (token) req.headers["auth-token"] = token;
14 | return req;
15 | },
16 | function (error) {
17 | return Promise.reject(error);
18 | }
19 | );
20 |
21 | export default API;
22 |
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "settings": {
8 | "react": {
9 | "version": "detect"
10 | }
11 | },
12 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "ecmaVersion": "latest",
18 | "sourceType": "module"
19 | },
20 | "plugins": ["react"],
21 | "rules": {
22 | "react/prop-types": "off",
23 | "react/react-in-jsx-scope": "off"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/services/payment.service.js:
--------------------------------------------------------------------------------
1 | const Stripe = require("stripe");
2 | const { ErrorHandler } = require("../helpers/error");
3 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
4 |
5 | class PaymentService {
6 | payment = async (amount, email) => {
7 | try {
8 | return await stripe.paymentIntents.create({
9 | amount,
10 | currency: "ngn",
11 | payment_method_types: ["card"],
12 | receipt_email: email,
13 | });
14 | } catch (error) {
15 | throw new ErrorHandler(error.statusCode, error.message);
16 | }
17 | };
18 | }
19 |
20 | module.exports = new PaymentService();
21 |
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | const {
2 | getAllUsers,
3 | createUser,
4 | deleteUser,
5 | getUserById,
6 | updateUser,
7 | getUserProfile,
8 | } = require("../controllers/users.controller");
9 | const router = require("express").Router();
10 | const verifyAdmin = require("../middleware/verifyAdmin");
11 | const verifyToken = require("../middleware/verifyToken");
12 |
13 | router.use(verifyToken);
14 | router.route("/").get(verifyAdmin, getAllUsers).post(verifyAdmin, createUser);
15 | router.route("/profile").get(getUserProfile);
16 | router.route("/:id").get(getUserById).put(updateUser).delete(deleteUser);
17 |
18 | module.exports = router;
19 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "PERN Store",
3 | "name": "PERN Store E-Commerce App",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "android-chrome-192x192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "android-chrome-512x512.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 |
--------------------------------------------------------------------------------
/client/src/context/OrderContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from "react";
2 |
3 | const OrderContext = createContext();
4 |
5 | const OrderProvider = ({ children }) => {
6 | const [orders, setOrders] = useState(null);
7 |
8 | return {children};
9 | };
10 |
11 | const useOrders = () => {
12 | const context = useContext(OrderContext);
13 | if (context === undefined) {
14 | throw new Error("useOrders must be used within a OrderProvider");
15 | }
16 | return context;
17 | };
18 |
19 | export { OrderProvider, useOrders };
20 |
--------------------------------------------------------------------------------
/client/src/context/ReviewContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from "react";
2 |
3 | const ReviewContext = createContext();
4 |
5 | const ReviewProvider = ({ children }) => {
6 | const [reviews, setReviews] = useState(null);
7 | return (
8 | {children}
9 | );
10 | };
11 |
12 | const useReview = () => {
13 | const context = useContext(ReviewContext);
14 |
15 | if (context === undefined) {
16 | throw new Error("useReview must be used within ReviewProvider");
17 | }
18 | return context;
19 | };
20 |
21 | export { ReviewProvider, useReview };
22 |
--------------------------------------------------------------------------------
/client/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | export { default as NotFound } from "./404";
2 | export { default as Account } from "./Account";
3 | export { default as Cart } from "./Cart";
4 | export { default as Checkout } from "./Checkout";
5 | export { default as Confirmation } from "./Confirmation";
6 | export { default as Login } from "./Login";
7 | export { default as OrderDetails } from "./OrderDetails";
8 | export { default as Orders } from "./Orders";
9 | export { default as ProductDetails } from "./ProductDetails";
10 | export { default as ProductList } from "./ProductList";
11 | export { default as Register } from "./Register";
12 | export { default as ResetPassword } from "./ResetPassword";
13 |
--------------------------------------------------------------------------------
/server/helpers/error.js:
--------------------------------------------------------------------------------
1 | const { logger } = require("../utils/logger");
2 | class ErrorHandler extends Error {
3 | constructor(statusCode, message) {
4 | super();
5 | this.status = "error";
6 | this.statusCode = statusCode;
7 | this.message = message;
8 | }
9 | }
10 |
11 | const handleError = (err, req, res, next) => {
12 | const { statusCode, message } = err;
13 | logger.error(err);
14 | res.status(statusCode || 500).json({
15 | status: "error",
16 | statusCode: statusCode || 500,
17 | message: statusCode === 500 ? "An error occurred" : message,
18 | });
19 | next();
20 | };
21 | module.exports = {
22 | ErrorHandler,
23 | handleError,
24 | };
25 |
--------------------------------------------------------------------------------
/Deployment/Terraform/variables.tf:
--------------------------------------------------------------------------------
1 | variable "region" {
2 | type = string
3 | description = "default region will be taken "
4 | default = "us-east-1"
5 | }
6 |
7 | variable "environment" {
8 | type = string
9 | description = "Different for every env "
10 | }
11 |
12 | variable "vpc_cidr" {
13 | description = "CIDR blocks for VPCs in different environments"
14 | type = map(string)
15 | default = {
16 | dev = "10.0.0.0/16"
17 | stage = "10.1.0.0/16"
18 | prod = "10.2.0.0/16"
19 | }
20 | }
21 |
22 | variable "ami" {
23 | type = string
24 | }
25 |
26 | variable "instance_type" {
27 | type = string
28 | }
29 | variable "key_name" {
30 | type = string
31 |
32 | }
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const cart = require("./cart");
3 | const order = require("./order");
4 | const product = require("./product");
5 | const users = require("./users");
6 | const auth = require("./auth");
7 | const payment = require("./payment");
8 | const swaggerUi = require("swagger-ui-express");
9 | const docs = require("../docs");
10 |
11 | router.use("/auth", auth);
12 | router.use("/users", users);
13 | router.use("/products", product);
14 | router.use("/orders", order);
15 | router.use("/cart", cart);
16 | router.use("/payment", payment);
17 | router.use("/docs", swaggerUi.serve, swaggerUi.setup(docs));
18 |
19 | module.exports = router;
20 |
--------------------------------------------------------------------------------
/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const {
3 | createAccount,
4 | loginUser,
5 | googleLogin,
6 | forgotPassword,
7 | verifyResetToken,
8 | resetPassword,
9 | refreshToken,
10 | } = require("../controllers/auth.controller");
11 |
12 | router.post("/signup", createAccount);
13 |
14 | router.post("/login", loginUser);
15 |
16 | router.post("/google", googleLogin);
17 |
18 | router.post("/forgot-password", forgotPassword);
19 |
20 | // token for reset password
21 | router.post("/check-token", verifyResetToken);
22 |
23 | router.post("/reset-password", resetPassword);
24 |
25 | router.post("/refresh-token", refreshToken);
26 |
27 | module.exports = router;
28 |
--------------------------------------------------------------------------------
/client/src/components/OrderItem.jsx:
--------------------------------------------------------------------------------
1 | import { Badge, TableCell } from "@windmill/react-ui";
2 | import { format, parseISO } from "date-fns";
3 | import { formatCurrency } from "helpers/formatCurrency";
4 |
5 | const OrderItem = ({ order }) => {
6 | return (
7 | <>
8 | #{order.order_id}
9 | {order.total || "Not available"}
10 |
11 | {order.status}{" "}
12 |
13 | {formatCurrency(order.amount)}
14 | {format(parseISO(order.date), "dd/MM/yy")}
15 | >
16 | );
17 | };
18 |
19 | export default OrderItem;
20 |
--------------------------------------------------------------------------------
/client/src/services/cart.service.js:
--------------------------------------------------------------------------------
1 | import API from "../api/axios.config";
2 |
3 | class CartService {
4 | getCart() {
5 | return API.get("/cart");
6 | }
7 | async addToCart(product_id, quantity) {
8 | return await API.post("/cart/add", { product_id, quantity });
9 | }
10 |
11 | async removeFromCart(product_id) {
12 | return await API.delete("/cart/delete", {
13 | data: { product_id: Number(product_id) },
14 | });
15 | }
16 |
17 | async increment(product_id) {
18 | return API.put("/cart/increment", { product_id });
19 | }
20 |
21 | async decrement(product_id) {
22 | return API.put("/cart/decrement", { product_id });
23 | }
24 | }
25 |
26 | export default new CartService();
27 |
--------------------------------------------------------------------------------
/server/routes/cart.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const verifyToken = require("../middleware/verifyToken");
3 | const {
4 | getCart,
5 | addItem,
6 | deleteItem,
7 | increaseItemQuantity,
8 | decreaseItemQuantity,
9 | } = require("../controllers/cart.controller");
10 |
11 | router.use(verifyToken);
12 | // get cart items
13 | router.route("/").get(getCart);
14 |
15 | // add item to cart
16 | router.route("/add").post(addItem);
17 |
18 | // delete item from cart
19 | router.route("/delete").delete(deleteItem);
20 |
21 | // increment item quantity
22 | router.route("/increment").put(increaseItemQuantity);
23 |
24 | // decrement item quantity
25 | router.route("/decrement").put(decreaseItemQuantity);
26 |
27 | module.exports = router;
28 |
--------------------------------------------------------------------------------
/server/config/index.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const { Pool } = require("pg");
3 |
4 | const isProduction = process.env.NODE_ENV === "production";
5 | const database =
6 | process.env.NODE_ENV === "test"
7 | ? process.env.POSTGRES_DB_TEST
8 | : process.env.POSTGRES_DB;
9 |
10 | const connectionString = `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT}/${database}`;
11 | const pool = new Pool({
12 | connectionString,
13 | /*
14 | SSL is not supported in development
15 | */
16 | ssl: isProduction ? { rejectUnauthorized: false } : false,
17 | });
18 |
19 | module.exports = {
20 | query: (text, params) => pool.query(text, params),
21 | end: () => pool.end(),
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/services/review.service.js:
--------------------------------------------------------------------------------
1 | import API from "../api/axios.config";
2 |
3 | const user_id = JSON.parse(localStorage.getItem("user"))?.user_id;
4 |
5 | class ReviewService {
6 | getReviews(product_id) {
7 | return API.get("/reviews", {
8 | params: {
9 | product_id,
10 | user_id,
11 | },
12 | });
13 | }
14 | addReview(product_id, rating, content) {
15 | return API.post("/reviews", {
16 | product_id,
17 | rating,
18 | content,
19 | });
20 | }
21 |
22 | updateReview(id, product_id, content, rating) {
23 | return API.put("/reviews", {
24 | id,
25 | content,
26 | rating,
27 | product_id,
28 | });
29 | }
30 | }
31 |
32 | export default new ReviewService();
33 |
--------------------------------------------------------------------------------
/client/src/pages/404.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | function NotFound() {
4 | return (
5 |
6 |
7 |
404
8 |
9 |
Uh-oh!
10 |
11 |
We can't find that page.
12 |
13 |
17 | Go Back Home
18 |
19 |
20 |
21 | );
22 | }
23 | export default NotFound;
24 |
--------------------------------------------------------------------------------
/server/docs/products/updateProduct.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | put: {
4 | tags: ["Products"], // operation's tag
5 | description: "Update product", // short desc
6 | operationId: "updateProduct", // unique operation id
7 | summary: "Update a product",
8 | parameters: [], // expected params
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | // expected responses
15 | responses: {
16 | // response code
17 | 200: {
18 | description: "Product updated successfully", // response desc
19 | },
20 | // response code
21 | 404: {
22 | description: "Product not found",
23 | },
24 | 500: {
25 | description: "Server error", // response desc
26 | },
27 | },
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/server/docs/auth/index.js:
--------------------------------------------------------------------------------
1 | const googleLogin = require("./googleLogin");
2 | const login = require("./login");
3 | const signup = require("./signup");
4 | const checkToken = require("./check-token");
5 | const forgotPassword = require("./forgotPassword");
6 | const resetPassword = require("./reset-password");
7 | const refreshToken = require("./refresh-token");
8 |
9 | module.exports = {
10 | "/auth/login": {
11 | ...login,
12 | },
13 | "/auth/signup": {
14 | ...signup,
15 | },
16 | "/auth/google": {
17 | ...googleLogin,
18 | },
19 | "/auth/refresh-token": {
20 | ...refreshToken,
21 | },
22 | "/auth/forgot-password": {
23 | ...forgotPassword,
24 | },
25 | "/auth/check-token": {
26 | ...checkToken,
27 | },
28 | "/auth/reset-password": {
29 | ...resetPassword,
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/Deployment/Terraform/beckend.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "State_bucket" {
2 | bucket = "pern-store-s3-bucket-777"
3 |
4 | }
5 | resource "aws_s3_bucket_versioning" "version_s3" {
6 | bucket = aws_s3_bucket.State_bucket.id
7 | versioning_configuration {
8 | status = "Enabled"
9 |
10 | }
11 |
12 | }
13 |
14 | resource "aws_s3_bucket_server_side_encryption_configuration" "encryption_s3" {
15 | bucket = aws_s3_bucket.State_bucket.id
16 | rule {
17 | apply_server_side_encryption_by_default {
18 | sse_algorithm = "AES256"
19 | }
20 | }
21 |
22 | }
23 | # creating aws dynamodb for enabling state locking
24 | resource "aws_dynamodb_table" "statelocking" {
25 | name = "state-lock"
26 | billing_mode = "PAY_PER_REQUEST"
27 | hash_key = "LockID"
28 | attribute {
29 | name = "LockID"
30 | type = "S"
31 | }
32 |
33 |
34 | }
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('vite').UserConfig} */
2 | import react from "@vitejs/plugin-react";
3 | import { readdirSync } from "fs";
4 | import path from "path";
5 | import { defineConfig } from "vite";
6 |
7 | const absolutePathAliases = {};
8 | const srcPath = path.resolve("./src/");
9 | const srcRootContent = readdirSync(srcPath, { withFileTypes: true }).map((dirent) =>
10 | dirent.name.replace(/(\.js){1}(x?)/, "")
11 | );
12 |
13 | srcRootContent.forEach((directory) => {
14 | absolutePathAliases[directory] = path.join(srcPath, directory);
15 | });
16 |
17 | export default defineConfig({
18 | resolve: {
19 | alias: {
20 | ...absolutePathAliases,
21 | },
22 | },
23 | plugins: [react()],
24 | server: {
25 | watch: {
26 | usePolling: true,
27 | },
28 | host: true,
29 | strictPort: true,
30 | port: 3000,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/server/docs/products/getProducts.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // method of operation
3 | get: {
4 | tags: ["Products"], // operation's tag.
5 | description: "Get products", // operation's desc.
6 | summary: "Get all products",
7 | operationId: "getProducts", // unique operation id.
8 | parameters: [],
9 | // expected responses
10 | responses: {
11 | // response code
12 | 200: {
13 | description: "Products obtained", // response desc.
14 | content: {
15 | // content-type
16 | "application/json": {
17 | schema: {
18 | type: "array",
19 | items: {
20 | $ref: "#/components/schemas/Product",
21 | },
22 | },
23 | },
24 | },
25 | },
26 | 500: {
27 | description: "Internal server error", // response desc.
28 | },
29 | },
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | require("express-async-errors");
3 | const cors = require("cors");
4 | const morgan = require("morgan");
5 | const cookieParser = require("cookie-parser");
6 | const routes = require("./routes");
7 | const helmet = require("helmet");
8 | const compression = require("compression");
9 | const unknownEndpoint = require("./middleware/unKnownEndpoint");
10 | const { handleError } = require("./helpers/error");
11 |
12 | const app = express();
13 |
14 | app.set("trust proxy", 1);
15 | app.use(cors({ credentials: true, origin: true }));
16 | app.use(express.json());
17 | app.use(morgan("dev"));
18 | app.use(compression());
19 | app.use(helmet());
20 | app.use(cookieParser());
21 |
22 | app.use("/api", routes);
23 |
24 | app.get("/", (req, res) =>
25 | res.send("E-COMMERCE API
")
26 | );
27 | app.use(unknownEndpoint);
28 | app.use(handleError);
29 |
30 | module.exports = app;
31 |
--------------------------------------------------------------------------------
/server/docs/auth/signup.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Create an account", // short desc
6 | summary: "Signup",
7 | operationId: "signup", // unique operation id
8 | parameters: [],
9 | requestBody: {
10 | // expected request body
11 | content: {
12 | // content-type
13 | "application/json": {
14 | schema: {
15 | $ref: "#/components/schemas/SignupInput",
16 | },
17 | },
18 | },
19 | },
20 | // expected responses
21 | responses: {
22 | // response code
23 | 201: {
24 | description: "Signup successful", // response desc
25 | },
26 | 401: {
27 | description: "Input error", // response desc
28 | },
29 | // response code
30 | 500: {
31 | description: "Server error", // response desc
32 | },
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/server/docs/orders/getOrders.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | get: {
4 | tags: ["Orders"], // operation's tag
5 | description: "Get all orders", // short desc
6 | summary: "Get all orders",
7 | operationId: "getOrders", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [],
14 | // expected responses
15 | responses: {
16 | // response code
17 | 201: {
18 | description: "Orders obtained successfully", // response desc
19 | content: {
20 | // content-type
21 | "application/json": {
22 | schema: {
23 | type: "array",
24 | items: {
25 | $ref: "#/components/schemas/Order",
26 | },
27 | },
28 | },
29 | },
30 | },
31 | // response code
32 | 500: {
33 | description: "Server error", // response desc
34 | },
35 | },
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | PERN Store
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/server/routes/product.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const {
3 | getAllProducts,
4 | createProduct,
5 | getProduct,
6 | updateProduct,
7 | deleteProduct,
8 | getProductByName,
9 | getProductReviews,
10 | createProductReview,
11 | updateProductReview,
12 | getProductBySlug,
13 | } = require("../controllers/products.controller");
14 | const verifyAdmin = require("../middleware/verifyAdmin");
15 | const verifyToken = require("../middleware/verifyToken");
16 |
17 | router
18 | .route("/")
19 | .get(getAllProducts)
20 | .post(verifyToken, verifyAdmin, createProduct);
21 |
22 | router
23 | .route("/:slug")
24 | .get(getProductBySlug)
25 | // .get(getProduct)
26 | // .get(getProductByName)
27 | .put(verifyToken, verifyAdmin, updateProduct)
28 | .delete(verifyToken, verifyAdmin, deleteProduct);
29 |
30 | router
31 | .route("/:id/reviews")
32 | .get(getProductReviews)
33 | .post(verifyToken, createProductReview)
34 | .put(verifyToken, updateProductReview);
35 |
36 | module.exports = router;
37 |
--------------------------------------------------------------------------------
/server/docs/cart/getCart.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // method of operation
3 | get: {
4 | tags: ["Cart"], // operation's tag.
5 | description: "Get cart", // operation's desc.
6 | summary: "Get cart items",
7 | operationId: "getCart", // unique operation id.
8 | parameters: [], // expected params.
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | // expected responses
15 | responses: {
16 | // response code
17 | 200: {
18 | description: "Cart obtained", // response desc.
19 | content: {
20 | // content-type
21 | "application/json": {
22 | schema: {
23 | type: "array",
24 | items: {
25 | $ref: "#/components/schemas/CartItem",
26 | },
27 | },
28 | },
29 | },
30 | },
31 | 401: {
32 | description: "Unauthorized",
33 | },
34 | 500: {
35 | description: "Server error",
36 | },
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/server/docs/auth/forgotPassword.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Forgot password", // short desc
6 | summary: "Retrieve password",
7 | operationId: "forgotPassword", // unique operation id
8 | parameters: [],
9 | requestBody: {
10 | // expected request body
11 | content: {
12 | // content-type
13 | "application/json": {
14 | schema: {
15 | type: "object",
16 | properties: {
17 | email: {
18 | type: "string",
19 | },
20 | },
21 | },
22 | },
23 | },
24 | },
25 | // expected responses
26 | responses: {
27 | // response code
28 | 200: {
29 | description: "Success", // response desc
30 | },
31 | 400: {
32 | description: "Email not found", // response desc
33 | },
34 | // response code
35 | 500: {
36 | description: "Server error", // response desc
37 | },
38 | },
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/network/outputs.tf:
--------------------------------------------------------------------------------
1 | output "vpc_id" {
2 | value = aws_vpc.main.id
3 | }
4 | output "public_ip_Basiton_host" {
5 | value = aws_instance.bastion_host.public_ip
6 | }
7 | output "Basiton_Instance_Id" {
8 | value = aws_instance.bastion_host.id
9 | description = "id needed for ssm"
10 | }
11 |
12 | output "sonar-subnet_id" {
13 | value = aws_subnet.subnets["public_az_1a"].id
14 | }
15 |
16 |
17 | output "jenkins_subnet_id" {
18 | value = aws_subnet.subnets["private_az_1a"].id
19 | }
20 |
21 | output "jenkins_availablity_zone" {
22 | value = aws_subnet.subnets["private_az_1a"].availability_zone
23 | }
24 |
25 | output "nexus_subnet" {
26 | value = aws_subnet.subnets["public_az_1b"].id
27 | }
28 |
29 | output "sonar_subnet_ids" {
30 |
31 | value = [
32 | aws_subnet.subnets["public_az_1a"].id,
33 | aws_subnet.subnets["public_az_1b"].id
34 | ]
35 | }
36 |
37 |
38 |
39 | output "availability_zone" {
40 | value = aws_subnet.subnets["public_az_1a"].availability_zone
41 | }
42 |
43 | output "igw_id" {
44 | value = aws_internet_gateway.igw.id
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/server/docs/auth/check-token.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Verify password reset token", // short desc
6 | summary: "Verify password reset token",
7 | operationId: "checkToken", // unique operation id
8 | parameters: [],
9 | requestBody: {
10 | // expected request body
11 | content: {
12 | // content-type
13 | "application/json": {
14 | schema: {
15 | type: "object",
16 | properties: {
17 | token: {
18 | type: "string",
19 | },
20 | email: {
21 | type: "string",
22 | },
23 | },
24 | },
25 | },
26 | },
27 | },
28 | // expected responses
29 | responses: {
30 | // response code
31 | 200: {
32 | description: "Success", // response desc
33 | },
34 | // response code
35 | 500: {
36 | description: "Server error", // response desc
37 | },
38 | },
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/server/services/order.service.js:
--------------------------------------------------------------------------------
1 | const {
2 | createOrderDb,
3 | getAllOrdersDb,
4 | getOrderDb,
5 | } = require("../db/orders.db");
6 | const { ErrorHandler } = require("../helpers/error");
7 |
8 | class OrderService {
9 | createOrder = async (data) => {
10 | try {
11 | return await createOrderDb(data);
12 | } catch (error) {
13 | throw new ErrorHandler(error.statusCode, error.message);
14 | }
15 | };
16 |
17 | getAllOrders = async (userId, page) => {
18 | const limit = 5;
19 | const offset = (page - 1) * limit;
20 | try {
21 | return await getAllOrdersDb({ userId, limit, offset });
22 | } catch (error) {
23 | throw new ErrorHandler(error.statusCode, error.message);
24 | }
25 | };
26 |
27 | getOrderById = async (data) => {
28 | try {
29 | const order = await getOrderDb(data);
30 | if (!order) {
31 | throw new ErrorHandler(404, "Order does not exist");
32 | }
33 | return order;
34 | } catch (error) {
35 | throw new ErrorHandler(error.statusCode, error.message);
36 | }
37 | };
38 | }
39 |
40 | module.exports = new OrderService();
41 |
--------------------------------------------------------------------------------
/server/.env.example:
--------------------------------------------------------------------------------
1 | # Postgres database local connection
2 | POSTGRES_USER=postgres
3 | # Postgres host (default: localhost)
4 | POSTGRES_HOST=3.84.191.228
5 | POSTGRES_PASSWORD=newpassword
6 | POSTGRES_DB=pernstore
7 |
8 | POSTGRES_PORT=5432
9 |
10 | # Application Port - express server listens on this port (default 9000).
11 | PORT=9000
12 |
13 | # JWT access secret
14 | SECRET=secret
15 |
16 | # JWT refresh secret
17 | REFRESH_SECRET=refreshsecret
18 |
19 | # mail server settings
20 | SMTP_FROM=youremail
21 | SMTP_USER=youremail
22 |
23 | # Stripe secret key - https://stripe.com/docs/keys
24 | STRIPE_SECRET_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc
25 |
26 | # Google OAuth2.0 settings for sign in with Google - https://console.developers.google.com/
27 | OAUTH_CLIENT_ID=287280guajkxxxxxxx.apps.googleusercontent.com
28 | OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxx
29 | OAUTH_REFRESH_TOKEN=1//XXXXXXXXXX
30 |
31 | # Google OAuth2.0 settings for sending emails - https://console.developers.google.com/
32 | CLIENT_ID=938729280guajk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
33 | CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
34 | REFRESH_TOKEN=1//XXXXXXXX
35 |
--------------------------------------------------------------------------------
/client/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.pern-store.netlify.app/
4 | https://www.pern-store.netlify.app/
5 | https://www.pern-store.netlify.app/
6 | https://www.pern-store.netlify.app/products/:id
7 | https://www.pern-store.netlify.app/cart
8 | https://www.pern-store.netlify.app/profile
9 | https://www.pern-store.netlify.app/orders
10 | https://www.pern-store.netlify.app/orders/:id
11 | https://www.pern-store.netlify.app/login
12 | https://www.pern-store.netlify.app/signup
13 | https://www.pern-store.netlify.app/*
14 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/cicd/variables.tf:
--------------------------------------------------------------------------------
1 | variable "ami" {
2 | type = string
3 | description = "the ami id used for sonarqube instance"
4 | }
5 |
6 | variable "instance_type" {
7 | type = string
8 | description = "instance type used for sonarqube instance"
9 | default = "t2.micro"
10 | }
11 |
12 | variable "sonar-subnet_id" {
13 | type = string
14 | description = "subnet id where the sonarqube instance is deployed "
15 | }
16 | variable "jenkins_subnet_id" {
17 | type = string
18 | description = "subnet id where the sonarqube instance is deployed "
19 | }
20 |
21 | variable "environment" {
22 | type = string
23 | description = "env value "
24 | }
25 |
26 | variable "sonar_sg_id" {
27 | type = string
28 | description = "security group that attach to the sonarqube instance "
29 | }
30 |
31 |
32 | variable "availability_zone" {
33 | type = string
34 | description = "availablity zone for ebs of sonarqube"
35 | }
36 |
37 |
38 | variable "jenkins_sg" {
39 | type = string
40 | }
41 |
42 |
43 | variable "key_name" {
44 |
45 | type = string
46 | }
47 |
48 | variable "jenkins_availablity_zone" {
49 | type = string
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/server/docs/orders/createOrder.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Orders"], // operation's tag
5 | description: "Create an order", // short desc
6 | summary: "Place an order",
7 | operationId: "createOrder", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [],
14 | requestBody: {
15 | content: {
16 | // content-type
17 | "application/json": {
18 | schema: {
19 | $ref: "#/components/schemas/OrderInput",
20 | },
21 | },
22 | },
23 | },
24 | // expected responses
25 | responses: {
26 | // response code
27 | 201: {
28 | description: "Order created successfully", // response desc
29 | content: {
30 | // content-type
31 | "application/json": {
32 | schema: {
33 | $ref: "#/components/schemas/Order",
34 | },
35 | },
36 | },
37 | },
38 | // response code
39 | 500: {
40 | description: "Server error", // response desc
41 | },
42 | },
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/client/src/context/ProductContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 | import productService from "services/product.service";
3 |
4 | const ProductContext = createContext();
5 |
6 | const ProductProvider = ({ children }) => {
7 | const [products, setProducts] = useState(null);
8 | const [isLoading, setIsLoading] = useState(false);
9 | const [page, setPage] = useState(1);
10 |
11 | useEffect(() => {
12 | setIsLoading(true);
13 | productService.getProducts(page).then((response) => {
14 | setProducts(response.data);
15 | setIsLoading(false);
16 | });
17 | }, [page]);
18 |
19 | return (
20 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | const useProduct = () => {
29 | const context = useContext(ProductContext);
30 | if (context === undefined) {
31 | throw new Error("useProduct must be used within a ProductProvider");
32 | }
33 | return context;
34 | };
35 |
36 | export { ProductContext, ProductProvider, useProduct };
37 |
--------------------------------------------------------------------------------
/client/src/components/OrderSummary.jsx:
--------------------------------------------------------------------------------
1 | import { useCart } from "context/CartContext";
2 | import { formatCurrency } from "helpers/formatCurrency";
3 |
4 | const OrderSummary = () => {
5 | const { cartData, cartSubtotal } = useCart();
6 | return (
7 |
8 |
Order Summary
9 | {cartData?.items.map((item) => (
10 |
11 |

18 |
19 | {item.name}
20 | {formatCurrency(item.price)}
21 | Quantity: {item.quantity}
22 |
23 |
24 | ))}
25 |
Total: {formatCurrency(cartSubtotal)}
26 |
27 | );
28 | };
29 |
30 | export default OrderSummary;
31 |
--------------------------------------------------------------------------------
/server/db/auth.db.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 |
3 | const isValidTokenDb = async ({ token, email, curDate }) => {
4 | const { rows } = await pool.query(
5 | `
6 | SELECT EXISTS(select * from public."resetTokens"
7 | where token = $1 AND email = $2 AND expiration > $3 AND used = $4)
8 | `,
9 | [token, email, curDate, false]
10 | );
11 | return rows[0].exists;
12 | };
13 |
14 | const createResetTokenDb = async ({ email, expireDate, fpSalt }) => {
15 | await pool.query(
16 | 'insert into public."resetTokens" (email, expiration, token) values ($1, $2, $3)',
17 | [email, expireDate, fpSalt]
18 | );
19 |
20 | return true;
21 | };
22 |
23 | const setTokenStatusDb = async (email) => {
24 | await pool.query(
25 | 'update public."resetTokens" set used = $1 where email = $2',
26 | [true, email]
27 | );
28 |
29 | return true;
30 | };
31 |
32 | const deleteResetTokenDb = async (curDate) => {
33 | await pool.query('delete from public."resetTokens" where expiration <= $1', [
34 | curDate,
35 | ]);
36 | return true;
37 | };
38 |
39 | module.exports = {
40 | isValidTokenDb,
41 | createResetTokenDb,
42 | setTokenStatusDb,
43 | deleteResetTokenDb,
44 | };
45 |
--------------------------------------------------------------------------------
/server/docs/products/deleteProduct.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | delete: {
4 | tags: ["Products"], // operation's tag
5 | description: "Delete product", // short desc
6 | summary: "Delete a product",
7 | operationId: "deleteProduct", // unique operation id
8 | parameters: [
9 | {
10 | name: "id", // name of the param
11 | in: "path", // location of the param
12 | schema: {
13 | $ref: "#/components/schemas/id", // data model of the param
14 | },
15 | required: true, // Mandatory param
16 | description: "A product id", // param desc.
17 | },
18 | ], // expected params
19 | security: [
20 | {
21 | JWT: [],
22 | },
23 | ],
24 | // expected responses
25 | responses: {
26 | // response code
27 | 200: {
28 | description: "Product deleted successfully", // response desc
29 | },
30 | 401: {
31 | description: "Unauthorized",
32 | },
33 | 404: {
34 | description: "Product not found",
35 | },
36 | // response code
37 | 500: {
38 | description: "Server error", // response desc
39 | },
40 | },
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/server/controllers/orders.controller.js:
--------------------------------------------------------------------------------
1 | const orderService = require("../services/order.service");
2 | const cartService = require("../services/cart.service");
3 |
4 | const createOrder = async (req, res) => {
5 | const { amount, itemTotal, paymentMethod, ref } = req.body;
6 | const userId = req.user.id;
7 | const cartId = req.user.cart_id;
8 |
9 | const newOrder = await orderService.createOrder({
10 | cartId,
11 | amount,
12 | itemTotal,
13 | userId,
14 | paymentMethod,
15 | ref,
16 | });
17 |
18 | // delete all items from cart_items table for the user after order has been processed
19 | await cartService.emptyCart(cartId);
20 |
21 | res.status(201).json(newOrder);
22 | };
23 |
24 | const getAllOrders = async (req, res) => {
25 | const { page = 1 } = req.query;
26 | const userId = req.user.id;
27 |
28 | const orders = await orderService.getAllOrders(userId, page);
29 | res.json(orders);
30 | };
31 |
32 | const getOrder = async (req, res) => {
33 | const { id } = req.params;
34 | const userId = req.user.id;
35 |
36 | const order = await orderService.getOrderById({ id, userId });
37 | res.json(order);
38 | };
39 |
40 | module.exports = {
41 | createOrder,
42 | getAllOrders,
43 | getOrder,
44 | };
45 |
--------------------------------------------------------------------------------
/client/src/components/ReviewCard.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardBody } from "@windmill/react-ui";
2 | import { format, parseISO } from "date-fns";
3 | import ReactStars from "react-rating-stars-component";
4 |
5 | const ReviewCard = ({ reviews }) => {
6 | if (reviews.length === 0) {
7 | return (
8 |
11 | );
12 | }
13 | return (
14 | <>
15 | {reviews.map((review) => (
16 |
21 |
22 |
29 | {review.content}
30 | {`${format(
31 | parseISO(review.date),
32 | "dd-MM-yy"
33 | )} by ${review.name}`}
34 |
35 |
36 | ))}
37 | >
38 | );
39 | };
40 |
41 | export default ReviewCard;
42 |
--------------------------------------------------------------------------------
/server/docs/products/getProduct.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // method of operation
3 | get: {
4 | tags: ["Products"], // operation's tag.
5 | description: "Get a product", // operation's desc.
6 | summary: "Get a product by id",
7 | operationId: "getProduct", // unique operation id.
8 | parameters: [
9 | {
10 | name: "id", // name of the param
11 | in: "path", // location of the param
12 | schema: {
13 | $ref: "#/components/schemas/id", // data model of the param
14 | },
15 | required: true, // Mandatory param
16 | description: "A product id", // param desc.
17 | },
18 | ], // expected params.
19 | // expected responses
20 | responses: {
21 | // response code
22 | 200: {
23 | description: "Product obtained", // response desc.
24 | content: {
25 | // content-type
26 | "application/json": {
27 | schema: {
28 | $ref: "#/components/schemas/Product",
29 | },
30 | },
31 | },
32 | },
33 | 404: {
34 | description: "Product not found",
35 | },
36 | 500: {
37 | description: "Internal server error", // response desc.
38 | },
39 | },
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/server/docs/users/delete-user.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method.
3 | delete: {
4 | tags: ["Users"], // operation's tag
5 | description: "Deleting a user", // short desc
6 | summary: "Delete a user",
7 | operationId: "deleteUser", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [
14 | // expected parameters
15 | {
16 | name: "id", // name of param
17 | in: "path", // location of param
18 | schema: {
19 | $ref: "#/components/schemas/id", // id model
20 | },
21 | required: true, // mandatory
22 | description: "Deleting a user", // param desc
23 | },
24 | ],
25 | // expected responses
26 | responses: {
27 | // response code
28 | 200: {
29 | description: "User deleted successfully", // response desc
30 | },
31 | // response code
32 | 401: {
33 | description: "Unauthorized", // response desc
34 | },
35 | // response code
36 | 404: {
37 | description: "User not found", // response desc
38 | },
39 | // response code
40 | 500: {
41 | description: "Server error", // response desc
42 | },
43 | },
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/server/docs/orders/getOrder.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | get: {
4 | tags: ["Orders"], // operation's tag
5 | description: "Get an order", // short desc
6 | summary: "Get an order by id",
7 | operationId: "getOrder", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [
14 | {
15 | name: "id", // name of param
16 | in: "path", // location of param
17 | schema: {
18 | $ref: "#/components/schemas/id", // id model
19 | },
20 | required: true, // mandatory
21 | description: "Id of order", // short desc.
22 | },
23 | ],
24 | // expected responses
25 | responses: {
26 | // response code
27 | 200: {
28 | description: "Order obtained", // response desc.
29 | content: {
30 | // content-type
31 | "application/json": {
32 | schema: {
33 | $ref: "#/components/schemas/Order",
34 | },
35 | },
36 | },
37 | },
38 | 404: {
39 | description: "Order not found",
40 | },
41 | // response code
42 | 500: {
43 | description: "Server error", // response desc
44 | },
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/server/docs/users/create-user.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Users"], // operation's tag
5 | description: "Create a user", // short desc
6 | summary: "Create a new user",
7 | operationId: "createUser", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [
14 | {
15 | name: "user",
16 | in: "body",
17 | description: "Details of user to be created",
18 | schema: {
19 | $ref: "#/components/schemas/User",
20 | },
21 | },
22 | ], // expected params
23 | requestBody: {
24 | // expected request body
25 | content: {
26 | // content-type
27 | "application/json": {
28 | schema: {
29 | $ref: "#/components/schemas/User",
30 | },
31 | },
32 | },
33 | },
34 | // expected responses
35 | responses: {
36 | // response code
37 | 201: {
38 | description: "User created successfully", // response desc
39 | },
40 | 401: {
41 | description: "Unauthorized", // response desc
42 | },
43 | // response code
44 | 500: {
45 | description: "Server error", // response desc
46 | },
47 | },
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/load_balancers/main.tf:
--------------------------------------------------------------------------------
1 | # load balancer configuration for sonar qube
2 | resource "aws_lb" "sonar-lb" {
3 | name = "${var.enviroment}-sonar-lb"
4 | load_balancer_type = "application"
5 | internal = false
6 | security_groups = [var.sonar_alb_sg]
7 | subnets = var.sonar_subnet_ids
8 | depends_on = [var.igw_id]
9 |
10 | }
11 |
12 |
13 | # target group for sonar alb
14 |
15 | resource "aws_lb_target_group" "sonar_lb_tg" {
16 | name = "${var.enviroment}-sonar-lb-tg"
17 | vpc_id = var.vpc_id
18 |
19 | target_type = "instance"
20 | protocol_version = "HTTP1"
21 | health_check {
22 | path = "/"
23 | }
24 | port = 9000
25 | protocol = "HTTP"
26 |
27 | tags = {
28 | Name = "${var.enviroment}-sonar-lb-tg"
29 | }
30 | }
31 |
32 | resource "aws_lb_target_group_attachment" "sonar_lb_tg_attachment" {
33 | target_group_arn = aws_lb_target_group.sonar_lb_tg.arn
34 | target_id = var.sonar_target_instance
35 | port = 9000
36 | }
37 |
38 |
39 | # Listener for sonar lb
40 |
41 | resource "aws_lb_listener" "sonar_alb_listener" {
42 | protocol = "HTTP"
43 | port = 9000
44 | load_balancer_arn = aws_lb.sonar-lb.arn
45 | default_action {
46 | type = "forward"
47 | target_group_arn = aws_lb_target_group.sonar_lb_tg.arn
48 | }
49 | }
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/server/docs/auth/login.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Create a login session", // short desc
6 | summary: "Login",
7 | operationId: "login", // unique operation id
8 | parameters: [],
9 | requestBody: {
10 | // expected request body
11 | content: {
12 | // content-type
13 | "application/json": {
14 | schema: {
15 | $ref: "#/components/schemas/LoginInput",
16 | },
17 | },
18 | },
19 | },
20 | // expected responses
21 | responses: {
22 | // response code
23 | 200: {
24 | description: "Login successful", // response desc
25 | headers: {
26 | "set-cookie": {
27 | description: "`refreshToken`",
28 | schema: {
29 | type: "string",
30 | example:
31 | "refreshToken=0IjoxNjIyMzEzMjI4LCJleHAiOjE2MjIzMTY4Mjh9.LXKZmJW1mUyoHOsmhYdFni8mcEhON4dPAxAtSKoEqCo; Path=/; HttpOnly; Secure; SameSite=None",
32 | },
33 | },
34 | },
35 | },
36 | 403: {
37 | description: "Invalid login", // response desc
38 | },
39 | // response code
40 | 500: {
41 | description: "Server error", // response desc
42 | },
43 | },
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/server/docs/auth/reset-password.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Reset password", // short desc
6 | summary: "Change password",
7 | operationId: "resetPassword", // unique operation id
8 | parameters: [],
9 | requestBody: {
10 | // expected request body
11 | content: {
12 | // content-type
13 | "application/json": {
14 | schema: {
15 | type: "object",
16 | properties: {
17 | token: {
18 | type: "string",
19 | },
20 | email: {
21 | type: "string",
22 | },
23 | password: {
24 | type: "string",
25 | },
26 | password2: {
27 | type: "string",
28 | },
29 | },
30 | },
31 | },
32 | },
33 | },
34 | // expected responses
35 | responses: {
36 | // response code
37 | 200: {
38 | description: "Password reset successful", // response desc
39 | },
40 | 400: {
41 | description: "validation error", // response desc
42 | },
43 | // response code
44 | 500: {
45 | description: "Server error", // response desc
46 | },
47 | },
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/server/docs/auth/refresh-token.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Get refresh token", // short desc
6 | summary: "Refresh Token",
7 | operationId: "refreshToken", // unique operation id
8 | parameters: [],
9 | security: [
10 | {
11 | cookie: [],
12 | },
13 | ],
14 | // expected responses
15 | responses: {
16 | // response code
17 | 200: {
18 | description: "success", // response desc
19 | headers: {
20 | "set-cookie": {
21 | description: "`refreshToken`",
22 | schema: {
23 | type: "string",
24 | example:
25 | "refreshToken=0IjoxNjIyMzEzMjI4LCJleHAiOjE2MjIzMTY4Mjh9.LXKZmJW1mUyoHOsmhYdFni8mcEhON4dPAxAtSKoEqCo; Path=/; HttpOnly; Secure; SameSite=None",
26 | },
27 | },
28 | "auth-token": {
29 | description: "`accessToken`",
30 | schema: {
31 | type: "string",
32 | example:
33 | "0IjoxNjIyMzEzMjI4LCJleHAiOjE2MjIzMTY4Mjh9.LXKZmJW1mUyoHOsmhYdFni8mcEhON4dPAxAtSKoEqCo",
34 | },
35 | },
36 | },
37 | },
38 | // response code
39 | 500: {
40 | description: "Server error", // response desc
41 | },
42 | },
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/client/src/components/CartItem.jsx:
--------------------------------------------------------------------------------
1 | import { Button, TableCell } from "@windmill/react-ui";
2 | import { useCart } from "context/CartContext";
3 | import { formatCurrency } from "helpers/formatCurrency";
4 |
5 | const CartItem = ({ item }) => {
6 | const { decrement, increment, deleteItem } = useCart();
7 |
8 | const increase = () => {
9 | increment(item.product_id);
10 | };
11 | const decrease = () => {
12 | decrement(item.product_id);
13 | };
14 | return (
15 | <>
16 | {item.name}
17 | {formatCurrency(item.price)}
18 |
19 |
27 | {item.quantity}
28 |
31 |
32 | {formatCurrency(item.subtotal)}
33 |
34 |
37 |
38 | >
39 | );
40 | };
41 |
42 | export default CartItem;
43 |
--------------------------------------------------------------------------------
/client/src/services/auth.service.js:
--------------------------------------------------------------------------------
1 | import API from "api/axios.config";
2 |
3 | class AuthService {
4 | async login(email, password) {
5 | const { data } = await API.post("/auth/login", {
6 | email,
7 | password,
8 | });
9 | return data;
10 | }
11 |
12 | async googleLogin(code) {
13 | const { data } = await API.post("/auth/google", {
14 | code,
15 | });
16 | return data;
17 | }
18 |
19 | logout() {
20 | localStorage.removeItem("user");
21 | localStorage.removeItem("token");
22 | localStorage.removeItem("expiresAt");
23 | }
24 |
25 | forgotPassword(email) {
26 | return API.post("/auth/forgot-password", {
27 | email,
28 | });
29 | }
30 |
31 | checkToken(token, email) {
32 | return API.post("auth/check-token", {
33 | token,
34 | email,
35 | });
36 | }
37 |
38 | resetPassword(token, email, password, password2) {
39 | return API.post("auth/reset-password", {
40 | token,
41 | email,
42 | password,
43 | password2,
44 | });
45 | }
46 |
47 | register(username, email, password) {
48 | return API.post("auth/signup", {
49 | username,
50 | email,
51 | password,
52 | });
53 | }
54 |
55 | getCurrentUser() {
56 | return API.get("/users/profile");
57 | }
58 | }
59 |
60 | export default new AuthService();
61 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "lint": "eslint .",
10 | "lint:fix": "eslint --fix",
11 | "format": "prettier --write . --ignore-path .eslintignore"
12 | },
13 | "dependencies": {
14 | "@react-oauth/google": "^0.12.1",
15 | "@stripe/react-stripe-js": "^2.4.0",
16 | "@stripe/stripe-js": "^2.2.2",
17 | "@windmill/react-ui": "^0.6.0",
18 | "axios": "^1.6.2",
19 | "date-fns": "^3.0.6",
20 | "history": "^5.3.0",
21 | "pluralize": "^8.0.0",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-feather": "^2.0.10",
25 | "react-helmet-async": "^2.0.4",
26 | "react-hook-form": "^7.49.2",
27 | "react-hot-toast": "^2.4.1",
28 | "react-paystack": "^4.0.3",
29 | "react-rating-stars-component": "^2.2.0",
30 | "react-router-dom": "^6.21.1",
31 | "react-spinners": "^0.13.8"
32 | },
33 | "devDependencies": {
34 | "@vitejs/plugin-react": "^4.2.1",
35 | "autoprefixer": "^10.4.16",
36 | "eslint": "^8.56.0",
37 | "eslint-config-prettier": "^9.1.0",
38 | "eslint-plugin-prettier": "^5.1.2",
39 | "eslint-plugin-react": "^7.33.2",
40 | "postcss": "^8.4.32",
41 | "prettier": "^3.1.1",
42 | "tailwindcss": "^3.4.0",
43 | "vite": "^5.0.10"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/docs/users/get-users.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // method of operation
3 | get: {
4 | tags: ["Users"], // operation's tag.
5 | description: "Get users", // operation's desc.
6 | summary: "Get all users",
7 | operationId: "getUsers", // unique operation id.
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [], // expected params.
14 | // expected responses
15 | responses: {
16 | // response code
17 | 200: {
18 | description: "Users were obtained", // response desc.
19 | content: {
20 | // content-type
21 | "application/json": {
22 | schema: {
23 | $ref: "#/components/schemas/User", // user model
24 | },
25 | },
26 | },
27 | },
28 | 401: {
29 | description: "Unauthorized", // response desc.
30 | content: {
31 | // content-type
32 | "application/json": {
33 | schema: {
34 | $ref: "#/components/schemas/Error",
35 | },
36 | },
37 | },
38 | },
39 | 500: {
40 | description: "Internal Server error", // response desc.
41 | content: {
42 | // content-type
43 | "application/json": {
44 | schema: {
45 | $ref: "#/components/schemas/Error",
46 | },
47 | },
48 | },
49 | },
50 | },
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/server/db/review.db.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 |
3 | const getReviewsDb = async ({ productId, userId }) => {
4 | // check if current logged user review exist for the product
5 | const reviewExist = await pool.query(
6 | "SELECT EXISTS (SELECT * FROM reviews where product_id = $1 and user_id = $2)",
7 | [productId, userId]
8 | );
9 |
10 | // get reviews associated with the product
11 | const reviews = await pool.query(
12 | `SELECT users.fullname as name, reviews.* FROM reviews
13 | join users
14 | on users.user_id = reviews.user_id
15 | WHERE product_id = $1`,
16 | [productId]
17 | );
18 | return {
19 | reviewExist: reviewExist.rows[0].exists,
20 | reviews: reviews.rows,
21 | };
22 | };
23 |
24 | const createReviewDb = async ({ productId, content, rating, userId }) => {
25 | const { rows: review } = await pool.query(
26 | `INSERT INTO reviews(user_id, product_id, content, rating)
27 | VALUES($1, $2, $3, $4) returning *
28 | `,
29 | [userId, productId, content, rating]
30 | );
31 | return review[0];
32 | };
33 |
34 | const updateReviewDb = async ({ content, rating, id }) => {
35 | const { rows: review } = await pool.query(
36 | `UPDATE reviews set content = $1, rating = $2 where id = $3 returning *
37 | `,
38 | [content, rating, id]
39 | );
40 | return review[0];
41 | };
42 |
43 | module.exports = {
44 | createReviewDb,
45 | updateReviewDb,
46 | getReviewsDb,
47 | };
48 |
--------------------------------------------------------------------------------
/client/src/pages/ProductList.jsx:
--------------------------------------------------------------------------------
1 | import { Card, Pagination } from "@windmill/react-ui";
2 | import Product from "components/Product";
3 | import Spinner from "components/Spinner";
4 | import { useProduct } from "context/ProductContext";
5 | import Layout from "layout/Layout";
6 |
7 | const ProductList = () => {
8 | const { products, setPage } = useProduct();
9 |
10 | const handleChange = (page) => {
11 | setPage(page);
12 | window.scrollTo({ behavior: "smooth", top: 0 });
13 | };
14 |
15 | if (!products) {
16 | return (
17 | <>
18 |
19 |
20 |
21 | >
22 | );
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 | {products?.map((prod) => (
30 |
36 | ))}
37 |
38 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default ProductList;
50 |
--------------------------------------------------------------------------------
/Deployment/Terraform/file_structure.txt:
--------------------------------------------------------------------------------
1 | project-root/
2 | ├── main.tf
3 | ├── variables.tf
4 | ├── outputs.tf
5 | ├── terraform.tfvars
6 | ├── provider.tf
7 | ├── env/
8 | │ ├── dev/
9 | │ │ ├── main.tf
10 | │ │ ├── variables.tf
11 | │ │ ├── outputs.tf
12 | │ │ └── backend.tf
13 | │ ├── staging/
14 | │ │ ├── main.tf
15 | │ │ ├── variables.tf
16 | │ │ ├── outputs.tf
17 | │ │ └── backend.tf
18 | │ └── prod/
19 | │ ├── main.tf
20 | │ ├── variables.tf
21 | │ ├── outputs.tf
22 | │ └── backend.tf
23 | ├── modules/
24 | │ ├── network/
25 | │ │ ├── main.tf
26 | │ │ ├── variables.tf
27 | │ │ └── outputs.tf
28 | │ ├── compute/
29 | │ │ ├── main.tf
30 | │ │ ├── variables.tf
31 | │ │ └── outputs.tf
32 | │ ├── load_balancer/
33 | │ │ ├── main.tf
34 | │ │ ├── variables.tf
35 | │ │ └── outputs.tf
36 | │ ├── database/
37 | │ │ ├── main.tf
38 | │ │ ├── variables.tf
39 | │ │ └── outputs.tf
40 | │ ├── security/
41 | │ │ ├── main.tf
42 | │ │ ├── variables.tf
43 | │ │ └── outputs.tf
44 | │ ├── monitoring/
45 | │ │ ├── main.tf
46 | │ │ ├── variables.tf
47 | │ │ └── outputs.tf
48 | │ └── ci_cd/
49 | │ ├── main.tf
50 | │ ├── variables.tf
51 | │ └── outputs.tf
52 | └── scripts/
53 | ├── init.sh
54 | └── deploy.sh
55 |
--------------------------------------------------------------------------------
/server/controllers/cart.controller.js:
--------------------------------------------------------------------------------
1 | const cartService = require("../services/cart.service");
2 |
3 | const getCart = async (req, res) => {
4 | const userId = req.user.id;
5 |
6 | // get cart items
7 | const cart = await cartService.getCart(userId);
8 | res.json({ items: cart });
9 | };
10 |
11 | // add item to cart
12 | const addItem = async (req, res) => {
13 | const cart_id = req.user.cart_id;
14 |
15 | const cart = await cartService.addItem({ ...req.body, cart_id });
16 | res.status(200).json({ data: cart });
17 | };
18 |
19 | // delete item from cart
20 | const deleteItem = async (req, res) => {
21 | const { product_id } = req.body;
22 | const cart_id = req.user.cart_id;
23 |
24 | const data = await cartService.removeItem({ cart_id, product_id });
25 | res.status(200).json(data);
26 | };
27 |
28 | // increment item quantity by 1
29 | const increaseItemQuantity = async (req, res) => {
30 | const { product_id } = req.body;
31 | const cart_id = req.user.cart_id;
32 |
33 | const cart = await cartService.increaseQuantity({ cart_id, product_id });
34 | res.json(cart);
35 | };
36 |
37 | // decrement item quantity by 1
38 | const decreaseItemQuantity = async (req, res) => {
39 | const { product_id } = req.body;
40 | const cart_id = req.user.cart_id;
41 |
42 | const cart = await cartService.decreaseQuantity({ cart_id, product_id });
43 | res.json(cart);
44 | };
45 |
46 | module.exports = {
47 | getCart,
48 | addItem,
49 | increaseItemQuantity,
50 | decreaseItemQuantity,
51 | deleteItem,
52 | };
53 |
--------------------------------------------------------------------------------
/client/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { GoogleOAuthProvider } from "@react-oauth/google";
2 | import { Windmill } from "@windmill/react-ui";
3 | import { GlobalHistory } from "components/GlobalHistory";
4 | import { CartProvider } from "context/CartContext";
5 | import { OrderProvider } from "context/OrderContext";
6 | import { ProductProvider } from "context/ProductContext";
7 | import { ReviewProvider } from "context/ReviewContext";
8 | import { UserProvider } from "context/UserContext";
9 | import { createRoot } from "react-dom/client";
10 | import { HelmetProvider } from "react-helmet-async";
11 | import { BrowserRouter } from "react-router-dom";
12 | import App from "./App";
13 | import "./index.css";
14 |
15 | const container = document.getElementById("root");
16 | const root = createRoot(container);
17 |
18 | const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
19 |
20 | root.render(
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 |
--------------------------------------------------------------------------------
/client/src/pages/Checkout.jsx:
--------------------------------------------------------------------------------
1 | import AddressForm from "components/AddressForm";
2 | import PaymentForm from "components/PaymentForm";
3 | import { useCart } from "context/CartContext";
4 | import Layout from "layout/Layout";
5 | import { useEffect, useState } from "react";
6 | import { useLocation, useNavigate } from "react-router";
7 |
8 | const Checkout = () => {
9 | const [activeStep, setActiveStep] = useState(0);
10 | const [addressData, setAddressData] = useState();
11 | const { state } = useLocation();
12 | const navigate = useNavigate();
13 | const { cartData } = useCart();
14 |
15 | useEffect(() => {
16 | if (!state?.fromCartPage) {
17 | return navigate("/cart");
18 | }
19 |
20 | if (cartData.items.length === 0) {
21 | return navigate("/cart");
22 | }
23 | }, [cartData, navigate, state]);
24 |
25 | const nextStep = () => setActiveStep((prevStep) => setActiveStep(prevStep + 1));
26 | const previousStep = () => setActiveStep((prevStep) => setActiveStep(prevStep - 1));
27 |
28 | const next = (data) => {
29 | setAddressData(data);
30 | nextStep();
31 | };
32 | return (
33 |
34 |
35 | {activeStep === 0 ? (
36 |
37 | ) : (
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
45 | export default Checkout;
46 |
--------------------------------------------------------------------------------
/server/docs/cart/addItem.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Cart"], // operation's tag
5 | description: "Add an item to cart", // short desc
6 | summary: "Add an item",
7 | operationId: "addItem", // unique operation id
8 | parameters: [], // expected params
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | requestBody: {
15 | // expected request body
16 | content: {
17 | // content-type
18 | "application/json": {
19 | schema: {
20 | type: "object",
21 | properties: {
22 | product_id: {
23 | type: "number",
24 | },
25 | quantity: {
26 | type: "number",
27 | },
28 | },
29 | },
30 | },
31 | },
32 | },
33 | // expected responses
34 | responses: {
35 | // response code
36 | 200: {
37 | description: "item added successfully", // response desc
38 | content: {
39 | // content-type
40 | "application/json": {
41 | schema: {
42 | type: "array",
43 | items: {
44 | $ref: "#/components/schemas/CartItem",
45 | },
46 | },
47 | },
48 | },
49 | },
50 | 401: {
51 | description: "Unauthorized",
52 | },
53 | // response code
54 | 500: {
55 | description: "Server error", // response desc
56 | },
57 | },
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/server/docs/cart/removeItem.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method.
3 | delete: {
4 | tags: ["Cart"], // operation's tag
5 | description: "Removing a item", // short desc
6 | summary: "Remove item from cart",
7 | operationId: "removeItem", // unique operation id
8 | parameters: [],
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | requestBody: {
15 | // expected request body
16 | content: {
17 | // content-type
18 | "application/json": {
19 | schema: {
20 | type: "object",
21 | properties: {
22 | id: {
23 | type: "number",
24 | description: "ID of product to remove from cart",
25 | example: 34,
26 | },
27 | },
28 | },
29 | },
30 | },
31 | },
32 | // expected responses
33 | responses: {
34 | // response code
35 | 200: {
36 | description: "Item removed successfully", // response desc
37 | content: {
38 | // content-type
39 | "application/json": {
40 | schema: {
41 | type: "array",
42 | items: {
43 | $ref: "#/components/schemas/CartItem",
44 | },
45 | },
46 | },
47 | },
48 | },
49 | 401: {
50 | description: "Unauthorized",
51 | },
52 | // response code
53 | 500: {
54 | description: "Server error", // response desc
55 | },
56 | },
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/client/src/pages/Confirmation.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@windmill/react-ui";
2 | import { useUser } from "context/UserContext";
3 | import Layout from "layout/Layout";
4 | import { useEffect } from "react";
5 | import { CheckCircle } from "react-feather";
6 | import { Link, useLocation, useNavigate } from "react-router-dom";
7 |
8 | const Confirmation = () => {
9 | const { state } = useLocation();
10 | const navigate = useNavigate();
11 | const { userData } = useUser();
12 |
13 | useEffect(() => {
14 | if (!state?.fromPaymentPage) {
15 | return navigate("/");
16 | }
17 | }, [state]);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
Order Confirmed
28 |
Thank you for your purchase, {`${userData?.fullname}`}!
29 |
30 |
33 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Confirmation;
45 |
--------------------------------------------------------------------------------
/server/docs/cart/decrease.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | put: {
4 | tags: ["Cart"], // operation's tag
5 | description: "Decrease item quantity", // short desc
6 | summary: "Decrease item quantity",
7 | operationId: "decrement", // unique operation id
8 | parameters: [],
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | requestBody: {
15 | // expected request body
16 | content: {
17 | // content-type
18 | "application/json": {
19 | schema: {
20 | type: "object",
21 | properties: {
22 | id: {
23 | type: "number",
24 | description: "ID of product to decrease quantity from cart",
25 | example: 34,
26 | },
27 | },
28 | },
29 | },
30 | },
31 | },
32 | // expected responses
33 | responses: {
34 | // response code
35 | 200: {
36 | description: "Quantity decreased successfully", // response desc.
37 | content: {
38 | // content-type
39 | "application/json": {
40 | schema: {
41 | type: "array",
42 | items: {
43 | $ref: "#/components/schemas/CartItem",
44 | },
45 | },
46 | },
47 | },
48 | },
49 | // response code
50 | 401: {
51 | description: "Unauthorized",
52 | },
53 | // response code
54 | 500: {
55 | description: "Server error", // response desc.
56 | },
57 | },
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/server/docs/cart/increase.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | put: {
4 | tags: ["Cart"], // operation's tag
5 | description: "Increase item quantity", // short desc
6 | summary: "Increase item quantity",
7 | operationId: "increment", // unique operation id
8 | parameters: [],
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | // expected responses
15 | requestBody: {
16 | // expected request body
17 | content: {
18 | // content-type
19 | "application/json": {
20 | schema: {
21 | type: "object",
22 | properties: {
23 | id: {
24 | type: "number",
25 | description: "ID of product to increase quantity from cart",
26 | example: 34,
27 | },
28 | },
29 | },
30 | },
31 | },
32 | },
33 | responses: {
34 | // response code
35 | 200: {
36 | description: "Quantity increased successfully", // response desc.
37 | content: {
38 | // content-type
39 | "application/json": {
40 | schema: {
41 | type: "array",
42 | items: {
43 | $ref: "#/components/schemas/CartItem",
44 | },
45 | },
46 | },
47 | },
48 | },
49 | // response code
50 | 401: {
51 | description: "Unauthorized",
52 | },
53 | // response code
54 | 500: {
55 | description: "Server error", // response desc.
56 | },
57 | },
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/client/src/components/PaystackBtn.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@windmill/react-ui";
2 | import { useCart } from "context/CartContext";
3 | import { useUser } from "context/UserContext";
4 | import toast from "react-hot-toast";
5 | import { usePaystackPayment } from "react-paystack";
6 | import { useNavigate } from "react-router-dom";
7 | import orderService from "services/order.service";
8 |
9 | const PaystackBtn = ({ isProcessing, setIsProcessing }) => {
10 | const { cartSubtotal, cartTotal, cartData, setCartData } = useCart();
11 | const { userData } = useUser();
12 | const navigate = useNavigate();
13 |
14 | const onSuccess = (data) => {
15 | orderService.createOrder(cartSubtotal, cartTotal, data.reference, "PAYSTACK").then(() => {
16 | setCartData({ ...cartData, items: [] });
17 | setIsProcessing(false);
18 | navigate("/cart/success", {
19 | state: {
20 | fromPaymentPage: true,
21 | },
22 | });
23 | });
24 | };
25 |
26 | const onClose = () => {
27 | toast.error("Payment cancelled");
28 | setIsProcessing(false);
29 | };
30 |
31 | const config = {
32 | email: userData.email,
33 | amount: (cartSubtotal * 100).toFixed(2),
34 | publicKey: import.meta.env.VITE_PAYSTACK_PUB_KEY,
35 | };
36 |
37 | const initializePayment = usePaystackPayment(config);
38 | return (
39 |
49 | );
50 | };
51 |
52 | export default PaystackBtn;
53 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Spinner from "components/Spinner";
2 | import Layout from "layout/Layout";
3 | import {
4 | Account,
5 | Cart,
6 | Checkout,
7 | Confirmation,
8 | Login,
9 | NotFound,
10 | OrderDetails,
11 | Orders,
12 | ProductDetails,
13 | ProductList,
14 | Register,
15 | ResetPassword,
16 | } from "pages";
17 | import { Suspense } from "react";
18 | import { Toaster } from "react-hot-toast";
19 | import { Route, Routes } from "react-router-dom";
20 | import { ProtectedRoute } from "routes/protected.route";
21 |
22 | function App() {
23 | return (
24 |
27 |
28 |
29 | }
30 | >
31 |
32 |
33 | }>
34 | } />
35 | } />
36 | } />
37 | } />
38 | } />
39 |
40 |
41 | } />
42 | } />
43 | } />
44 | } />
45 | } />
46 | } />
47 | }>
48 |
49 |
50 | );
51 | }
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/Deployment/Terraform/main.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = var.region
3 | }
4 |
5 | module "cicd" {
6 | source = "./modules/cicd"
7 | environment = var.environment
8 | ami = var.ami
9 | key_name = var.key_name
10 | instance_type = var.instance_type
11 | availability_zone = module.network.availability_zone
12 | sonar-subnet_id = module.network.sonar-subnet_id
13 | jenkins_subnet_id = module.network.jenkins_subnet_id
14 | jenkins_sg = module.security.jenkins_sg
15 | sonar_sg_id = module.security.sonar_sg_id
16 | jenkins_availablity_zone = module.network.jenkins_availablity_zone
17 | }
18 |
19 | module "network" {
20 | source = "./modules/network"
21 | environment = var.environment
22 | bastion_sg_id = module.security.bastion_sg_id
23 | bastion_ssm_role_name = module.security.bastion_ssm_role_name
24 | key_name = var.key_name
25 | ami = var.ami
26 | instance_type = var.instance_type
27 | region = var.region
28 | }
29 |
30 | module "security" {
31 | source = "./modules/security"
32 | vpc_id = module.network.vpc_id
33 | environment = var.environment
34 |
35 | }
36 |
37 | module "load_balancer" {
38 | source = "./modules/load_balancers"
39 | enviroment = var.environment
40 | sonar_subnet_ids = module.network.sonar_subnet_ids
41 | sonar_alb_sg = module.security.sonar_alb_sg
42 | igw_id = module.network.igw_id
43 | vpc_id = module.network.vpc_id
44 | sonar_target_instance = module.cicd.sonar_target_instance
45 |
46 | }
47 | module "artifact" {
48 | source = "./modules/artifacts"
49 | key_name = var.key_name
50 | nexus_sg = module.security.nexus_sg
51 | nexus_subnet = module.network.nexus_subnet
52 | enviroment = var.environment
53 | ami = var.ami
54 | }
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production node index",
8 | "dev": "cross-env NODE_ENV=development && nodemon --legacy-watch",
9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand",
10 | "test:watch": "cross-env NODE_ENV=test jest --verbose --runInBand --watch",
11 | "lint": "eslint .",
12 | "lint:fix": "eslint . --fix",
13 | "format": "prettier --write ."
14 | },
15 | "jest": {
16 | "testEnvironment": "node",
17 | "coveragePathIgnorePatterns": [
18 | "/node_modules/"
19 | ]
20 | },
21 | "keywords": [],
22 | "author": "",
23 | "license": "ISC",
24 | "dependencies": {
25 | "bcrypt": "^5.1.1",
26 | "compression": "^1.7.4",
27 | "cookie-parser": "^1.4.6",
28 | "cors": "^2.8.5",
29 | "crypto": "^1.0.1",
30 | "dotenv": "^8.2.0",
31 | "express": "^4.18.2",
32 | "express-async-errors": "^3.1.1",
33 | "google-auth-library": "^8.7.0",
34 | "googleapis": "^112.0.0",
35 | "helmet": "^4.4.1",
36 | "jsonwebtoken": "^8.5.1",
37 | "moment": "^2.29.4",
38 | "morgan": "^1.10.0",
39 | "nodemailer": "^6.8.0",
40 | "pg": "^8.8.0",
41 | "pino": "^6.11.3",
42 | "stripe": "^8.138.0",
43 | "swagger-ui-express": "^4.6.0"
44 | },
45 | "devDependencies": {
46 | "babel-eslint": "^10.1.0",
47 | "cross-env": "^7.0.3",
48 | "eslint": "^7.32.0",
49 | "eslint-plugin-babel": "^5.3.1",
50 | "eslint-plugin-prettier": "^4.2.1",
51 | "nodemon": "^2.0.20",
52 | "pino-pretty": "^4.8.0",
53 | "prettier": "^2.8.1",
54 | "supertest": "^6.3.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/client/src/helpers/WithAxios.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import API from "api/axios.config";
3 | import { useUser } from "context/UserContext";
4 | import history from "helpers/history";
5 |
6 | const WithAxios = ({ children }) => {
7 | const { setIsLoggedIn, setUserData, setAuthData, isLoggedIn } = useUser();
8 |
9 | useMemo(() => {
10 | if (isLoggedIn) {
11 | API.interceptors.response.use(
12 | (response) => response,
13 | async (error) => {
14 | const originalRequest = error.config;
15 | if (error.response.status === 401 && originalRequest.url === "/auth/refresh-token") {
16 | return new Promise((resolve, reject) => {
17 | setIsLoggedIn(false);
18 | setAuthData(null);
19 | setUserData(null);
20 | history.push("/login");
21 | reject(error);
22 | });
23 | }
24 |
25 | if (error.response.status === 401 && !originalRequest._retry) {
26 | try {
27 | originalRequest._retry = true;
28 | const res = await API.post("/auth/refresh-token");
29 | localStorage.setItem("token", JSON.stringify(res.data.token));
30 | return API(originalRequest);
31 | } catch (error) {
32 | localStorage.removeItem("token");
33 | setIsLoggedIn(false);
34 | setAuthData(null);
35 | setUserData(null);
36 | history.push("/login");
37 | }
38 | }
39 | return Promise.reject(error);
40 | }
41 | );
42 | }
43 | }, [isLoggedIn, setAuthData, setIsLoggedIn, setUserData]);
44 |
45 | return children;
46 | };
47 |
48 | export default WithAxios;
49 |
--------------------------------------------------------------------------------
/server/docs/products/createProduct.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Products"], // operation's tag
5 | description: "Create product", // short desc
6 | summary: "Create a product",
7 | operationId: "createProduct", // unique operation id
8 | parameters: [], // expected params
9 | security: [
10 | {
11 | JWT: [],
12 | },
13 | ],
14 | requestBody: {
15 | // expected request body
16 | content: {
17 | // content-type
18 | "application/json": {
19 | schema: {
20 | type: "object", // data type
21 | properties: {
22 | name: {
23 | type: "string", // data-type
24 | description: "Product's name", // desc
25 | },
26 | price: {
27 | type: "integer", // data-type
28 | description: "Product price", // desc
29 | },
30 | description: {
31 | type: "string", // data-type
32 | description: "Product description", // desc
33 | },
34 | image_url: {
35 | type: "string", // data-type
36 | description: "product's image url", // desc
37 | },
38 | },
39 | },
40 | },
41 | },
42 | },
43 | // expected responses
44 | responses: {
45 | // response code
46 | 201: {
47 | description: "Product created successfully", // response desc
48 | },
49 | 401: {
50 | description: "Unauthorized",
51 | },
52 | // response code
53 | 500: {
54 | description: "Server error", // response desc
55 | },
56 | },
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/client/src/helpers/localStorage.js:
--------------------------------------------------------------------------------
1 | class LocalCart {
2 | isExist = (id) => !!this.getItem(id);
3 |
4 | getItems = () => JSON.parse(localStorage.getItem("__cart")) || [];
5 |
6 | getItem = (id) => this.getItems().find((product) => product.product_id === id);
7 |
8 | saveItems = (data) => localStorage.setItem("__cart", JSON.stringify(data));
9 |
10 | removeItem = (id) =>
11 | this.saveItems(this.getItems().filter((product) => product.product_id !== id));
12 |
13 | incrementQuantity = (id) =>
14 | this.saveItems(
15 | this.getItems().map((prod) => {
16 | if (id === prod.product_id) {
17 | prod.quantity += 1;
18 | prod.subtotal = prod.price * prod.quantity;
19 | }
20 | return prod;
21 | })
22 | );
23 |
24 | decrementQuantity = (id) =>
25 | this.saveItems(
26 | this.getItems().map((prod) => {
27 | if (id === prod.product_id) {
28 | if (prod.quantity === 0) {
29 | prod.quantity = 0;
30 | } else {
31 | prod.quantity -= 1;
32 | }
33 | prod.subtotal = prod.price * prod.quantity;
34 | }
35 | return prod;
36 | })
37 | );
38 |
39 | addItem = (product, quantity) => {
40 | if (this.isExist(product.product_id)) {
41 | this.saveItems(
42 | this.getItems().map((prod) => {
43 | if (product.product_id === prod.product_id) {
44 | prod.quantity += quantity || 1;
45 | }
46 | return prod;
47 | })
48 | );
49 | } else {
50 | product.quantity = 1;
51 | product.subtotal = product.price;
52 | this.saveItems([...this.getItems(), product]);
53 | }
54 | };
55 | clearCart = () => localStorage.removeItem("__cart");
56 | }
57 |
58 | export default new LocalCart();
59 |
--------------------------------------------------------------------------------
/server/services/cart.service.js:
--------------------------------------------------------------------------------
1 | const {
2 | createCartDb,
3 | getCartDb,
4 | addItemDb,
5 | deleteItemDb,
6 | increaseItemQuantityDb,
7 | decreaseItemQuantityDb,
8 | emptyCartDb,
9 | } = require("../db/cart.db");
10 | const { ErrorHandler } = require("../helpers/error");
11 |
12 | class CartService {
13 | createCart = async (userId) => {
14 | try {
15 | return await createCartDb(userId);
16 | } catch (error) {
17 | throw new ErrorHandler(error.statusCode, error.message);
18 | }
19 | };
20 | getCart = async (userId) => {
21 | try {
22 | return await getCartDb(userId);
23 | } catch (error) {
24 | throw new ErrorHandler(error.statusCode, error.message);
25 | }
26 | };
27 |
28 | addItem = async (data) => {
29 | try {
30 | return await addItemDb(data);
31 | } catch (error) {
32 | throw new ErrorHandler(error.statusCode, error.message);
33 | }
34 | };
35 |
36 | removeItem = async (data) => {
37 | try {
38 | return await deleteItemDb(data);
39 | } catch (error) {
40 | throw new ErrorHandler(error.statusCode, error.message);
41 | }
42 | };
43 |
44 | increaseQuantity = async (data) => {
45 | try {
46 | return await increaseItemQuantityDb(data);
47 | } catch (error) {
48 | throw new ErrorHandler(error.statusCode, error.message);
49 | }
50 | };
51 |
52 | decreaseQuantity = async (data) => {
53 | try {
54 | return await decreaseItemQuantityDb(data);
55 | } catch (error) {
56 | throw new ErrorHandler(error.statusCode, error.message);
57 | }
58 | };
59 | emptyCart = async (cartId) => {
60 | try {
61 | return await emptyCartDb(cartId);
62 | } catch (error) {
63 | throw new ErrorHandler(error.statusCode, error.message);
64 | }
65 | };
66 | }
67 |
68 | module.exports = new CartService();
69 |
--------------------------------------------------------------------------------
/server/docs/users/get-user.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | get: {
4 | tags: ["Users"], // operation's tag.
5 | description: "Get a user", // operation's desc.
6 | summary: "Get user by id",
7 | operationId: "getUser", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [
14 | // expected params.
15 | {
16 | name: "id", // name of the param
17 | in: "path", // location of the param
18 | schema: {
19 | $ref: "#/components/schemas/id", // data model of the param
20 | },
21 | required: true, // Mandatory param
22 | description: "ID of user to find", // param desc.
23 | },
24 | ],
25 | // expected responses
26 | responses: {
27 | // response code
28 | 200: {
29 | description: "User is obtained", // response desc.
30 | content: {
31 | // content-type
32 | "application/json": {
33 | schema: {
34 | $ref: "#/components/schemas/User", // user data model
35 | },
36 | },
37 | },
38 | },
39 | // response code
40 | 404: {
41 | description: "User is not found", // response desc.
42 | content: {
43 | // content-type
44 | "application/json": {
45 | schema: {
46 | $ref: "#/components/schemas/Error", // error data model
47 | },
48 | },
49 | },
50 | },
51 | 401: {
52 | description: "Unauthorized", // response desc.
53 | content: {
54 | // content-type
55 | "application/json": {
56 | schema: {
57 | $ref: "#/components/schemas/Error", // error data model
58 | },
59 | },
60 | },
61 | },
62 | },
63 | },
64 | };
65 |
--------------------------------------------------------------------------------
/server/db/orders.db.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config/index");
2 |
3 | const createOrderDb = async ({
4 | cartId,
5 | amount,
6 | itemTotal,
7 | userId,
8 | ref,
9 | paymentMethod,
10 | }) => {
11 | // create an order
12 | const { rows: order } = await pool.query(
13 | "INSERT INTO orders(user_id, status, amount, total, ref, payment_method) VALUES($1, 'complete', $2, $3, $4, $5) returning *",
14 | [userId, amount, itemTotal, ref, paymentMethod]
15 | );
16 |
17 | // copy cart items from the current cart_item table into order_item table
18 | await pool.query(
19 | `
20 | INSERT INTO order_item(order_id,product_id, quantity)
21 | SELECT $1, product_id, quantity from cart_item where cart_id = $2
22 | returning *
23 | `,
24 | [order[0].order_id, cartId]
25 | );
26 | return order[0];
27 | };
28 |
29 | const getAllOrdersDb = async ({ userId, limit, offset }) => {
30 | const { rowCount } = await pool.query(
31 | "SELECT * from orders WHERE orders.user_id = $1",
32 | [userId]
33 | );
34 | const orders = await pool.query(
35 | `SELECT order_id, user_id, status, date::date, amount, total
36 | from orders WHERE orders.user_id = $1 order by order_id desc limit $2 offset $3`,
37 | [userId, limit, offset]
38 | );
39 | return { items: orders.rows, total: rowCount };
40 | };
41 |
42 | const getOrderDb = async ({ id, userId }) => {
43 | const { rows: order } = await pool.query(
44 | `SELECT products.*, order_item.quantity
45 | from orders
46 | join order_item
47 | on order_item.order_id = orders.order_id
48 | join products
49 | on products.product_id = order_item.product_id
50 | where orders.order_id = $1 AND orders.user_id = $2`,
51 | [id, userId]
52 | );
53 | return order;
54 | };
55 |
56 | module.exports = {
57 | createOrderDb,
58 | getAllOrdersDb,
59 | getOrderDb,
60 | };
61 |
--------------------------------------------------------------------------------
/server/docs/users/update-user.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | put: {
4 | tags: ["Users"], // operation's tag
5 | description: "Update user", // short desc
6 | summary: "Update a user",
7 | operationId: "updateUser", // unique operation id
8 | security: [
9 | {
10 | JWT: [],
11 | },
12 | ],
13 | parameters: [
14 | // expected params
15 | {
16 | name: "id", // name of param
17 | in: "path", // location of param
18 | schema: {
19 | $ref: "#/components/schemas/id", // id model
20 | },
21 | required: true, // mandatory
22 | description: "Id of user to be updated", // short desc.
23 | },
24 | ],
25 | // expected responses
26 | responses: {
27 | // response code
28 | 200: {
29 | description: "User updated successfully", // response desc.
30 | content: {
31 | // content-type
32 | "application/json": {
33 | schema: {
34 | $ref: "#/components/schemas/User", // user data model
35 | },
36 | },
37 | },
38 | },
39 | // response code
40 | 403: {
41 | description: "Data exists already", // response desc.
42 | content: {
43 | // content-type
44 | "application/json": {
45 | schema: {
46 | $ref: "#/components/schemas/Error", // user data model
47 | },
48 | },
49 | },
50 | },
51 | // response code
52 | 500: {
53 | description: "Server error", // response desc.
54 | content: {
55 | // content-type
56 | "application/json": {
57 | schema: {
58 | $ref: "#/components/schemas/Error", // user data model
59 | },
60 | },
61 | },
62 | },
63 | },
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/client/src/pages/OrderDetails.jsx:
--------------------------------------------------------------------------------
1 | import { Badge, Card, CardBody } from "@windmill/react-ui";
2 | import { format, parseISO } from "date-fns";
3 | import { formatCurrency } from "helpers/formatCurrency";
4 | import Layout from "layout/Layout";
5 | import { useEffect, useState } from "react";
6 | import { useLocation, useParams } from "react-router-dom";
7 | import orderService from "services/order.service";
8 |
9 | const OrderDetails = () => {
10 | const { id } = useParams();
11 | const { state } = useLocation();
12 | const [items, setItems] = useState(null);
13 |
14 | useEffect(() => {
15 | orderService.getOrder(id).then((res) => setItems(res.data));
16 | }, [id]);
17 |
18 | return (
19 |
20 |
21 |
Order Details
22 |
Order no: #{state.order.order_id}
23 |
{`${state.order.total || "Not available"} items`}
24 |
25 | Status: {state.order.status}
26 |
27 |
Total Amount: {formatCurrency(state.order.amount)}
28 |
Placed on: {format(parseISO(state.order.date), "d MMM, yyyy")}
29 |
30 |
Items in your order
31 | {items?.map((item) => (
32 |
33 |
40 |
41 | {item.name}
42 | {formatCurrency(item.price)}
43 | {item.description}
44 | Quantity: {item.quantity}
45 |
46 |
47 | ))}
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default OrderDetails;
55 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | server:
5 | image: pern-store-server
6 | container_name: pern-store-server
7 | build:
8 | context: ./server
9 | dockerfile: Dockerfile
10 | # restart: always
11 | env_file:
12 | - ./server/.env
13 | environment:
14 | - POSTGRES_HOST=3.84.191.228
15 | - POSTGRES_DB=pernstore
16 | - POSTGRES_USER=postgres
17 | - POSTGRES_PASSWORD=newpassword
18 | volumes:
19 | # Maps the server directory to
20 | # the working directory in the container
21 | - ./server:/usr/src/app
22 | # Maps the node_modules directory to
23 | # the working directory in the container
24 | # and also fixes bcrypt error
25 | - /usr/src/app/node_modules
26 | ports:
27 | - 9000:9000
28 | networks:
29 | - pernstore
30 | # depends_on:
31 | # - database
32 | client:
33 | image: pern-store-client
34 | container_name: pern-store-client
35 | restart: unless-stopped
36 | env_file:
37 | - ./client/.env
38 | build:
39 | context: ./client
40 | dockerfile: Dockerfile
41 | ports:
42 | - 3001:3000
43 | volumes:
44 | - ./client:/usr/src/app
45 | - /usr/src/app/node_modules
46 | depends_on:
47 | - server
48 | networks:
49 | - pernstore
50 | # database:
51 | # container_name: pern-store-db
52 | # image: postgres
53 | # restart: always
54 | # env_file:
55 | # - ./server/.env
56 | # environment:
57 | # # other environment variables is in ./server/.env file
58 | # - POSTGRES_PASSWORD=adminuser
59 | # ports:
60 | # - 7890:5432
61 | # volumes:
62 | # - ./db:/var/lib/postgresql/data
63 | # # When the PostgreSQL container is started it will run any scripts
64 | # # provided in the `docker-entrypoint-initdb.d` directory
65 | # # seed the database with the init.sql file
66 | # - ./server/config/init.sql:/docker-entrypoint-initdb.d/init.sql
67 | # pgadmin:
68 | # image: dpage/pgadmin4
69 | # environment:
70 | # PGADMIN_DEFAULT_EMAIL: "dhatguy@mail.com"
71 | # PGADMIN_DEFAULT_PASSWORD: "qwertyuiop"
72 | # ports:
73 | # - "16543:80"
74 | # depends_on:
75 | # - database
76 | networks:
77 | pernstore:
78 | driver: bridge
79 | volumes:
80 | db:
81 | driver: local
82 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | server:
5 | image: pern-store-server
6 | container_name: pern-store-server
7 | build:
8 | context: ./server
9 | dockerfile: Dockerfile
10 | # restart: always
11 | env_file:
12 | - ./server/.env
13 | environment:
14 | - POSTGRES_HOST=3.84.191.228
15 | - POSTGRES_DB=pernstore
16 | - POSTGRES_USER=postgres
17 | - POSTGRES_PASSWORD=newpassword
18 | volumes:
19 | # Maps the server directory to
20 | # the working directory in the container
21 | - ./server:/usr/src/app
22 | # Maps the node_modules directory to
23 | # the working directory in the container
24 | # and also fixes bcrypt error
25 | - /usr/src/app/node_modules
26 | ports:
27 | - 9000:9000
28 | networks:
29 | - pernstore
30 | # depends_on:
31 | # - database
32 | client:
33 | image: pern-store-client
34 | container_name: pern-store-client
35 | restart: unless-stopped
36 | env_file:
37 | - ./client/.env
38 | build:
39 | context: ./client
40 | dockerfile: Dockerfile
41 | ports:
42 | - 3001:3000
43 | volumes:
44 | - ./client:/usr/src/app
45 | - /usr/src/app/node_modules
46 | depends_on:
47 | - server
48 | networks:
49 | - pernstore
50 | # database:
51 | # container_name: pern-store-db
52 | # image: postgres
53 | # restart: always
54 | # env_file:
55 | # - ./server/.env
56 | # environment:
57 | # # other environment variables is in ./server/.env file
58 | # - POSTGRES_PASSWORD=adminuser
59 | # ports:
60 | # - 7890:5432
61 | # volumes:
62 | # - ./db:/var/lib/postgresql/data
63 | # # When the PostgreSQL container is started it will run any scripts
64 | # # provided in the `docker-entrypoint-initdb.d` directory
65 | # # seed the database with the init.sql file
66 | # - ./server/config/init.sql:/docker-entrypoint-initdb.d/init.sql
67 | # pgadmin:
68 | # image: dpage/pgadmin4
69 | # environment:
70 | # PGADMIN_DEFAULT_EMAIL: "dhatguy@mail.com"
71 | # PGADMIN_DEFAULT_PASSWORD: "qwertyuiop"
72 | # ports:
73 | # - "16543:80"
74 | # depends_on:
75 | # - database
76 | networks:
77 | pernstore:
78 | driver: bridge
79 | volumes:
80 | db:
81 | driver: local
82 |
--------------------------------------------------------------------------------
/server/docs/auth/googleLogin.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // operation's method
3 | post: {
4 | tags: ["Auth"], // operation's tag
5 | description: "Create a login via Google", // short desc
6 | summary: "Login with with Google",
7 | operationId: "googleLogin", // unique operation id
8 | parameters: [],
9 | requestBody: {
10 | // expected request body
11 | content: {
12 | // content-type
13 | "application/json": {
14 | schema: {
15 | type: "object",
16 | properties: {
17 | token: {
18 | type: "string",
19 | example:
20 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3MTllYjk1N2Y2OTU2YjU4MThjMTk2OGZm.zNzQ2MTY3NzItZGQ0bW05Z2FtYW02Z21NjQxMTAxNDYyMDgzMzURydWUsImF0X2hhc2giOiJZbUd5bm5nS05RVjA0ZmU0YkJfV0FBIiwibmFtZSI6Ikpvc2VwaCBPZHVuc2kiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2lFSTRQNlRTZF9URmtsLV83MWNySHVxcm1rQXM2bG9ZV2U1b1NsMW9vPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6Ikpvc2VwaCIsImZhbWlseV9uYW1lIjoiT2R1bnNpIiwibG9jYWxlIjoiZW4tR0IiLCJpYXQiOjE2MjIzOTMyNTYsImV4cCI6MTYyMjM5Njg1NiwianRpIjoiZDI0OWIzYzEzMGMxNjY2ZmQxMTYyNDU5Y2RlYTdiMGQ2MGQwODA3NSJ9.i22B4sDzegdSq1-CJ0FW9wRuTCvNHMBNrLHcEEfLZwZWVERhTpCJvoPkFdmSZWu8FQSY_q0IjDPT8UgDjNfHxZtGVGyva7CblrZUZRuWRtyJeSh9_xnW563suSEEGp7E-L8JXXjmoaQubiofeKIkT_Q1SYYm_MRNyWZpVjJ8wcEybWHBRs_XzbB4UFQNM31_96bbW8MvvVZmLnUDCeUMVHSs1dWJvbkW-ICVs4bMojOqWnXXWsDyELbmfmXNMCyYjBwt_yHKn5_L_PThKQ1ykAIt7dE6pDVoRe54V0WijPC1R7MT96TgwKZWfaXjMrlJ4o75CO7qoyCsPSI9KyH0Sg",
21 | },
22 | },
23 | },
24 | },
25 | },
26 | },
27 | // expected responses
28 | responses: {
29 | // response code
30 | 200: {
31 | description: "Login successful", // response desc
32 | headers: {
33 | "set-cookie": {
34 | description: "`refreshToken`",
35 | schema: {
36 | type: "string",
37 | example:
38 | "refreshToken=0IjoxNjIyMzEzMjI4LCJleHAiOjE2MjIzMTY4Mjh9.LXKZmJW1mUyoHOsmhYdFni8mcEhON4dPAxAtSKoEqCo; Path=/; HttpOnly; Secure; SameSite=None",
39 | },
40 | },
41 | },
42 | },
43 | 403: {
44 | description: "Invalid login", // response desc
45 | },
46 | // response code
47 | 500: {
48 | description: "Server error", // response desc
49 | },
50 | },
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/client/src/context/UserContext.jsx:
--------------------------------------------------------------------------------
1 | import API from "api/axios.config";
2 | import WithAxios from "helpers/WithAxios";
3 | import { createContext, useContext, useEffect, useState } from "react";
4 | import authService from "services/auth.service";
5 |
6 | const UserContext = createContext();
7 |
8 | const UserProvider = ({ children }) => {
9 | const [userData, setUserData] = useState(null);
10 | const [authData, setAuthData] = useState({
11 | token: "",
12 | });
13 | const [isLoggedIn, setIsLoggedIn] = useState(false);
14 |
15 | useEffect(() => {
16 | if (isLoggedIn) {
17 | authService.getCurrentUser().then((res) => setUserData(res?.data));
18 | }
19 | }, [isLoggedIn]);
20 |
21 | useEffect(() => {
22 | if (localStorage.getItem("token")) {
23 | setIsLoggedIn(true);
24 | setAuthData(JSON.parse(localStorage.getItem("token")));
25 | }
26 | }, []);
27 |
28 | const updateUserData = async ({ fullname, email, username, address, city, state, country }) => {
29 | const res = await API.put(`/users/${userData.user_id}`, {
30 | fullname,
31 | email,
32 | username,
33 | address,
34 | city,
35 | state,
36 | country,
37 | });
38 | setUserData(res.data);
39 | };
40 |
41 | const setUserInfo = (data) => {
42 | const { user, token } = data;
43 | setIsLoggedIn(true);
44 | setUserData(user);
45 | setAuthData({
46 | token,
47 | });
48 | localStorage.setItem("token", JSON.stringify(token));
49 | };
50 |
51 | const logout = () => {
52 | setUserData(null);
53 | setAuthData(null);
54 | setIsLoggedIn(false);
55 | authService.logout();
56 | };
57 |
58 | return (
59 | setUserInfo(data),
64 | logout,
65 | isLoggedIn,
66 | setIsLoggedIn,
67 | authData,
68 | setAuthData,
69 | updateUserData,
70 | }}
71 | >
72 | {children}
73 |
74 | );
75 | };
76 |
77 | const useUser = () => {
78 | const context = useContext(UserContext);
79 |
80 | if (context === undefined) {
81 | throw new Error("useUser must be used within UserProvider");
82 | }
83 | return context;
84 | };
85 |
86 | export { UserProvider, useUser };
87 |
--------------------------------------------------------------------------------
/client/src/components/Product.jsx:
--------------------------------------------------------------------------------
1 | import { Button, CardBody } from "@windmill/react-ui";
2 | import { useCart } from "context/CartContext";
3 | import { useState } from "react";
4 | import { ShoppingCart } from "react-feather";
5 | import toast from "react-hot-toast";
6 | import { Link } from "react-router-dom";
7 | import { ClipLoader } from "react-spinners";
8 | import { formatCurrency } from "../helpers/formatCurrency";
9 |
10 | const Product = ({ product }) => {
11 | const { addItem } = useCart();
12 | const [isLoading, setIsLoading] = useState(false);
13 |
14 | const addToCart = async (e) => {
15 | e.preventDefault();
16 | setIsLoading(true);
17 | try {
18 | await addItem(product, 1);
19 | toast.success("Added to cart");
20 | } catch (error) {
21 | console.log(error);
22 | toast.error("Error adding to cart");
23 | } finally {
24 | setIsLoading(false);
25 | }
26 | };
27 | return (
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 | {product.name}
43 |
44 | {formatCurrency(product.price)}
45 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default Product;
71 |
--------------------------------------------------------------------------------
/client/src/pages/Cart.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Table,
4 | TableBody,
5 | TableCell,
6 | TableContainer,
7 | TableFooter,
8 | TableHeader,
9 | TableRow,
10 | } from "@windmill/react-ui";
11 | import CartItem from "components/CartItem";
12 | import { useCart } from "context/CartContext";
13 | import { formatCurrency } from "helpers/formatCurrency";
14 | import Layout from "layout/Layout";
15 | import { ShoppingCart } from "react-feather";
16 | import { Link } from "react-router-dom";
17 |
18 | const Cart = () => {
19 | const { cartData, isLoading, cartSubtotal } = useCart();
20 |
21 | if (cartData?.items?.length === 0) {
22 | return (
23 |
24 | Shopping Cart
25 |
26 |
27 |
Cart is empty
28 |
31 |
32 |
33 | );
34 | }
35 |
36 | return (
37 |
38 | Shopping Cart
39 |
40 |
41 |
42 |
43 | Product
44 | Amount
45 | Quantity
46 | Total
47 | Remove
48 |
49 |
50 |
51 | {cartData?.items?.map((item) => {
52 | return (
53 |
54 |
55 |
56 | );
57 | })}
58 |
59 |
60 |
61 | Total: {formatCurrency(cartSubtotal)}
62 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Cart;
78 |
--------------------------------------------------------------------------------
/server/services/product.service.js:
--------------------------------------------------------------------------------
1 | const {
2 | getAllProductsDb,
3 | createProductDb,
4 | getProductDb,
5 | updateProductDb,
6 | deleteProductDb,
7 | getProductByNameDb,
8 | getProductBySlugDb,
9 | } = require("../db/product.db");
10 | const { ErrorHandler } = require("../helpers/error");
11 |
12 | class ProductService {
13 | getAllProducts = async (page) => {
14 | const limit = 12;
15 | const offset = (page - 1) * limit;
16 | try {
17 | return await getAllProductsDb({ limit, offset });
18 | } catch (error) {
19 | throw new ErrorHandler(error.statusCode, error.message);
20 | }
21 | };
22 |
23 | addProduct = async (data) => {
24 | try {
25 | return await createProductDb(data);
26 | } catch (error) {
27 | throw new ErrorHandler(error.statusCode, error.message);
28 | }
29 | };
30 |
31 | getProductById = async (id) => {
32 | try {
33 | const product = await getProductDb(id);
34 | if (!product) {
35 | throw new ErrorHandler(404, "product not found");
36 | }
37 | return product;
38 | } catch (error) {
39 | throw new ErrorHandler(error.statusCode, error.message);
40 | }
41 | };
42 |
43 | getProductBySlug = async (slug) => {
44 | try {
45 | const product = await getProductBySlugDb(slug);
46 | if (!product) {
47 | throw new ErrorHandler(404, "product not found");
48 | }
49 | return product;
50 | } catch (error) {
51 | throw new ErrorHandler(error.statusCode, error.message);
52 | }
53 | };
54 |
55 | getProductByName = async (name) => {
56 | try {
57 | const product = await getProductByNameDb(name);
58 | if (!product) {
59 | throw new ErrorHandler(404, "product not found");
60 | }
61 | } catch (error) {
62 | throw new ErrorHandler(error.statusCode, error.message);
63 | }
64 | };
65 |
66 | updateProduct = async (data) => {
67 | try {
68 | const product = await getProductDb(data.id);
69 | if (!product) {
70 | throw new ErrorHandler(404, "product not found");
71 | }
72 | return await updateProductDb(data);
73 | } catch (error) {
74 | throw new ErrorHandler(error.statusCode, error.message);
75 | }
76 | };
77 |
78 | removeProduct = async (id) => {
79 | try {
80 | const product = await getProductDb(id);
81 | if (!product) {
82 | throw new ErrorHandler(404, "product not found");
83 | }
84 | return await deleteProductDb(id);
85 | } catch (error) {
86 | throw new ErrorHandler(error.statusCode, error.message);
87 | }
88 | };
89 | }
90 |
91 | module.exports = new ProductService();
92 |
--------------------------------------------------------------------------------
/server/controllers/users.controller.js:
--------------------------------------------------------------------------------
1 | const userService = require("../services/user.service");
2 | const { ErrorHandler } = require("../helpers/error");
3 | const { hashPassword } = require("../helpers/hashPassword");
4 |
5 | const getAllUsers = async (req, res) => {
6 | const results = await userService.getAllUsers();
7 | res.status(200).json(results);
8 | };
9 |
10 | const createUser = async (req, res) => {
11 | const { username, password, email, fullname } = req.body;
12 | const hashedPassword = hashPassword(password);
13 |
14 | const user = await userService.createUser({
15 | username,
16 | hashedPassword,
17 | email,
18 | fullname,
19 | });
20 |
21 | res.status(201).json({
22 | status: "success",
23 | user,
24 | });
25 | };
26 |
27 | const getUserById = async (req, res) => {
28 | const { id } = req.params;
29 | if (+id === req.user.id || req.user.roles.includes("admin")) {
30 | try {
31 | const user = await userService.getUserById(id);
32 | return res.status(200).json(user);
33 | } catch (error) {
34 | throw new ErrorHandler(error.statusCode, "User not found");
35 | }
36 | }
37 | throw new ErrorHandler(401, "Unauthorized");
38 | };
39 |
40 | const getUserProfile = async (req, res) => {
41 | const { id } = req.user;
42 |
43 | const user = await userService.getUserById(id);
44 |
45 | return res.status(200).json(user);
46 | };
47 |
48 | const updateUser = async (req, res) => {
49 | const { username, email, fullname, address, city, state, country } = req.body;
50 | if (+req.params.id === req.user.id || req.user.roles.includes("admin")) {
51 | try {
52 | const results = await userService.updateUser({
53 | username,
54 | email,
55 | fullname,
56 | address,
57 | city,
58 | state,
59 | country,
60 | id: req.params.id,
61 | });
62 | return res.status(201).json(results);
63 | } catch (error) {
64 | throw new ErrorHandler(error.statusCode, error.message);
65 | }
66 | }
67 | throw new ErrorHandler(401, "Unauthorized");
68 | };
69 |
70 | const deleteUser = async (req, res) => {
71 | const { id } = req.params;
72 | if (+id === req.user.id || req.user.roles.includes("admin")) {
73 | try {
74 | const result = await userService.deleteUser(id);
75 | res.status(200).json(result);
76 | } catch (error) {
77 | throw new ErrorHandler(error.statusCode, error.message);
78 | }
79 | }
80 | throw new ErrorHandler(401, "Unauthorized");
81 | };
82 |
83 | module.exports = {
84 | getAllUsers,
85 | createUser,
86 | getUserById,
87 | updateUser,
88 | deleteUser,
89 | getUserProfile,
90 | };
91 |
--------------------------------------------------------------------------------
/client/src/pages/Orders.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Pagination,
3 | Table,
4 | TableBody,
5 | TableCell,
6 | TableContainer,
7 | TableFooter,
8 | TableHeader,
9 | TableRow,
10 | } from "@windmill/react-ui";
11 | import OrderItem from "components/OrderItem";
12 | import { useOrders } from "context/OrderContext";
13 | import Layout from "layout/Layout";
14 | import { useEffect, useState } from "react";
15 | import { useNavigate } from "react-router-dom";
16 | import orderService from "services/order.service";
17 |
18 | const Orders = () => {
19 | const { orders, setOrders } = useOrders();
20 | const [currentPage, setCurrentPage] = useState(1);
21 | const navigate = useNavigate();
22 |
23 | const handlePage = (num) => {
24 | setCurrentPage(num);
25 | };
26 |
27 | const goToDetails = (order) => {
28 | navigate(`/orders/${order.order_id}`, { state: { order } });
29 | };
30 |
31 | useEffect(() => {
32 | orderService.getAllOrders(currentPage).then((res) => setOrders(res.data));
33 | }, [currentPage, setOrders]);
34 |
35 | if (orders?.length === 0) {
36 | return (
37 |
38 | Orders
39 | You are yet to place an order
40 |
41 | );
42 | }
43 |
44 | return (
45 |
46 | Orders
47 |
48 |
49 |
50 |
51 | ID
52 | No. of items
53 | Status
54 | Amount
55 | Date
56 |
57 |
58 |
59 | {orders?.items.map((order) => (
60 | goToDetails(order)}
63 | key={order.order_id}
64 | >
65 |
66 |
67 | ))}
68 |
69 |
70 |
71 |
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default Orders;
84 |
--------------------------------------------------------------------------------
/server/db/product.db.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 |
3 | const getAllProductsDb = async ({ limit, offset }) => {
4 | const { rows } = await pool.query(
5 | `select products.*, trunc(avg(reviews.rating)) as avg_rating, count(reviews.*) from products
6 | LEFT JOIN reviews
7 | ON products.product_id = reviews.product_id
8 | group by products.product_id limit $1 offset $2 `,
9 | [limit, offset]
10 | );
11 | const products = [...rows].sort(() => Math.random() - 0.5);
12 | return products;
13 | };
14 |
15 | const createProductDb = async ({ name, price, description, image_url }) => {
16 | const { rows: product } = await pool.query(
17 | "INSERT INTO products(name, price, description, image_url) VALUES($1, $2, $3, $4) returning *",
18 | [name, price, description, image_url]
19 | );
20 | return product[0];
21 | };
22 |
23 | const getProductDb = async ({ id }) => {
24 | const { rows: product } = await pool.query(
25 | `select products.*, trunc(avg(reviews.rating),1) as avg_rating, count(reviews.*) from products
26 | LEFT JOIN reviews
27 | ON products.product_id = reviews.product_id
28 | where products.product_id = $1
29 | group by products.product_id`,
30 | [id]
31 | );
32 | return product[0];
33 | };
34 |
35 | const getProductBySlugDb = async ({ slug }) => {
36 | const { rows: product } = await pool.query(
37 | `select products.*, trunc(avg(reviews.rating),1) as avg_rating, count(reviews.*) from products
38 | LEFT JOIN reviews
39 | ON products.product_id = reviews.product_id
40 | where products.slug = $1
41 | group by products.product_id`,
42 | [slug]
43 | );
44 | return product[0];
45 | };
46 |
47 | const getProductByNameDb = async ({ name }) => {
48 | const { rows: product } = await pool.query(
49 | `select products.*, trunc(avg(reviews.rating),1) as avg_rating, count(reviews.*) from products
50 | LEFT JOIN reviews
51 | ON products.product_id = reviews.product_id
52 | where products.name = $1
53 | group by products.product_id`,
54 | [name]
55 | );
56 | return product[0];
57 | };
58 |
59 | const updateProductDb = async ({ name, price, description, image_url, id }) => {
60 | const { rows: product } = await pool.query(
61 | "UPDATE products set name = $1, price = $2, description = $3 image_url = $4 where product_id = $5 returning *",
62 | [name, price, description, image_url, id]
63 | );
64 | return product[0];
65 | };
66 |
67 | const deleteProductDb = async ({ id }) => {
68 | const { rows } = await pool.query(
69 | "DELETE FROM products where product_id = $1 returning *",
70 | [id]
71 | );
72 | return rows[0];
73 | };
74 |
75 | module.exports = {
76 | getProductDb,
77 | getProductByNameDb,
78 | createProductDb,
79 | updateProductDb,
80 | deleteProductDb,
81 | getAllProductsDb,
82 | getProductBySlugDb,
83 | };
84 |
--------------------------------------------------------------------------------
/server/db/user.db.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 |
3 | const getAllUsersDb = async () => {
4 | const { rows: users } = await pool.query("select * from users");
5 | return users;
6 | };
7 |
8 | const createUserDb = async ({ username, password, email, fullname }) => {
9 | const { rows: user } = await pool.query(
10 | `INSERT INTO users(username, password, email, fullname)
11 | VALUES($1, $2, $3, $4)
12 | returning user_id, username, email, fullname, roles, address, city, state, country, created_at`,
13 | [username, password, email, fullname]
14 | );
15 | return user[0];
16 | };
17 |
18 | const getUserByIdDb = async (id) => {
19 | const { rows: user } = await pool.query(
20 | "select users.*, cart.id as cart_id from users left join cart on cart.user_id = users.user_id where users.user_id = $1",
21 | [id]
22 | );
23 | return user[0];
24 | };
25 | const getUserByUsernameDb = async (username) => {
26 | const { rows: user } = await pool.query(
27 | "select users.*, cart.id as cart_id from users left join cart on cart.user_id = users.user_id where lower(users.username) = lower($1)",
28 | [username]
29 | );
30 | return user[0];
31 | };
32 |
33 | const getUserByEmailDb = async (email) => {
34 | const { rows: user } = await pool.query(
35 | "select users.*, cart.id as cart_id from users left join cart on cart.user_id = users.user_id where lower(email) = lower($1)",
36 | [email]
37 | );
38 | return user[0];
39 | };
40 |
41 | const updateUserDb = async ({
42 | username,
43 | email,
44 | fullname,
45 | id,
46 | address,
47 | city,
48 | state,
49 | country,
50 | }) => {
51 | const { rows: user } = await pool.query(
52 | `UPDATE users set username = $1, email = $2, fullname = $3, address = $4, city = $5, state = $6, country = $7
53 | where user_id = $8 returning username, email, fullname, user_id, address, city, country, state`,
54 | [username, email, fullname, address, city, state, country, id]
55 | );
56 | return user[0];
57 | };
58 |
59 | const deleteUserDb = async (id) => {
60 | const { rows: user } = await pool.query(
61 | "DELETE FROM users where user_id = $1 returning *",
62 | [id]
63 | );
64 | return user[0];
65 | };
66 |
67 | const createUserGoogleDb = async ({ sub, defaultUsername, email, name }) => {
68 | const { rows } = await pool.query(
69 | `INSERT INTO users(google_id,username, email, fullname)
70 | VALUES($1, $2, $3, $4) ON CONFLICT (email)
71 | DO UPDATE SET google_id = $1, fullname = $4 returning *`,
72 | [sub, defaultUsername, email, name]
73 | );
74 | return rows[0];
75 | };
76 |
77 | const changeUserPasswordDb = async (hashedPassword, email) => {
78 | return await pool.query("update users set password = $1 where email = $2", [
79 | hashedPassword,
80 | email,
81 | ]);
82 | };
83 |
84 | module.exports = {
85 | getAllUsersDb,
86 | getUserByIdDb,
87 | getUserByEmailDb,
88 | updateUserDb,
89 | createUserDb,
90 | createUserGoogleDb,
91 | deleteUserDb,
92 | getUserByUsernameDb,
93 | changeUserPasswordDb,
94 | };
95 |
--------------------------------------------------------------------------------
/client/src/layout/Layout.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "components/Nav";
2 | import Spinner from "components/Spinner";
3 | import { Helmet } from "react-helmet-async";
4 |
5 | const Layout = ({ children, title, loading }) => {
6 | return (
7 | <>
8 |
9 |
10 | {title ?? "Home"} | PERN Store
11 |
15 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
45 |
46 |
47 |
48 | {loading ? (
49 | <>
50 |
51 | >
52 | ) : (
53 |
54 | {children}
55 |
56 | )}
57 |
58 |
71 |
72 | >
73 | );
74 | };
75 |
76 | export default Layout;
77 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/network/main.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = var.region
3 | }
4 |
5 | # Create VPC
6 | resource "aws_vpc" "main" {
7 | cidr_block = var.vpc_cidr[var.environment]
8 | enable_dns_support = true
9 | enable_dns_hostnames = true
10 |
11 | tags = {
12 | Name = "${var.environment}-VPC"
13 | }
14 | }
15 |
16 | # Create Subnets (public and private)
17 | resource "aws_subnet" "subnets" {
18 | for_each = var.subnets[var.environment]
19 |
20 | vpc_id = aws_vpc.main.id
21 | cidr_block = each.value.cidr_block
22 | map_public_ip_on_launch = each.value.map_public_ip
23 | availability_zone = each.value.availability_zone
24 |
25 | tags = {
26 | Name = "${var.environment}-${each.value.name}"
27 | }
28 | }
29 |
30 | # Create Internet Gateway
31 | resource "aws_internet_gateway" "igw" {
32 | vpc_id = aws_vpc.main.id
33 |
34 | tags = {
35 | Name = "${var.environment}-IGW"
36 | }
37 | }
38 |
39 | # Elastic IP for NAT Gateway
40 | resource "aws_eip" "nat_eip" {
41 | domain = "vpc"
42 |
43 | tags = {
44 | Name = "${var.environment}-EIP"
45 | }
46 | }
47 |
48 | # Create NAT Gateway
49 | resource "aws_nat_gateway" "nat_gw" {
50 | allocation_id = aws_eip.nat_eip.id
51 | subnet_id = aws_subnet.subnets["public_az_1a"].id
52 |
53 | tags = {
54 | Name = "${var.environment}-NAT-Gateway"
55 | }
56 | }
57 |
58 | # Create Route Tables
59 | resource "aws_route_table" "public_rt" {
60 | vpc_id = aws_vpc.main.id
61 | route {
62 | cidr_block = "0.0.0.0/0"
63 | gateway_id = aws_internet_gateway.igw.id
64 | }
65 |
66 | route {
67 | cidr_block = var.vpc_cidr[var.environment]
68 | gateway_id = "local"
69 | }
70 |
71 | tags = {
72 | Name = "${var.environment}-Public-Route-Table"
73 | }
74 | }
75 |
76 | resource "aws_route_table" "private_rt" {
77 | vpc_id = aws_vpc.main.id
78 | route {
79 | cidr_block = "0.0.0.0/0"
80 | nat_gateway_id = aws_nat_gateway.nat_gw.id
81 | }
82 |
83 | route {
84 | cidr_block = var.vpc_cidr[var.environment]
85 | gateway_id = "local"
86 | }
87 |
88 | tags = {
89 | Name = "${var.environment}-Private-Route-Table"
90 | }
91 | }
92 |
93 | # Associate Route Tables with Subnets
94 | resource "aws_route_table_association" "route_association" {
95 | for_each = var.subnets[var.environment]
96 |
97 | subnet_id = aws_subnet.subnets[each.key].id
98 | route_table_id = each.value.map_public_ip ? aws_route_table.public_rt.id : aws_route_table.private_rt.id
99 | }
100 |
101 | # Bastion Host Configuration
102 | resource "aws_instance" "bastion_host" {
103 | ami = var.ami
104 | instance_type = var.instance_type
105 | subnet_id = aws_subnet.subnets["public_az_1a"].id
106 | security_groups = [ var.bastion_sg_id ]
107 | key_name = var.key_name
108 | tags = {
109 | Name = "${var.environment}-bastion-host"
110 | }
111 | }
112 |
113 | resource "aws_iam_instance_profile" "basiton_ssm_profile" {
114 | name = "${var.environment}-bastion-ssm-profile"
115 | role = var.bastion_ssm_role_name
116 | }
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/client/jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent any
3 |
4 | environment {
5 | GIT_REPO = 'https://github.com/Boora-Raman/PERN-Store.git'
6 | GIT_BRANCH = 'main'
7 | SONARQUBE_SERVER = 'SonarQube'
8 | NEXUS_PROTOCOL = "https"
9 | NEXUS_URL = "35.172.231.232:8082"
10 | NEXUS_REPOSITORY = "docker"
11 | NEXUS_CREDENTIAL_ID = "NexusCreds"
12 | IMAGE_NAME = "pern-app"
13 | IMAGE_TAG = "latest"
14 | PATH = "/usr/bin:/usr/local/bin:$PATH"
15 | }
16 |
17 | stages {
18 | stage('Clone Repository') {
19 | steps {
20 | script {
21 | deleteDir()
22 | echo "Cloning Git Repository..."
23 | checkout([$class: 'GitSCM', branches: [[name: "*/${GIT_BRANCH}"]],
24 | userRemoteConfigs: [[url: "${GIT_REPO}"]]])
25 | }
26 | }
27 | }
28 |
29 | stage('Verify Node.js Installation') {
30 | steps {
31 | sh 'node -v'
32 | sh 'npm -v'
33 | }
34 | }
35 |
36 | stage('Install Dependencies') {
37 | steps {
38 | script {
39 | echo "Installing dependencies..."
40 | sh 'npm install'
41 | }
42 | }
43 | }
44 |
45 | stage('SonarQube Analysis') {
46 | steps {
47 | script {
48 | echo "Running SonarQube Analysis..."
49 | def scannerHome = tool name: 'SonarScanner', type: 'hudson.plugins.sonar.SonarRunnerInstallation'
50 | withSonarQubeEnv(SONARQUBE_SERVER) {
51 | sh "${scannerHome}/bin/sonar-scanner"
52 | }
53 | }
54 | }
55 | }
56 |
57 | stage('Verify Docker') {
58 | steps {
59 | sh 'docker --version'
60 | }
61 | }
62 |
63 | stage('Build Docker Image') {
64 | steps {
65 | script {
66 | echo "Building Docker image..."
67 | sh """
68 | docker build -t ${IMAGE_NAME}:${IMAGE_TAG} -f client/Dockerfile client
69 | """
70 | }
71 | }
72 | }
73 |
74 | stage('Push Docker Image to Nexus') {
75 | steps {
76 | script {
77 | echo "Pushing Docker Image to Nexus..."
78 | sh """
79 | docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${NEXUS_URL}/${IMAGE_NAME}:${IMAGE_TAG}
80 | docker push ${NEXUS_URL}/${IMAGE_NAME}:${IMAGE_TAG}
81 | """
82 | }
83 | }
84 | }
85 | }
86 |
87 | post {
88 | always {
89 | echo 'Cleaning up...'
90 | cleanWs()
91 | }
92 | success {
93 | echo 'Build, test, and analysis succeeded!'
94 | }
95 | failure {
96 | echo 'Build, test, or analysis failed.'
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/server/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | const authService = require("../services/auth.service");
2 | const mail = require("../services/mail.service");
3 | const { ErrorHandler } = require("../helpers/error");
4 |
5 | const createAccount = async (req, res) => {
6 | const { token, refreshToken, user } = await authService.signUp(req.body);
7 |
8 | if (process.env.NODE_ENV !== "test") {
9 | await mail.signupMail(user.email, user.fullname.split(" ")[0]);
10 | }
11 |
12 | res.header("auth-token", token);
13 | res.cookie("refreshToken", refreshToken, {
14 | httpOnly: true,
15 | sameSite: process.env.NODE_ENV === "development" ? true : "none",
16 | secure: process.env.NODE_ENV === "development" ? false : true,
17 | });
18 | res.status(201).json({
19 | token,
20 | user,
21 | });
22 | };
23 |
24 | const loginUser = async (req, res) => {
25 | const { email, password } = req.body;
26 | const { token, refreshToken, user } = await authService.login(
27 | email,
28 | password
29 | );
30 |
31 | res.header("auth-token", token);
32 | res.cookie("refreshToken", refreshToken, {
33 | httpOnly: true,
34 | sameSite: process.env.NODE_ENV === "development" ? true : "none",
35 | secure: process.env.NODE_ENV === "development" ? false : true,
36 | });
37 | res.status(200).json({
38 | token,
39 | user,
40 | });
41 | };
42 |
43 | const googleLogin = async (req, res) => {
44 | const { code } = req.body;
45 |
46 | const user = await authService.googleLogin(code);
47 | res.header("auth-token", user.token);
48 | res.cookie("refreshToken", user.refreshToken, {
49 | httpOnly: true,
50 | });
51 | res.json(user);
52 | };
53 |
54 | const forgotPassword = async (req, res) => {
55 | const { email } = req.body;
56 |
57 | await authService.forgotPassword(email);
58 |
59 | res.json({ status: "OK" });
60 | };
61 |
62 | // verify password reset token
63 | const verifyResetToken = async (req, res) => {
64 | const { token, email } = req.body;
65 | const isTokenValid = await authService.verifyResetToken(token, email);
66 |
67 | if (!isTokenValid) {
68 | res.json({
69 | message: "Token has expired. Please try password reset again.",
70 | showForm: false,
71 | });
72 | } else {
73 | res.json({
74 | showForm: true,
75 | });
76 | }
77 | };
78 |
79 | const refreshToken = async (req, res) => {
80 | if (!req.cookies.refreshToken) {
81 | throw new ErrorHandler(401, "Token missing");
82 | }
83 | const tokens = await authService.generateRefreshToken(
84 | req.cookies.refreshToken
85 | );
86 | res.header("auth-token", tokens.token);
87 | res.cookie("refreshToken", tokens.refreshToken, {
88 | httpOnly: true,
89 | });
90 | res.json(tokens);
91 | };
92 |
93 | const resetPassword = async (req, res) => {
94 | const { password, password2, token, email } = req.body;
95 |
96 | await authService.resetPassword(password, password2, token, email);
97 |
98 | res.json({
99 | status: "OK",
100 | message: "Password reset. Please login with your new password.",
101 | });
102 | };
103 |
104 | module.exports = {
105 | createAccount,
106 | loginUser,
107 | googleLogin,
108 | forgotPassword,
109 | verifyResetToken,
110 | resetPassword,
111 | refreshToken,
112 | };
113 |
--------------------------------------------------------------------------------
/server/db/cart.db.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 |
3 | const createCartDb = async (userId) => {
4 | const { rows: cart } = await pool.query(
5 | "INSERT INTO cart(user_id) values($1) returning cart.id",
6 | [userId]
7 | );
8 |
9 | return cart[0];
10 | };
11 |
12 | const getCartDb = async (userId) => {
13 | // get cart items
14 | const cart = await pool.query(
15 | `SELECT products.*, cart_item.quantity, round((products.price * cart_item.quantity)::numeric, 2) as subtotal from users
16 | join cart on users.user_id = cart.user_id
17 | join cart_item on cart.id = cart_item.cart_id
18 | join products on products.product_id = cart_item.product_id
19 | where users.user_id = $1
20 | `,
21 | [userId]
22 | );
23 |
24 | return cart.rows;
25 | };
26 |
27 | // add item to cart
28 | const addItemDb = async ({ cart_id, product_id, quantity }) => {
29 | await pool.query(
30 | `INSERT INTO cart_item(cart_id, product_id, quantity)
31 | VALUES($1, $2, $3) ON CONFLICT (cart_id, product_id)
32 | DO UPDATE set quantity = cart_item.quantity + 1 returning *`,
33 | [cart_id, product_id, quantity]
34 | );
35 |
36 | const results = await pool.query(
37 | "Select products.*, cart_item.quantity, round((products.price * cart_item.quantity)::numeric, 2) as subtotal from cart_item join products on cart_item.product_id = products.product_id where cart_item.cart_id = $1",
38 | [cart_id]
39 | );
40 |
41 | return results.rows;
42 | };
43 |
44 | // delete item from cart
45 | const deleteItemDb = async ({ cart_id, product_id }) => {
46 | const result = await pool.query(
47 | "delete from cart_item where cart_id = $1 AND product_id = $2 returning *",
48 | [cart_id, product_id]
49 | );
50 | return result.rows[0];
51 | };
52 |
53 | // increment item quantity by 1
54 | const increaseItemQuantityDb = async ({ cart_id, product_id }) => {
55 | await pool.query(
56 | "update cart_item set quantity = quantity + 1 where cart_item.cart_id = $1 and cart_item.product_id = $2",
57 | [cart_id, product_id]
58 | );
59 |
60 | const results = await pool.query(
61 | `Select products.*, cart_item.quantity,
62 | round((products.price * cart_item.quantity)::numeric, 2) as subtotal
63 | from cart_item join products
64 | on cart_item.product_id = products.product_id
65 | where cart_item.cart_id = $1
66 | `,
67 | [cart_id]
68 | );
69 | return results.rows;
70 | };
71 |
72 | // decrement item quantity by 1
73 | const decreaseItemQuantityDb = async ({ cart_id, product_id }) => {
74 | await pool.query(
75 | "update cart_item set quantity = quantity - 1 where cart_item.cart_id = $1 AND cart_item.product_id = $2 returning *",
76 | [cart_id, product_id]
77 | );
78 |
79 | const results = await pool.query(
80 | "Select products.*, cart_item.quantity, round((products.price * cart_item.quantity)::numeric, 2) as subtotal from cart_item join products on cart_item.product_id = products.product_id where cart_item.cart_id = $1",
81 | [cart_id]
82 | );
83 | return results.rows;
84 | };
85 |
86 | const emptyCartDb = async (cartId) => {
87 | return await pool.query("delete from cart_item where cart_id = $1", [cartId]);
88 | };
89 |
90 | module.exports = {
91 | createCartDb,
92 | getCartDb,
93 | addItemDb,
94 | increaseItemQuantityDb,
95 | decreaseItemQuantityDb,
96 | deleteItemDb,
97 | emptyCartDb,
98 | };
99 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/artifacts/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_instance" "nexus" {
2 | ami = var.ami
3 | instance_type = var.instance_type
4 | key_name = var.key_name
5 | subnet_id = var.nexus_subnet
6 | security_groups = [var.nexus_sg]
7 |
8 | user_data = <<-EOF
9 | #!/bin/bash
10 |
11 | # Update and install Java
12 | echo "Updating system and installing Java..."
13 | sudo apt update -y
14 | sudo apt install openjdk-11-jdk -y
15 |
16 | # Verify Java installation
17 | java -version
18 | if [ $? -ne 0 ]; then
19 | echo "Java installation failed. Exiting."
20 | exit 1
21 | fi
22 |
23 | # Download Nexus
24 | NEXUS_VERSION="nexus-3.74.0-05-unix"
25 | NEXUS_URL="https://download.sonatype.com/nexus/3/$NEXUS_VERSION.tar.gz"
26 | echo "Downloading Nexus from $NEXUS_URL..."
27 | wget $NEXUS_URL -O /tmp/nexus.tar.gz
28 |
29 | # Extract Nexus and move to /opt
30 | echo "Extracting Nexus and moving it to /opt..."
31 | sudo tar -xvf /tmp/nexus.tar.gz -C /tmp
32 | sudo mv /tmp/nexus-3.* /opt/nexus
33 | sudo mkdir -p /opt/sonatype-work
34 |
35 | # Create Nexus user and set permissions
36 | echo "Creating 'nexus' user and setting permissions..."
37 | sudo useradd -r -m -d /opt/nexus -s /bin/bash nexus
38 | sudo chown -R nexus:nexus /opt/nexus
39 | sudo chown -R nexus:nexus /opt/sonatype-work
40 |
41 | # Update sudoers file for Nexus user
42 | echo "Updating sudoers file for 'nexus' user..."
43 | echo "nexus ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers
44 |
45 | # Update Nexus configuration to run as 'nexus' user
46 | echo "Configuring Nexus to run as 'nexus' user..."
47 | sudo sed -i 's/#run_as_user=""/run_as_user="nexus"/' /opt/nexus/bin/nexus.rc
48 |
49 | # Create a systemd service file for Nexus
50 | echo "Creating systemd service file for Nexus..."
51 | sudo tee /etc/systemd/system/nexus.service > /dev/null << EOL
52 | [Unit]
53 | Description=Nexus Repository Manager
54 | After=network.target
55 |
56 | [Service]
57 | Type=simple
58 | User=nexus
59 | Group=nexus
60 | ExecStart=/opt/nexus/bin/nexus start
61 | ExecStop=/opt/nexus/bin/nexus stop
62 | Restart=on-failure
63 | LimitNOFILE=65536
64 |
65 | [Install]
66 | WantedBy=multi-user.target
67 | EOL
68 |
69 | # Reload systemd daemon and start Nexus service
70 | echo "Starting Nexus service..."
71 | sudo systemctl daemon-reload
72 | sudo systemctl enable nexus
73 | sudo systemctl start nexus
74 |
75 | # Verify Nexus service status
76 | echo "Verifying Nexus service status..."
77 | sudo systemctl status nexus --no-pager
78 |
79 | echo "Nexus Repository Manager setup is complete!"
80 |
81 | EOF
82 |
83 | tags = {
84 | Name = "${var.enviroment}-nexus-repo"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/server/__tests__/controllers/auth.test.js:
--------------------------------------------------------------------------------
1 | const supertest = require("supertest");
2 | const app = require("../../app");
3 | const api = supertest(app);
4 | const pool = require("../../config");
5 | const bcrypt = require("bcrypt");
6 |
7 | beforeAll(async () => {
8 | await pool.query("DELETE FROM users");
9 | });
10 |
11 | describe("/api/auth/signup", () => {
12 | it("should create an account for user", async () => {
13 | const res = await api.post("/api/auth/signup").send({
14 | email: "email@email.com",
15 | password: "secret",
16 | fullname: "test db",
17 | username: "test",
18 | });
19 | expect(res.body).toHaveProperty("userId");
20 | expect(res.body).toHaveProperty("cartId");
21 | expect(res.statusCode).toBe(201);
22 | });
23 |
24 | describe("return error if username or email is taken", () => {
25 | beforeAll(async () => {
26 | await pool.query("DELETE FROM users");
27 | const hashedPassword = await bcrypt.hash("secret", 1);
28 | await pool.query(
29 | "INSERT INTO users(username, password, email, fullname) VALUES($1, $2, $3, $4) returning user_id",
30 | ["test", hashedPassword, "email@email.com", "test db"]
31 | );
32 | });
33 | it("should return error if username is taken", async () => {
34 | const res = await api
35 | .post("/api/auth/signup")
36 | .send({
37 | email: "odunsiolakunbi@yahoo.com",
38 | password: "secret",
39 | fullname: "test db",
40 | username: "test",
41 | })
42 | .expect(401);
43 |
44 | expect(res.body).toHaveProperty("message", "username taken already");
45 | expect(res.body).toHaveProperty("status", "error");
46 | });
47 |
48 | it("should return error if email is taken", async () => {
49 | const res = await api
50 | .post("/api/auth/signup")
51 | .send({
52 | email: "email@email.com",
53 | password: "secret",
54 | fullname: "test db",
55 | username: "newtest",
56 | })
57 | .expect(401);
58 |
59 | expect(res.body).toHaveProperty("message", "email taken already");
60 | expect(res.body).toHaveProperty("status", "error");
61 | });
62 | });
63 | });
64 |
65 | describe("/api/auth/login", () => {
66 | beforeEach(async () => {
67 | await api.post("/api/auth/signup").send({
68 | email: "email@email.com",
69 | password: "secret",
70 | fullname: "test db",
71 | username: "test",
72 | });
73 | });
74 |
75 | it("should login a user", async () => {
76 | const res = await api
77 | .post("/api/auth/login")
78 | .send({ email: "email@email.com", password: "secret" });
79 |
80 | expect(res.body).toHaveProperty("token");
81 | expect(res.body).toHaveProperty("user");
82 | expect(res.header).toHaveProperty("auth-token");
83 | expect(res.header).toHaveProperty("set-cookie");
84 | expect(res.statusCode).toBe(200);
85 | });
86 |
87 | it("should return error if invalid credentials is entered", async () => {
88 | const res = await api
89 | .post("/api/auth/login")
90 | .send({ email: "tt@email.com", password: "qwecret" })
91 | .expect(403);
92 |
93 | expect(res.body).toHaveProperty("status", "error");
94 | expect(res.body).toHaveProperty("message", "Email or password incorrect.");
95 | });
96 | });
97 |
98 | afterAll(async () => {
99 | await pool.end();
100 | });
101 |
--------------------------------------------------------------------------------
/server/services/user.service.js:
--------------------------------------------------------------------------------
1 | const {
2 | createUserDb,
3 | getUserByEmailDb,
4 | createUserGoogleDb,
5 | changeUserPasswordDb,
6 | getUserByIdDb,
7 | updateUserDb,
8 | deleteUserDb,
9 | getAllUsersDb,
10 | getUserByUsernameDb,
11 | } = require("../db/user.db");
12 | const { ErrorHandler } = require("../helpers/error");
13 |
14 | class UserService {
15 | createUser = async (user) => {
16 | try {
17 | return await createUserDb(user);
18 | } catch (error) {
19 | throw new ErrorHandler(error.statusCode, error.message);
20 | }
21 | };
22 | getUserByEmail = async (email) => {
23 | try {
24 | const user = await getUserByEmailDb(email);
25 | return user;
26 | } catch (error) {
27 | throw new ErrorHandler(error.statusCode, error.message);
28 | }
29 | };
30 | getUserByUsername = async (username) => {
31 | try {
32 | const user = await getUserByUsernameDb(username);
33 | return user;
34 | } catch (error) {
35 | throw new ErrorHandler(error.statusCode, error.message);
36 | }
37 | };
38 | getUserById = async (id) => {
39 | try {
40 | const user = await getUserByIdDb(id);
41 | user.password = undefined;
42 | user.google_id = undefined;
43 | user.cart_id = undefined;
44 | return user;
45 | } catch (error) {
46 | throw new ErrorHandler(error.statusCode, error.message);
47 | }
48 | };
49 | createGoogleAccount = async (user) => {
50 | try {
51 | return await createUserGoogleDb(user);
52 | } catch (error) {
53 | throw new ErrorHandler(error.statusCode, error.message);
54 | }
55 | };
56 | changeUserPassword = async (password, email) => {
57 | try {
58 | return await changeUserPasswordDb(password, email);
59 | } catch (error) {
60 | throw new ErrorHandler(error.statusCode, error.message);
61 | }
62 | };
63 | updateUser = async (user) => {
64 | const { email, username, id } = user;
65 | const errors = {};
66 | try {
67 | const getUser = await getUserByIdDb(id);
68 | const findUserByEmail = await getUserByEmailDb(email);
69 | const findUserByUsername = await getUserByUsernameDb(username);
70 | const emailChanged =
71 | email && getUser.email.toLowerCase() !== email.toLowerCase();
72 | const usernameChanged =
73 | username && getUser.username.toLowerCase() !== username.toLowerCase();
74 |
75 | if (emailChanged && typeof findUserByEmail === "object") {
76 | errors["email"] = "Email is already taken";
77 | }
78 | if (usernameChanged && typeof findUserByUsername === "object") {
79 | errors["username"] = "Username is already taken";
80 | }
81 |
82 | if (Object.keys(errors).length > 0) {
83 | throw new ErrorHandler(403, errors);
84 | }
85 |
86 | return await updateUserDb(user);
87 | } catch (error) {
88 | throw new ErrorHandler(error.statusCode, error.message);
89 | }
90 | };
91 |
92 | deleteUser = async (id) => {
93 | try {
94 | return await deleteUserDb(id);
95 | } catch (error) {
96 | throw new ErrorHandler(error.statusCode, error.message);
97 | }
98 | };
99 |
100 | getAllUsers = async () => {
101 | try {
102 | return await getAllUsersDb();
103 | } catch (error) {
104 | throw new ErrorHandler(error.statusCode, error.message);
105 | }
106 | };
107 | }
108 |
109 | module.exports = new UserService();
110 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/server/controllers/products.controller.js:
--------------------------------------------------------------------------------
1 | const pool = require("../config");
2 | const productService = require("../services/product.service");
3 |
4 | const getAllProducts = async (req, res) => {
5 | const { page = 1 } = req.query;
6 |
7 | const products = await productService.getAllProducts(page);
8 | res.json(products);
9 | };
10 |
11 | const createProduct = async (req, res) => {
12 | const newProduct = await productService.addProduct(req.body);
13 | res.status(200).json(newProduct);
14 | };
15 |
16 | const getProduct = async (req, res) => {
17 | const product = await productService.getProductById(req.params);
18 | res.status(200).json(product);
19 | };
20 |
21 | const getProductBySlug = async (req, res) => {
22 | const product = await productService.getProductBySlug(req.params);
23 | res.status(200).json(product);
24 | };
25 |
26 | const getProductByName = async (req, res) => {
27 | const product = await productService.getProductByName(req.params);
28 | res.status(200).json(product);
29 | };
30 | const updateProduct = async (req, res) => {
31 | const { name, price, description } = req.body;
32 | const { id } = req.params;
33 |
34 | const updatedProduct = await productService.updateProduct({
35 | name,
36 | price,
37 | description,
38 | id,
39 | });
40 | res.status(200).json(updatedProduct);
41 | };
42 |
43 | const deleteProduct = async (req, res) => {
44 | const { id } = req.params;
45 |
46 | const deletedProduct = await productService.removeProduct(id);
47 | res.status(200).json(deletedProduct);
48 | };
49 |
50 | // TODO create a service for reviews
51 |
52 | const getProductReviews = async (req, res) => {
53 | const { product_id, user_id } = req.query;
54 | try {
55 | // check if current logged user review exist for the product
56 | const reviewExist = await pool.query(
57 | "SELECT EXISTS (SELECT * FROM reviews where product_id = $1 and user_id = $2)",
58 | [product_id, user_id]
59 | );
60 |
61 | // get reviews associated with the product
62 | const reviews = await pool.query(
63 | `SELECT users.fullname as name, reviews.* FROM reviews
64 | join users
65 | on users.user_id = reviews.user_id
66 | WHERE product_id = $1`,
67 | [product_id]
68 | );
69 | res.status(200).json({
70 | reviewExist: reviewExist.rows[0].exists,
71 | reviews: reviews.rows,
72 | });
73 | } catch (error) {
74 | res.status(500).json(error);
75 | }
76 | };
77 |
78 | const createProductReview = async (req, res) => {
79 | const { product_id, content, rating } = req.body;
80 | const user_id = req.user.id;
81 |
82 | try {
83 | const result = await pool.query(
84 | `INSERT INTO reviews(user_id, product_id, content, rating)
85 | VALUES($1, $2, $3, $4) returning *
86 | `,
87 | [user_id, product_id, content, rating]
88 | );
89 | res.json(result.rows);
90 | } catch (error) {
91 | res.status(500).json(error.detail);
92 | }
93 | };
94 |
95 | const updateProductReview = async (req, res) => {
96 | const { content, rating, id } = req.body;
97 |
98 | try {
99 | const result = await pool.query(
100 | `UPDATE reviews set content = $1, rating = $2 where id = $3 returning *
101 | `,
102 | [content, rating, id]
103 | );
104 | res.json(result.rows);
105 | } catch (error) {
106 | res.status(500).json(error);
107 | }
108 | };
109 |
110 | module.exports = {
111 | getProduct,
112 | createProduct,
113 | updateProduct,
114 | deleteProduct,
115 | getAllProducts,
116 | getProductByName,
117 | getProductBySlug,
118 | getProductReviews,
119 | updateProductReview,
120 | createProductReview,
121 | };
122 |
--------------------------------------------------------------------------------
/client/src/components/ForgotPasswordModal.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Backdrop,
3 | Button,
4 | HelperText,
5 | Input,
6 | Label,
7 | Modal,
8 | ModalBody,
9 | ModalFooter,
10 | ModalHeader,
11 | } from "@windmill/react-ui";
12 | import { useState } from "react";
13 | import { useForm } from "react-hook-form";
14 | import toast from "react-hot-toast";
15 | import PulseLoader from "react-spinners/PulseLoader";
16 | import authService from "services/auth.service";
17 |
18 | const ForgotPasswordModal = () => {
19 | const [isOpen, setIsOpen] = useState(false);
20 | const [msg, setMsg] = useState("");
21 | const [isSending, setIsSending] = useState(false);
22 | const {
23 | handleSubmit,
24 | register,
25 | formState: { errors },
26 | } = useForm();
27 |
28 | const toggleModal = () => {
29 | setMsg("");
30 | setIsOpen(!isOpen);
31 | };
32 |
33 | const onSubmitReset = (data) => {
34 | setMsg("");
35 | setIsSending(true);
36 | authService
37 | .forgotPassword(data.email)
38 | .then((data) => {
39 | if (data.data.status === "OK") {
40 | setIsSending(false);
41 | toast.success("Email has been sent successfully.");
42 | setIsOpen(false);
43 | }
44 | })
45 | .catch((error) => {
46 | setIsSending(false);
47 | setMsg(error.response.data.message);
48 | });
49 | };
50 | return (
51 |
52 | <>
53 | {isOpen &&
}
54 |
setIsOpen(!isOpen)}
56 | className="mb-1 text-sm text-purple-700 cursor-pointer"
57 | >
58 | Forgot password?
59 |
60 |
61 |
108 |
109 | >
110 |
111 | );
112 | };
113 |
114 | export default ForgotPasswordModal;
115 |
--------------------------------------------------------------------------------
/Deployment/Terraform/modules/security/variables.tf:
--------------------------------------------------------------------------------
1 |
2 | variable "vpc_id" {
3 | type = string
4 | description = "VPC ID used for security group resources"
5 | }
6 |
7 | variable "environment" {
8 | type = string
9 | description = "Environment for tagging resources"
10 | }
11 |
12 | variable "bastion_ingress_rules" {
13 | type = map(object({
14 | from_port = number
15 | to_port = number
16 | protocol = string
17 |
18 | cidr_block = list(string)
19 | }))
20 | default = {
21 |
22 | http = {
23 | from_port = 80
24 | to_port = 80
25 | protocol = "tcp"
26 | cidr_block = ["0.0.0.0/0"]
27 | }
28 | https = {
29 | from_port = 443
30 | to_port = 443
31 | protocol = "tcp"
32 | cidr_block = ["0.0.0.0/0"]
33 | }
34 | ssh ={
35 | from_port = 22
36 | to_port = 22
37 | protocol = "tcp"
38 | cidr_block = ["0.0.0.0/0"]
39 | }
40 | }
41 | }
42 |
43 | variable "sonar_ingress_rules" {
44 | type = map(object({
45 | from_port = number
46 | to_port = number
47 | protocol = string
48 | cidr_block = list(string)
49 | }))
50 | default = {
51 | ssh = {
52 | from_port = 22
53 | to_port = 22
54 | protocol = "tcp"
55 | cidr_block = ["0.0.0.0/0"]
56 | }
57 | http = {
58 | from_port = 80
59 | to_port = 80
60 | protocol = "tcp"
61 | cidr_block = ["0.0.0.0/0"]
62 | }
63 | https = {
64 | from_port = 443
65 | to_port = 443
66 | protocol = "tcp"
67 | cidr_block = ["0.0.0.0/0"]
68 | }
69 | custom_tcp = {
70 | from_port = 9000
71 | to_port = 9000
72 | protocol = "tcp"
73 | cidr_block = ["0.0.0.0/0"]
74 | }
75 | }
76 | }
77 |
78 | variable "jenkins_ingress_rules" {
79 | type = map(object({
80 | from_port = number
81 | to_port = number
82 | protocol = string
83 | cidr_block = list(string)
84 | }))
85 |
86 | default = {
87 | "ssh" = {
88 | from_port = 22
89 | to_port = 22
90 | protocol = "tcp"
91 | cidr_block = ["0.0.0.0/0"]
92 | }
93 | "jenkins" = {
94 | from_port = 8080
95 | to_port = 8080
96 | protocol = "tcp"
97 | cidr_block = ["0.0.0.0/0"]
98 | }
99 | }
100 | }
101 |
102 | variable "sonar_alb_ingress_rules" {
103 | type = map(object({
104 | from_port = number
105 | to_port = number
106 | protocol = string
107 | cidr_block = list(string)
108 | }))
109 |
110 | default = {
111 | "http" = {
112 | from_port = 80
113 | to_port = 80
114 | protocol = "tcp"
115 | cidr_block = ["0.0.0.0/0"]
116 | }
117 | "sonar" = {
118 | from_port = 9000
119 | to_port = 9000
120 | protocol = "tcp"
121 | cidr_block = ["0.0.0.0/0"]
122 | }
123 | }
124 | }
125 |
126 | variable "nexus_ingress_rules" {
127 | type = map(object({
128 | from_port = number
129 | to_port = number
130 | protocol = string
131 | cidr_block = list(string)
132 | }))
133 |
134 | default = {
135 | "http" = {
136 | from_port = 80
137 | to_port = 80
138 | protocol = "tcp"
139 | cidr_block = ["0.0.0.0/0"]
140 | }
141 | "nexus" = {
142 | from_port = 8081
143 | to_port = 8081
144 | protocol = "tcp"
145 | cidr_block = ["0.0.0.0/0"]
146 | }
147 |
148 | "push_port" = {
149 | from_port = 8082
150 | to_port = 8082
151 | protocol = "tcp"
152 | cidr_block = ["0.0.0.0/0"]
153 | }
154 |
155 | "pull_port" = {
156 | from_port = 8083
157 | to_port = 8083
158 | protocol = "tcp"
159 | cidr_block = ["0.0.0.0/0"]
160 | }
161 |
162 | "ssh" = {
163 | from_port = 22
164 | to_port = 22
165 | protocol = "tcp"
166 | cidr_block = ["0.0.0.0/0"]
167 | }
168 |
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/client/src/components/PaymentForm.jsx:
--------------------------------------------------------------------------------
1 | import { CardElement, Elements, ElementsConsumer } from "@stripe/react-stripe-js";
2 | import { loadStripe } from "@stripe/stripe-js";
3 | import { Button, HelperText } from "@windmill/react-ui";
4 | import API from "api/axios.config";
5 | import { useCart } from "context/CartContext";
6 | import { formatCurrency } from "helpers/formatCurrency";
7 | import { useState } from "react";
8 | import { useNavigate } from "react-router-dom";
9 | import PulseLoader from "react-spinners/PulseLoader";
10 | import OrderService from "services/order.service";
11 | import OrderSummary from "./OrderSummary";
12 | import PaystackBtn from "./PaystackBtn";
13 |
14 | const PaymentForm = ({ previousStep, addressData, nextStep }) => {
15 | const { cartSubtotal, cartTotal, cartData, setCartData } = useCart();
16 | const [error, setError] = useState();
17 | const [isProcessing, setIsProcessing] = useState(false);
18 | const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUB_KEY);
19 | const navigate = useNavigate();
20 |
21 | const handleSubmit = async (e, elements, stripe) => {
22 | e.preventDefault();
23 | setError();
24 | const { fullname, email, address, city, state } = addressData;
25 | if (!stripe || !elements) {
26 | return;
27 | }
28 | try {
29 | setIsProcessing(true);
30 | const { data } = await API.post("/payment", {
31 | amount: (cartSubtotal * 100).toFixed(),
32 | email,
33 | });
34 |
35 | const card = elements.getElement(CardElement);
36 | const result = await stripe.createPaymentMethod({
37 | type: "card",
38 | card,
39 | billing_details: {
40 | name: fullname,
41 | email,
42 | address: {
43 | city,
44 | line1: address,
45 | state,
46 | country: "NG", // TODO: change later
47 | },
48 | },
49 | });
50 | if (result.error) {
51 | setError(result.error);
52 | }
53 |
54 | await stripe.confirmCardPayment(data.client_secret, {
55 | payment_method: result.paymentMethod.id,
56 | });
57 |
58 | OrderService.createOrder(cartSubtotal, cartTotal, data.id, "STRIPE").then(() => {
59 | setCartData({ ...cartData, items: [] });
60 | setIsProcessing(false);
61 | navigate("/cart/success", {
62 | state: {
63 | fromPaymentPage: true,
64 | },
65 | });
66 | });
67 | } catch (error) {
68 | setIsProcessing(false);
69 | // throw error
70 | }
71 | };
72 |
73 | return (
74 |
75 |
Checkout
76 |
77 |
Pay with Stripe
78 |
79 |
80 | {({ stripe, elements }) => (
81 |
97 | )}
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default PaymentForm;
106 |
--------------------------------------------------------------------------------
/client/src/components/ReviewModal.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Backdrop,
3 | Button,
4 | HelperText,
5 | Label,
6 | Modal,
7 | ModalBody,
8 | ModalFooter,
9 | ModalHeader,
10 | Textarea,
11 | } from "@windmill/react-ui";
12 | import { useUser } from "context/UserContext";
13 | import { useState } from "react";
14 | import toast from "react-hot-toast";
15 | import ReactStars from "react-rating-stars-component";
16 | import { useNavigate } from "react-router-dom";
17 | import reviewService from "services/review.service";
18 |
19 | const ReviewModal = ({ product_id, reviews }) => {
20 | const { userData } = useUser();
21 | const review = reviews.reviews.find((elm) => elm.user_id === userData?.user_id);
22 | const { reviewExist } = reviews;
23 | const [rating, setRating] = useState(1);
24 | const [content, setContent] = useState("");
25 | const [isOpen, setIsOpen] = useState(false);
26 | const navigate = useNavigate();
27 |
28 | const addReview = () => {
29 | reviewService
30 | .addReview(product_id, rating, content)
31 | .then(() => {
32 | toast.success("Review added successfully");
33 | setRating(1);
34 | setContent("");
35 | navigate(0);
36 | })
37 | .catch((error) => {
38 | toast.error("Error: ", error.response);
39 | });
40 | };
41 |
42 | const updateReview = () => {
43 | reviewService
44 | .updateReview(review.id, product_id, content, rating)
45 | .then(() => {
46 | toast.success("Review updated successfully");
47 | setRating(1);
48 | setContent("");
49 | navigate(0);
50 | })
51 | .catch((error) => {
52 | toast.error("Error: ", error.response);
53 | });
54 | };
55 |
56 | const handleSubmit = (e) => {
57 | e.preventDefault();
58 | reviewExist ? updateReview() : addReview();
59 | };
60 |
61 | const toggleModal = () => {
62 | setRating(reviewExist ? review.rating : 1);
63 | setContent(reviewExist ? review.content : "");
64 | setIsOpen(!isOpen);
65 | };
66 |
67 | return (
68 | <>
69 | {isOpen && }
70 |
71 |
72 |
73 |
74 | Add Review
75 |
76 |
103 |
104 |
105 |
108 |
115 |
116 |
117 | >
118 | );
119 | };
120 |
121 | export default ReviewModal;
122 |
--------------------------------------------------------------------------------
/client/src/context/CartContext.jsx:
--------------------------------------------------------------------------------
1 | import localCart from "helpers/localStorage";
2 | import { createContext, useContext, useEffect, useState } from "react";
3 | import cartService from "services/cart.service";
4 | import { useUser } from "./UserContext";
5 |
6 | const CartContext = createContext();
7 |
8 | const CartProvider = ({ children }) => {
9 | const [cartData, setCartData] = useState();
10 | const [cartSubtotal, setCartSubtotal] = useState(0);
11 | const [cartTotal, setCartTotal] = useState(0);
12 | const { isLoggedIn } = useUser();
13 | const [isLoading, setIsLoading] = useState(false);
14 |
15 | useEffect(() => {
16 | setIsLoading(true);
17 | if (isLoggedIn) {
18 | const saveLocalCart = async () => {
19 | const cartObj = localCart
20 | .getItems()
21 | .map(({ product_id, quantity }) => cartService.addToCart(product_id, quantity));
22 | await Promise.all(cartObj);
23 | localCart.clearCart();
24 | cartService.getCart().then((res) => {
25 | setCartData(res?.data);
26 | setIsLoading(false);
27 | });
28 | };
29 | saveLocalCart();
30 | } else {
31 | const items = localCart.getItems();
32 | if (items === null) {
33 | return;
34 | }
35 | setCartData({ items: [...items] });
36 | setIsLoading(false);
37 | }
38 | }, [isLoggedIn]);
39 |
40 | useEffect(() => {
41 | const quantity = cartData?.items?.reduce((acc, cur) => {
42 | return acc + Number(cur.quantity);
43 | }, 0);
44 | const totalAmt = cartData?.items.reduce((acc, cur) => {
45 | return acc + Number(cur.subtotal);
46 | }, 0);
47 | setCartSubtotal(totalAmt);
48 | setCartTotal(quantity);
49 | }, [cartData]);
50 |
51 | const addItem = async (product, quantity) => {
52 | if (isLoggedIn) {
53 | try {
54 | const { data } = await cartService.addToCart(product.product_id, quantity);
55 | setCartData({ items: [...data.data] });
56 | } catch (error) {
57 | return error;
58 | }
59 | } else {
60 | localCart.addItem(product, 1);
61 | setCartData({ ...cartData, items: localCart.getItems() });
62 | }
63 | };
64 |
65 | const deleteItem = (product_id) => {
66 | if (isLoggedIn) {
67 | const { items } = cartData;
68 | cartService.removeFromCart(product_id).then(() => {
69 | const data = items.filter((item) => item.product_id !== product_id);
70 | setCartData({ ...cartData, items: data });
71 | });
72 | } else {
73 | localCart.removeItem(product_id);
74 | setCartData({ ...cartData, items: localCart.getItems() });
75 | }
76 | };
77 |
78 | const increment = async (product_id) => {
79 | if (isLoggedIn) {
80 | const res = await cartService.increment(product_id);
81 | setCartData({ ...cartData, items: res.data });
82 | return res;
83 | } else {
84 | localCart.incrementQuantity(product_id);
85 | setCartData({ ...cartData, items: localCart.getItems() });
86 | }
87 | };
88 |
89 | const decrement = async (product_id) => {
90 | if (isLoggedIn) {
91 | const res = await cartService.decrement(product_id);
92 | setCartData({ ...cartData, items: res.data });
93 | return res;
94 | } else {
95 | localCart.decrementQuantity(product_id);
96 | setCartData({ ...cartData, items: localCart.getItems() });
97 | }
98 | };
99 |
100 | return (
101 |
114 | {children}
115 |
116 | );
117 | };
118 |
119 | const useCart = () => {
120 | const context = useContext(CartContext);
121 | if (context === undefined) {
122 | throw new Error("useCart must be used within a CartProvider");
123 | }
124 | return context;
125 | };
126 |
127 | export { CartProvider, useCart };
128 |
--------------------------------------------------------------------------------
/client/src/pages/ProductDetails.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@windmill/react-ui";
2 | import { useCart } from "context/CartContext";
3 | import { formatCurrency } from "helpers/formatCurrency";
4 | import Layout from "layout/Layout";
5 | import { useEffect, useState } from "react";
6 | import toast from "react-hot-toast";
7 | import ReactStars from "react-rating-stars-component";
8 | import { useNavigate, useParams } from "react-router-dom";
9 | import { ClipLoader } from "react-spinners";
10 | import productService from "services/product.service";
11 |
12 | const ProductDetails = () => {
13 | const { slug } = useParams();
14 | const [product, setProduct] = useState(null);
15 | const navigate = useNavigate();
16 | const { addItem } = useCart();
17 | const [isLoading, setIsLoading] = useState(false);
18 | const [isFetching, setIsFetching] = useState(false);
19 |
20 | const addToCart = async (e) => {
21 | e.preventDefault();
22 | setIsLoading(true);
23 | try {
24 | await addItem(product, 1);
25 | toast.success("Added to cart");
26 | } catch (error) {
27 | console.log(error);
28 | toast.error("Error adding to cart");
29 | } finally {
30 | setIsLoading(false);
31 | }
32 | };
33 |
34 | useEffect(() => {
35 | async function fetchData() {
36 | setIsFetching(true);
37 | try {
38 | const { data: product } = await productService.getProduct(slug);
39 | setProduct(product);
40 | } catch (error) {
41 | return navigate("/404", {
42 | replace: true,
43 | });
44 | } finally {
45 | setIsFetching(false);
46 | }
47 | }
48 | fetchData();
49 | }, [slug]);
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |

63 |
64 |
{product?.name}
65 |
66 |
67 |
74 |
75 | {+product?.count > 0 ? `${+product.count} Ratings` : "No ratings available"}
76 |
77 |
78 |
79 |
80 | {product?.description}
81 |
82 |
83 |
84 | {formatCurrency(product?.price)}
85 |
86 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default ProductDetails;
112 |
--------------------------------------------------------------------------------