├── 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 | {item.name} 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 |
9 |

No reviews yet

10 |
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 |
34 | 35 |
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 | {item.name} 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 | {product.name} 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 |
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 |
62 | Forgot Password 63 | 64 | 82 | {errors?.email && errors?.email.type === "required" && ( 83 | 84 | {errors.email.message} 85 | 86 | )} 87 | {errors?.email && errors?.email.type === "pattern" && ( 88 | 89 | {errors.email.message} 90 | 91 | )} 92 | {msg && ( 93 | 94 | {msg} 95 | 96 | )} 97 | 98 | 99 | 106 | 107 |
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 |
handleSubmit(e, elements, stripe)}> 82 | 83 | {error && {error.message}} 84 |
85 | 88 | 95 |
96 | 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 |
77 | 87 |