├── backend ├── logs │ ├── combined │ │ └── .keep │ └── error │ │ └── .keep ├── .dockerignore ├── .env.development ├── docker-setup │ ├── mongo │ │ └── db-init.js │ └── node │ │ ├── development │ │ └── Dockerfile │ │ └── production │ │ └── Dockerfile ├── .env.production ├── helpers │ ├── rate-limiter.js │ ├── user-jwt-validate.js │ ├── params-validator.js │ └── logger.js ├── config │ ├── db-connection.js │ └── passport.js ├── package.json ├── app.js ├── models │ └── user.js └── routes │ └── user.js ├── frontend ├── src │ ├── App.scss │ ├── containers │ │ ├── Home │ │ │ ├── Home.scss │ │ │ └── Home.jsx │ │ ├── About │ │ │ ├── About.scss │ │ │ └── About.jsx │ │ ├── Profile │ │ │ ├── Profile.scss │ │ │ └── Profile.jsx │ │ ├── Login │ │ │ ├── Login.scss │ │ │ └── Login.jsx │ │ └── Register │ │ │ ├── Register.scss │ │ │ └── Register.jsx │ ├── components │ │ ├── FormField │ │ │ ├── FormField.scss │ │ │ └── FormField.jsx │ │ ├── Toast │ │ │ ├── CustomToast.scss │ │ │ └── CustomToast.jsx │ │ └── Navbar │ │ │ ├── Navbar.scss │ │ │ └── Navbar.jsx │ ├── api │ │ ├── routes.js │ │ └── axios.js │ ├── setupTests.js │ ├── App.test.js │ ├── reportWebVitals.js │ ├── index.scss │ ├── index.js │ ├── App.js │ └── contexts │ │ └── AuthContext.jsx ├── .dockerignore ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── docker-setup │ ├── react │ │ ├── development │ │ │ └── Dockerfile │ │ └── production │ │ │ └── Dockerfile │ └── nginx │ │ └── mern-template.conf ├── .gitignore ├── package.json └── README.md ├── images ├── 1.png ├── 2.png └── 3.png ├── LICENSE ├── docker-compose.production.yml ├── docker-compose.yml ├── .gitignore └── README.md /backend/logs/combined/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/logs/error/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /frontend/src/containers/Home/Home.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/containers/About/About.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/FormField/FormField.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/containers/Profile/Profile.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaranJagtiani/MERN-Docker-Production-Boilerplate/HEAD/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaranJagtiani/MERN-Docker-Production-Boilerplate/HEAD/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaranJagtiani/MERN-Docker-Production-Boilerplate/HEAD/images/3.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaranJagtiani/MERN-Docker-Production-Boilerplate/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaranJagtiani/MERN-Docker-Production-Boilerplate/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaranJagtiani/MERN-Docker-Production-Boilerplate/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/components/Toast/CustomToast.scss: -------------------------------------------------------------------------------- 1 | .toast-container { 2 | margin-top: 60px; 3 | } 4 | 5 | .error { 6 | color: #ffffff; 7 | background: #e5155a !important; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/api/routes.js: -------------------------------------------------------------------------------- 1 | // User 2 | export const loginRoute = "/user/login"; 3 | export const registerRoute = "/user/signup"; 4 | export const changePasswordRoute = "/user/update-password"; 5 | -------------------------------------------------------------------------------- /backend/.env.development: -------------------------------------------------------------------------------- 1 | JWT_SECRET= 2 | DB_HOST=mongo 3 | DB_PORT=27017 4 | DB_NAME=mern_docker_starter 5 | DB_USER=local_user 6 | DB_PASSWORD=Password123 7 | SERVER_PORT=4000 8 | -------------------------------------------------------------------------------- /backend/docker-setup/mongo/db-init.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: "local_user", 3 | pwd: "Password123", 4 | roles: [ 5 | { 6 | role: "readWrite", 7 | db: "mern_docker_starter", 8 | }, 9 | ], 10 | }); 11 | -------------------------------------------------------------------------------- /backend/.env.production: -------------------------------------------------------------------------------- 1 | JWT_SECRET= 2 | DB_HOST= 3 | DB_PORT= 4 | DB_NAME= 5 | DB_USER= 6 | DB_PASSWORD= 7 | SERVER_PORT= 8 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/helpers/rate-limiter.js: -------------------------------------------------------------------------------- 1 | const rateLimit = require("express-rate-limit"); 2 | 3 | const rateLimiter = rateLimit({ 4 | windowMs: 1 * 60 * 1000, 5 | max: 100, 6 | standardHeaders: true, 7 | legacyHeaders: false, 8 | }); 9 | 10 | module.exports = { 11 | rateLimiter, 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/docker-setup/react/development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-slim 2 | 3 | # Create app directory 4 | WORKDIR /frontend 5 | 6 | # Install app dependencies 7 | COPY package.json yarn.lock ./ 8 | 9 | COPY . . 10 | 11 | RUN yarn install 12 | 13 | ENV NODE_ENV=development 14 | 15 | CMD [ "yarn", "start" ] 16 | -------------------------------------------------------------------------------- /frontend/src/containers/Login/Login.scss: -------------------------------------------------------------------------------- 1 | .login-form { 2 | margin-top: 100px; 3 | 4 | .login-header { 5 | margin-bottom: 50px; 6 | } 7 | 8 | .form-control { 9 | padding: 10px; 10 | } 11 | 12 | .submit-btn { 13 | button { 14 | letter-spacing: 0.7px; 15 | font-weight: 600; 16 | font-size: 18px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/containers/Register/Register.scss: -------------------------------------------------------------------------------- 1 | .register-form { 2 | margin-top: 100px; 3 | 4 | .register-header { 5 | margin-bottom: 50px; 6 | } 7 | 8 | .form-control { 9 | padding: 10px; 10 | } 11 | 12 | .submit-btn { 13 | button { 14 | letter-spacing: 0.7px; 15 | font-weight: 600; 16 | font-size: 18px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/docker-setup/node/development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /backend 5 | 6 | RUN apt-get update && apt-get install -y vim 7 | 8 | # Install app dependencies 9 | COPY package*.json ./ 10 | 11 | # Install dependencies 12 | RUN npm install 13 | 14 | RUN npm i --location=global nodemon 15 | 16 | # Copy source directory 17 | COPY . . 18 | 19 | CMD [ "nodemon" ] 20 | -------------------------------------------------------------------------------- /frontend/.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.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased !important; 7 | -moz-osx-font-smoothing: grayscale !important; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/FormField/FormField.jsx: -------------------------------------------------------------------------------- 1 | import "./FormField.scss"; 2 | 3 | const FormField = (props) => { 4 | const { type, placeholder, setFunc } = props; 5 | 6 | return ( 7 | setFunc(event.target.value)} 14 | /> 15 | ); 16 | }; 17 | 18 | export default FormField; 19 | -------------------------------------------------------------------------------- /frontend/src/components/Toast/CustomToast.jsx: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | import "react-toastify/dist/ReactToastify.css"; 3 | 4 | import "./CustomToast.scss"; 5 | 6 | const toastOptions = { 7 | position: "top-left", 8 | autoClose: 3000, 9 | hideProgressBar: false, 10 | closeOnClick: true, 11 | pauseOnHover: true, 12 | }; 13 | 14 | const ColoredToast = (message) => { 15 | return toast(message, toastOptions); 16 | }; 17 | 18 | export { ColoredToast }; 19 | -------------------------------------------------------------------------------- /frontend/src/containers/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | import "./Home.scss"; 4 | 5 | function Home() { 6 | return ( 7 |
8 |
9 |

Welcome to the Home Page!

10 |

Believe you can and you're halfway there.

11 |
12 | 15 |
16 | ); 17 | } 18 | 19 | export default Home; 20 | -------------------------------------------------------------------------------- /backend/helpers/user-jwt-validate.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | module.exports.validateUserJWTToken = function (user_token) { 4 | if (!user_token) return false; 5 | if (user_token.length <= 4) return false; 6 | let token = user_token.substr(4, user_token.length); 7 | try { 8 | const verified = jwt.verify(token, process.env.JWT_SECRET); 9 | if (!verified || !verified.data) return false; 10 | return verified.data; 11 | } catch (error) { 12 | return false; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/containers/About/About.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | import "./About.scss"; 4 | 5 | function About() { 6 | return ( 7 |
8 |
9 |

Welcome to the About Page!

10 |

11 | You are never too old to set another goal or to dream a new dream. 12 |

13 |
14 | 17 |
18 | ); 19 | } 20 | 21 | export default About; 22 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/api/axios.js: -------------------------------------------------------------------------------- 1 | import axiosDefault from "axios"; 2 | 3 | const isProduction = process.env.NODE_ENV === "production"; 4 | 5 | const baseURL = isProduction 6 | ? "https:///api" 7 | : "http://localhost:5000"; 8 | 9 | const defaultOptions = { 10 | baseURL, 11 | headers: { 12 | "Content-Type": "application/json", 13 | }, 14 | }; 15 | 16 | const axios = axiosDefault.create(defaultOptions); 17 | 18 | axios.interceptors.request.use((config) => { 19 | const token = localStorage.getItem("token"); 20 | config.headers.Authorization = token || ""; 21 | return config; 22 | }); 23 | 24 | export default axios; 25 | -------------------------------------------------------------------------------- /backend/config/db-connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const DB_HOST = process.env.DB_HOST; 4 | const DB_PORT = process.env.DB_PORT; 5 | const DB_NAME = process.env.DB_NAME; 6 | const DB_USER = process.env.DB_USER; 7 | const DB_PASSWORD = process.env.DB_PASSWORD; 8 | 9 | const DB_URL = `mongodb://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`; 10 | console.log(DB_URL); 11 | 12 | const mongoOpts = { 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | }; 16 | 17 | mongoose.connect(DB_URL, mongoOpts, (err, res) => { 18 | if (err) { 19 | console.error(err); 20 | } else { 21 | console.log("\nConnected to the Database."); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar/Navbar.scss: -------------------------------------------------------------------------------- 1 | .nav-bar { 2 | text-align: center; 3 | 4 | a { 5 | font-weight: 500; 6 | font-size: 20px !important; 7 | text-decoration: none; 8 | color: #afafaf; 9 | margin-left: 40px; 10 | padding: 10px 0; 11 | } 12 | 13 | a:hover { 14 | color: #f1f1f1; 15 | transition: 0.6s all; 16 | } 17 | 18 | .nav-heading { 19 | a { 20 | font-weight: 600; 21 | color: #e3e3e3; 22 | } 23 | a:hover { 24 | color: #ffffff; 25 | } 26 | } 27 | 28 | .nav-heading:hover { 29 | cursor: pointer; 30 | } 31 | 32 | .active { 33 | color: #ffffff; 34 | } 35 | } 36 | 37 | .logout-icon { 38 | color: #cd204f !important; 39 | } 40 | -------------------------------------------------------------------------------- /backend/docker-setup/node/production/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-slim 2 | 3 | ENV USER=node 4 | 5 | RUN apt-get update && apt-get install -y vim 6 | 7 | RUN mkdir -p /home/node/app 8 | 9 | WORKDIR /home/node/app 10 | 11 | COPY . . 12 | 13 | COPY package*.json ./ 14 | 15 | RUN chown -R node:node /home/node/app 16 | 17 | RUN mkdir /home/node/.npm-global 18 | 19 | RUN chown -R 1000:1000 /home/node/.npm-global 20 | 21 | ENV PATH=/home/node/.npm-global/bin:$PATH 22 | 23 | ENV NPM_CONFIG_PREFIX=/home/node/.npm-global 24 | 25 | RUN npm --global config set user "${USER}" 26 | 27 | USER node 28 | 29 | RUN npm install --loglevel=warn; 30 | 31 | RUN npm i --location=global pm2 32 | 33 | CMD [ "pm2-runtime", "--no-autorestart", "npm", "--", "run", "start-prod" ] 34 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.scss"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | 8 | import "bootstrap/dist/css/bootstrap.min.css"; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById("root")); 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | // If you want to start measuring performance in your app, pass a function 20 | // to log results (for example: reportWebVitals(console.log)) 21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 22 | reportWebVitals(); 23 | -------------------------------------------------------------------------------- /backend/config/passport.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require("passport-jwt").Strategy; 2 | const ExtractJwt = require("passport-jwt").ExtractJwt; 3 | const User = require("../models/user"); 4 | 5 | module.exports = function (passport) { 6 | let opts = {}; 7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("jwt"); 8 | opts.secretOrKey = process.env.JWT_SECRET; 9 | 10 | passport.use( 11 | "user", 12 | new JwtStrategy(opts, (jwt_payload, done) => { 13 | User.getUserById(jwt_payload.data._id, (err, user) => { 14 | if (err) { 15 | return done(err, false); 16 | } 17 | if (user) { 18 | return done(null, user); 19 | } else { 20 | return done(null, false); 21 | } 22 | }); 23 | }) 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from "react-toastify"; 2 | 3 | import NavBar from "./components/Navbar/Navbar"; 4 | import { AuthContextProvider } from "./contexts/AuthContext"; 5 | 6 | import "./App.scss"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | 9 | function App() { 10 | return ( 11 | 12 |
13 | 14 | 15 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /backend/helpers/params-validator.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | const lodash = require("lodash"); 3 | 4 | const validateParams = function (paramSchema) { 5 | return async (req, res, next) => { 6 | const schema = Joi.object().keys(paramSchema); 7 | const paramSchemaKeys = Object.keys(paramSchema); 8 | let requestParamObj = {}; 9 | for (let key of paramSchemaKeys) { 10 | requestParamObj[key] = lodash.get(req.body, key); 11 | } 12 | try { 13 | await schema.validateAsync(requestParamObj); 14 | } catch (err) { 15 | return res.status(422).json({ 16 | success: false, 17 | msg: err.details[0].message, // Something went wrong. 18 | }); 19 | } 20 | next(); 21 | }; 22 | }; 23 | 24 | module.exports = { 25 | validateParams: validateParams, 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/contexts/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | export const AuthContext = createContext(); 4 | 5 | const { Provider } = AuthContext; 6 | 7 | export function useAuthContext() { 8 | const contextValues = useContext(AuthContext); 9 | return contextValues; 10 | } 11 | 12 | export function AuthContextProvider({ children }) { 13 | const [isLoggedIn, setIsLoggedIn] = useState(); 14 | const [user, setUser] = useState(); 15 | 16 | // Effects 17 | useEffect(() => { 18 | const isLogin = !!localStorage.getItem("token"); 19 | if (isLogin) setIsLoggedIn(true); 20 | }, []); 21 | 22 | return ( 23 | 31 | {children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-docker-nginx-backend", 3 | "version": "1.0.0", 4 | "description": "Backend application built using Node JS & Express JS", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start-prod": "NODE_ENV=production pm2 start app.js", 9 | "scan": "auditjs ossi" 10 | }, 11 | "author": "Karan Jagtiani", 12 | "license": "MIT", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.0.1", 17 | "express": "^4.18.1", 18 | "express-rate-limit": "^6.4.0", 19 | "express-session": "^1.17.3", 20 | "express-winston": "^4.2.0", 21 | "helmet": "^5.1.0", 22 | "joi": "^17.6.0", 23 | "jsonwebtoken": "^8.5.1", 24 | "lodash": "^4.17.21", 25 | "mongoose": "^6.4.2", 26 | "passport": "^0.6.0", 27 | "passport-jwt": "^4.0.0", 28 | "winston": "^3.8.1", 29 | "winston-daily-rotate-file": "^4.7.1" 30 | }, 31 | "devDependencies": { 32 | "auditjs": "^4.0.37", 33 | "nodemon": "^2.0.18" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Karan Jagtiani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/helpers/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require("winston"); 2 | require("winston-daily-rotate-file"); 3 | const expressWinston = require("express-winston"); 4 | const { format } = winston; 5 | 6 | const infoLogger = expressWinston.logger({ 7 | transports: [ 8 | new winston.transports.DailyRotateFile({ 9 | filename: "logs/combined/combined-%DATE%.log", 10 | datePattern: "YYYY-MM-DD-HH", 11 | colorize: true, 12 | maxSize: "20m", 13 | maxFiles: "14d", 14 | zippedArchive: true, 15 | prepend: true, 16 | utc: true, 17 | }), 18 | ], 19 | }); 20 | 21 | const errorLogger = winston.createLogger({ 22 | level: "error", 23 | format: format.combine( 24 | format.errors({ stack: true }), 25 | format.metadata(), 26 | format.json() 27 | ), 28 | transports: [ 29 | new winston.transports.DailyRotateFile({ 30 | filename: "logs/error/error-%DATE%.log", 31 | datePattern: "YYYY-MM-DD-HH", 32 | colorize: true, 33 | maxSize: "20m", 34 | maxFiles: "14d", 35 | zippedArchive: true, 36 | prepend: true, 37 | utc: true, 38 | }), 39 | ], 40 | }); 41 | 42 | module.exports = { 43 | infoLogger, 44 | errorLogger, 45 | }; 46 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ 2 | path: `./.env.${process.env.NODE_ENV}`, 3 | }); 4 | require("./config/db-connection"); 5 | 6 | const express = require("express"); 7 | const expressSession = require("express-session"); 8 | const cors = require("cors"); 9 | const passport = require("passport"); 10 | const helmet = require("helmet"); 11 | 12 | const { infoLogger } = require("./helpers/logger"); 13 | const { rateLimiter } = require("./helpers/rate-limiter"); 14 | 15 | const app = express(); 16 | 17 | app.use(helmet()); 18 | app.use(cors()); 19 | app.use(express.json()); 20 | app.use( 21 | expressSession({ 22 | secret: process.env.JWT_SECRET, 23 | resave: true, 24 | saveUninitialized: true, 25 | }) 26 | ); 27 | app.use(passport.initialize()); 28 | app.use(passport.session()); 29 | require("./config/passport")(passport); 30 | app.use(infoLogger); 31 | app.use(rateLimiter); 32 | 33 | const users_route = require("./routes/user"); 34 | 35 | app.use("/user", users_route); 36 | 37 | // default case for unmatched routes 38 | app.use(function (req, res) { 39 | res.status(404); 40 | }); 41 | 42 | const port = process.env.SERVER_PORT; 43 | 44 | app.listen(port, () => { 45 | console.log(`\nServer Started on ${port}`); 46 | }); 47 | -------------------------------------------------------------------------------- /frontend/docker-setup/nginx/mern-template.conf: -------------------------------------------------------------------------------- 1 | upstream backend_upstream { 2 | server backend:4000; 3 | } 4 | 5 | server { 6 | listen 80 default_server; 7 | listen [::]:80 default_server; 8 | 9 | # Change 'localhost' below to your domain 10 | server_name localhost; 11 | 12 | server_tokens off; 13 | 14 | gzip on; 15 | gzip_proxied any; 16 | gzip_comp_level 4; 17 | gzip_types text/css application/javascript image/svg+xml; 18 | 19 | # HSTS protection 20 | add_header Strict-Transport-Security max-age=31536000; 21 | # XSS protection in old browsers 22 | add_header X-XSS-Protection "1; mode=block"; 23 | # Block leakage of information 24 | proxy_hide_header X-Powered-By; 25 | # Click-Jacking protection 26 | add_header X-Frame-Options "SAMEORIGIN"; 27 | # Prevents unexpected cross-origin information leakage 28 | add_header Referrer-Policy "strict-origin-when-cross-origin"; 29 | # MIME-sniffing protection in old browsers 30 | add_header X-Content-Type-Options nosniff; 31 | 32 | location / { 33 | root /usr/share/nginx/html; 34 | index index.html index.htm; 35 | try_files $uri $uri/ /index.html; 36 | } 37 | 38 | location /api { 39 | rewrite ^/api/(.*)$ /$1 break; 40 | proxy_pass http://backend_upstream; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/docker-setup/react/production/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Stage 1: React production build 3 | # 4 | FROM node:16-slim as frontend 5 | 6 | # Create app directory 7 | WORKDIR /frontend 8 | 9 | # Install app dependencies 10 | COPY package.json yarn.lock ./ 11 | 12 | RUN yarn install 13 | 14 | COPY . . 15 | 16 | ENV NODE_ENV=production 17 | 18 | RUN yarn build 19 | 20 | EXPOSE 3000 21 | 22 | # 23 | # Stage 2: Nginx as a proxy & static file server 24 | # 25 | FROM nginx:1.23.0 26 | 27 | # Set working directory to nginx asset directory 28 | WORKDIR /usr/share/nginx/html 29 | 30 | RUN apt-get update && apt-get install -y certbot python3-certbot-nginx vim 31 | 32 | # Remove default nginx configuration 33 | RUN rm /etc/nginx/conf.d/* 34 | 35 | # Remove default nginx static assets 36 | RUN rm -rf /usr/share/nginx/html/* 37 | 38 | # Copy static assets from builder stage 39 | COPY --from=frontend /frontend/build . 40 | 41 | # Update the nginx config with our own config file 42 | COPY --from=frontend /frontend/docker-setup/nginx/mern-template.conf /etc/nginx/conf.d/ 43 | 44 | EXPOSE 80 45 | 46 | RUN useradd -ms /bin/bash nonroot 47 | 48 | RUN chown -R nonroot:nonroot /etc/nginx/conf.d/ /var/cache/nginx/ /var/run/ 49 | 50 | USER nonroot 51 | 52 | # Containers run nginx with global directives and daemon off 53 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 54 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome": "^1.1.8", 7 | "@fortawesome/fontawesome-free-regular": "^5.0.13", 8 | "@fortawesome/fontawesome-free-solid": "^5.0.13", 9 | "@fortawesome/fontawesome-svg-core": "^6.1.1", 10 | "@fortawesome/free-solid-svg-icons": "^6.1.1", 11 | "@fortawesome/react-fontawesome": "^0.2.0", 12 | "@testing-library/jest-dom": "^5.16.4", 13 | "@testing-library/react": "^13.3.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "axios": "^0.27.2", 16 | "bootstrap": "^5.1.3", 17 | "react": "^18.2.0", 18 | "react-bootstrap": "^2.4.0", 19 | "react-dom": "^18.2.0", 20 | "react-router-dom": "^6.3.0", 21 | "react-scripts": "5.0.1", 22 | "react-toastify": "^9.0.5", 23 | "validator": "^13.7.0", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "sass": "^1.53.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo: 4 | container_name: mongo-mern-docker-boilerplate 5 | image: mongo 6 | restart: always 7 | ports: 8 | - "27017" 9 | networks: 10 | - default 11 | volumes: 12 | - ./backend/docker-setup/mongo/db-init.js:/docker-entrypoint-initdb.d/init-mongo.js:ro 13 | - ./backend/docker-setup/mongo/mongo-volume:/data/db 14 | environment: 15 | - MONGO_INITDB_DATABASE=mern_docker_starter 16 | - MONGO_INITDB_USER=local_user 17 | - MONGO_INITDB_PASSWORD=Password123 18 | 19 | backend: 20 | container_name: node-mern-docker-boilerplate 21 | build: 22 | context: ./backend 23 | dockerfile: docker-setup/node/production/Dockerfile 24 | networks: 25 | - default 26 | - frontend 27 | environment: 28 | - NODE_ENV=production 29 | env_file: 30 | - ./backend/.env.production 31 | tty: true 32 | stdin_open: true 33 | depends_on: 34 | - mongo 35 | ports: 36 | - "4000" 37 | 38 | frontend: 39 | container_name: react-mern-docker-boilerplate 40 | build: 41 | context: ./frontend 42 | dockerfile: docker-setup/react/production/Dockerfile 43 | networks: 44 | - frontend 45 | environment: 46 | - NODE_ENV=production 47 | depends_on: 48 | - backend 49 | tty: true 50 | stdin_open: true 51 | ports: 52 | - "80:80" 53 | - "443:443" 54 | 55 | networks: 56 | default: 57 | name: backend-network-mern-docker-boilerplate 58 | frontend: 59 | name: frontend-network-mern-docker-boilerplate 60 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo: 4 | container_name: mongo-mern-docker-boilerplate 5 | image: mongo 6 | restart: always 7 | ports: 8 | - "27018:27017" 9 | networks: 10 | - default 11 | volumes: 12 | - ./backend/docker-setup/mongo/db-init.js:/docker-entrypoint-initdb.d/init-mongo.js:ro 13 | - ./backend/docker-setup/mongo/mongo-volume:/data/db 14 | environment: 15 | - MONGO_INITDB_DATABASE=mern_docker_starter 16 | - MONGO_INITDB_USER=local_user 17 | - MONGO_INITDB_PASSWORD=Password123 18 | 19 | backend: 20 | container_name: node-mern-docker-boilerplate 21 | build: 22 | context: ./backend 23 | dockerfile: docker-setup/node/development/Dockerfile 24 | networks: 25 | - default 26 | - frontend 27 | volumes: 28 | - ./backend:/home/node/app 29 | environment: 30 | - NODE_ENV=development 31 | env_file: 32 | - ./backend/.env.development 33 | tty: true 34 | stdin_open: true 35 | depends_on: 36 | - mongo 37 | ports: 38 | - "5000:4000" 39 | 40 | frontend: 41 | container_name: react-mern-docker-boilerplate 42 | build: 43 | context: ./frontend 44 | dockerfile: docker-setup/react/development/Dockerfile 45 | networks: 46 | - frontend 47 | environment: 48 | - NODE_ENV=development 49 | volumes: 50 | - ./frontend:/frontend 51 | depends_on: 52 | - backend 53 | tty: true 54 | stdin_open: true 55 | ports: 56 | - "3000:3000" 57 | 58 | networks: 59 | default: 60 | name: backend-network-mern-docker-boilerplate 61 | frontend: 62 | name: frontend-network-mern-docker-boilerplate 63 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const bcrypt = require("bcryptjs"); 3 | 4 | const userSchema = mongoose.Schema({ 5 | email: { 6 | type: String, 7 | required: true, 8 | }, 9 | password: { 10 | type: String, 11 | required: true, 12 | }, 13 | name: { 14 | type: String, 15 | required: true, 16 | }, 17 | emailVerified: { 18 | type: Boolean, 19 | required: false, 20 | }, 21 | admin: { 22 | type: Boolean, 23 | required: false, 24 | }, 25 | }); 26 | 27 | const User = (module.exports = mongoose.model("User", userSchema)); 28 | 29 | module.exports.getUserById = function (id, callback) { 30 | User.findById({ _id: id }, callback); 31 | }; 32 | 33 | module.exports.authenticateUser = function (email, callback) { 34 | User.updateOne({ email: email }, { $set: { authenticated: true } }, callback); 35 | }; 36 | 37 | module.exports.getUserByEmail = function (email, callback) { 38 | const query = { email: email }; 39 | User.findOne(query, callback); 40 | }; 41 | 42 | module.exports.addUser = function (newUser, callback) { 43 | bcrypt.genSalt(10, (err, salt) => { 44 | bcrypt.hash(newUser.password, salt, (err, hash) => { 45 | if (err) throw err; 46 | newUser.password = hash; 47 | newUser.save(callback); 48 | }); 49 | }); 50 | }; 51 | 52 | module.exports.comparePassword = function (candidatePassword, hash, callback) { 53 | if (!candidatePassword) { 54 | return false; 55 | } 56 | bcrypt.compare(candidatePassword, hash, (err, isMatch) => { 57 | if (err) throw err; 58 | callback(null, isMatch); 59 | }); 60 | }; 61 | 62 | module.exports.updatePassword = function (newUser, callback) { 63 | bcrypt.genSalt(10, (err, salt) => { 64 | if (err) throw err; 65 | bcrypt.hash(newUser.newPassword, salt, (err, hash) => { 66 | if (err) throw err; 67 | newUser.newPassword = hash; 68 | User.updateOne( 69 | { email: newUser.email }, 70 | { $set: { password: newUser.newPassword } }, 71 | callback 72 | ); 73 | }); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | *.log.gz 4 | *audit.json 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | mongo-volume 108 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import { Routes, Route } from "react-router-dom"; 3 | 4 | import { Navbar, Container, Nav } from "react-bootstrap"; 5 | import { useAuthContext } from "../../contexts/AuthContext"; 6 | 7 | import Home from "../../containers/Home/Home"; 8 | import About from "../../containers/About/About"; 9 | import Login from "../../containers/Login/Login"; 10 | import Register from "../../containers/Register/Register"; 11 | import Profile from "../../containers/Profile/Profile"; 12 | 13 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 14 | import { faPowerOff } from "@fortawesome/fontawesome-free-solid"; 15 | 16 | import "./Navbar.scss"; 17 | 18 | const NavBar = () => { 19 | const { isLoggedIn, setIsLoggedIn } = useAuthContext(); 20 | 21 | return ( 22 | <> 23 |
24 | 25 | 26 | 27 | AppName 28 | 29 | 79 | 80 | 81 |
82 | 83 | 84 | } /> 85 | } /> 86 | {isLoggedIn && } />} 87 | {!isLoggedIn && } />} 88 | {!isLoggedIn && } />} 89 | 90 | 91 | ); 92 | }; 93 | 94 | export default NavBar; 95 | -------------------------------------------------------------------------------- /frontend/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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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 | -------------------------------------------------------------------------------- /frontend/src/containers/Profile/Profile.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import validator from "validator"; 4 | 5 | import { AuthContext } from "../../contexts/AuthContext"; 6 | 7 | import axios from "../../api/axios"; 8 | import { changePasswordRoute } from "../../api/routes"; 9 | import FormField from "../../components/FormField/FormField"; 10 | 11 | import "./Profile.scss"; 12 | 13 | const Profile = () => { 14 | const [currentPassword, setCurrentPassword] = useState(""); 15 | const [newPassword, setNewPassword] = useState(""); 16 | const [newConfirmPassword, setNewConfirmPassword] = useState(""); 17 | const [passwordError, setpasswordError] = useState(""); 18 | const [formValid, setFormValid] = useState(false); 19 | 20 | const { user } = useContext(AuthContext); 21 | 22 | const handleValidation = (event) => { 23 | let formIsValid = true; 24 | 25 | if ( 26 | !validator.isStrongPassword(currentPassword, { 27 | minLength: 8, 28 | minLowercase: 1, 29 | minUppercase: 1, 30 | minNumbers: 1, 31 | minSymbols: 1, 32 | }) 33 | ) { 34 | formIsValid = false; 35 | setpasswordError( 36 | "Must contain 8 characters, one uppercase letter, one lowercase letter, one number, and one special symbol" 37 | ); 38 | return false; 39 | } else { 40 | setpasswordError(""); 41 | formIsValid = true; 42 | } 43 | setFormValid(formIsValid); 44 | }; 45 | 46 | const loginSubmit = (e) => { 47 | e.preventDefault(); 48 | handleValidation(); 49 | 50 | if (formValid) { 51 | axios 52 | .post(changePasswordRoute, { 53 | email: user.email, 54 | currentPassword: currentPassword, 55 | newPassword: newPassword, 56 | newConfirmPassword: newConfirmPassword, 57 | }) 58 | .then(function (response) { 59 | toast.success("Password Changed Successfully!"); 60 | }) 61 | .catch(function (error) { 62 | toast.error(error.response.data.msg); 63 | }); 64 | } 65 | }; 66 | 67 | return ( 68 |
69 |
70 |
71 |
72 |

Change Password

73 |
74 |
Current Password
75 | 80 | 81 | {passwordError} 82 | 83 |
84 | 85 |
86 |
New Password
87 | 92 |
93 | 94 |
95 |
New Confirm Password
96 | 101 |
102 |
103 | 106 |
107 |
108 |
109 |
110 |
111 | ); 112 | }; 113 | 114 | export default Profile; 115 | -------------------------------------------------------------------------------- /frontend/src/containers/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import validator from "validator"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | import axios from "../../api/axios"; 7 | import { loginRoute } from "../../api/routes"; 8 | import FormField from "../../components/FormField/FormField"; 9 | import { AuthContext } from "../../contexts/AuthContext"; 10 | 11 | import "./Login.scss"; 12 | 13 | const Login = () => { 14 | const [password, setPassword] = useState(""); 15 | const [email, setEmail] = useState(""); 16 | const [passwordError, setpasswordError] = useState(""); 17 | const [emailError, setemailError] = useState(""); 18 | const [formValid, setFormValid] = useState(false); 19 | 20 | const { setIsLoggedIn, setUser } = useContext(AuthContext); 21 | const navigate = useNavigate(); 22 | 23 | const handleValidation = (event) => { 24 | let formIsValid = true; 25 | 26 | if (!validator.isEmail(email)) { 27 | formIsValid = false; 28 | setemailError("Email Not Valid"); 29 | return false; 30 | } else { 31 | setemailError(""); 32 | formIsValid = true; 33 | } 34 | 35 | if ( 36 | !validator.isStrongPassword(password, { 37 | minLength: 8, 38 | minLowercase: 1, 39 | minUppercase: 1, 40 | minNumbers: 1, 41 | minSymbols: 1, 42 | }) 43 | ) { 44 | formIsValid = false; 45 | setpasswordError( 46 | "Must contain 8 characters, one uppercase letter, one lowercase letter, one number, and one special symbol" 47 | ); 48 | return false; 49 | } else { 50 | setpasswordError(""); 51 | formIsValid = true; 52 | } 53 | setFormValid(formIsValid); 54 | }; 55 | 56 | const loginSubmit = (e) => { 57 | e.preventDefault(); 58 | handleValidation(); 59 | 60 | if (formValid) { 61 | axios 62 | .post(loginRoute, { 63 | email: email, 64 | password: password, 65 | }) 66 | .then(function (response) { 67 | toast.success("Logged In Successfully!"); 68 | setIsLoggedIn(true); 69 | setUser(response.data.user); 70 | localStorage.setItem("token", response.data.token); 71 | navigate("/"); 72 | }) 73 | .catch(function (error) { 74 | toast.error(error.response.data.msg); 75 | }); 76 | } 77 | }; 78 | 79 | return ( 80 |
81 |
82 |
83 |
84 |

Login

85 |
86 |
Email address
87 | 92 | 93 | {emailError} 94 | 95 |
96 |
97 |
Password
98 | 103 | 104 | {passwordError} 105 | 106 |
107 |
108 | 111 |
112 |
113 |
114 |
115 |
116 | ); 117 | }; 118 | 119 | export default Login; 120 | -------------------------------------------------------------------------------- /frontend/src/containers/Register/Register.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | import validator from "validator"; 6 | 7 | import axios from "../../api/axios"; 8 | import { registerRoute } from "../../api/routes"; 9 | import FormField from "../../components/FormField/FormField"; 10 | 11 | import "./Register.scss"; 12 | 13 | const Register = () => { 14 | const [email, setEmail] = useState(""); 15 | const [name, setName] = useState(""); 16 | const [password, setPassword] = useState(""); 17 | 18 | const [nameError, setNameError] = useState(""); 19 | const [emailError, setemailError] = useState(""); 20 | const [passwordError, setpasswordError] = useState(""); 21 | 22 | const [formValid, setFormValid] = useState(false); 23 | 24 | const navigate = useNavigate(); 25 | 26 | const handleValidation = (event) => { 27 | let formIsValid = true; 28 | 29 | if (!validator.isAlpha(name)) { 30 | formIsValid = false; 31 | setNameError("Name Not Valid"); 32 | return false; 33 | } else { 34 | setNameError(""); 35 | formIsValid = true; 36 | } 37 | 38 | if (!validator.isEmail(email)) { 39 | formIsValid = false; 40 | setemailError("Email Not Valid"); 41 | return false; 42 | } else { 43 | setemailError(""); 44 | formIsValid = true; 45 | } 46 | 47 | if ( 48 | !validator.isStrongPassword(password, { 49 | minLength: 8, 50 | minLowercase: 1, 51 | minUppercase: 1, 52 | minNumbers: 1, 53 | minSymbols: 1, 54 | }) 55 | ) { 56 | formIsValid = false; 57 | setpasswordError( 58 | "Must contain 8 characters, one uppercase letter, one lowercase letter, one number, and one special symbol" 59 | ); 60 | return false; 61 | } else { 62 | setpasswordError(""); 63 | formIsValid = true; 64 | } 65 | setFormValid(formIsValid); 66 | }; 67 | 68 | const loginSubmit = (e) => { 69 | e.preventDefault(); 70 | handleValidation(); 71 | 72 | if (formValid) { 73 | axios 74 | .post(registerRoute, { 75 | name: name, 76 | email: email, 77 | password: password, 78 | }) 79 | .then(function (response) { 80 | toast.success("Registration Successful!"); 81 | navigate("/login"); 82 | }) 83 | .catch(function (error) { 84 | toast.error(error.response.data.msg); 85 | }); 86 | } 87 | }; 88 | 89 | return ( 90 |
91 |
92 |
93 |
94 |

Register

95 |
96 |
Name
97 | 102 | 103 | {nameError} 104 | 105 |
106 |
107 |
Email address
108 | 113 | 114 | {emailError} 115 | 116 |
117 |
118 |
Password
119 | 124 | 125 | {passwordError} 126 | 127 |
128 |
129 | 132 |
133 |
134 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | export default Register; 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | A production ready & secure boilerplate for the MERN Stack that uses Docker & Nginx. 6 | 7 | - **Focus on the product and not the setup**. You can directly start working on your idea and not worry about the nitty gritty setup. 8 | - Most useful libraries already installed so you don't have to run those same npm commands and configure the same thing again and again. 9 | - Optimized for Production out of the box with security kept in mind. 10 | - Ready for local development! You just need to install two requirements! 11 | 12 | ### Requirements 13 | 1. Docker - [Install](https://docs.docker.com/engine/install/) 14 | 2. NodeJS - [Install](https://nodejs.org/en/download/) 15 | 16 | ### Technologies Used 17 | - React (v18) 18 | - Node (v16) 19 | - Express (v4) 20 | - MongoDB (latest) 21 | - Nginx (v1.23.0) 22 | - Docker (v20.10.7) 23 | 24 | ### Folder Structure 25 | ```project-root/ 26 | ├── backend/ 27 | │ ├── docker-setup/ 28 | │ └── ... 29 | └── frontend/ 30 | │ ├── docker-setup/ 31 | │ └── ... 32 | ├── docker-compose.yml 33 | ├── docker-compose.production.yml 34 | ``` 35 | 36 | ## Features 37 | ### Security 38 | 1. [Bcrypt](https://www.npmjs.com/package/bcrypt) is used for storing hashed passwords. 39 | 2. [Passport-JWT](https://www.npmjs.com/package/passport-jwt) is used for session management. 40 | 3. The [Helmet](https://www.npmjs.com/package/helmet) library is used for adding the security headers to every request. 41 | 4. [Winston](https://www.npmjs.com/package/winston) is used for logging the incoming request information and errors inside request handlers. The log files are `compressed` and are `rotated` every 14 days. 42 | 5. [Express-Rate-Limiter](https://www.npmjs.com/package/express-rate-limit) is used for limitimg the number of requests in a particular timeframe to avoid any DOS based attacks. 43 | 6. The [Joi](https://www.npmjs.com/package/joi) library is used for checking and validating the params for any given Express request. 44 | 7. Has [auditjs](https://www.npmjs.com/package/auditjs) installed as a dev dependency. Run the `npm run scan` command to check for any vulnerabilities in the packages installed in the Backend. 45 | 8. Only the Backend (NodeJS) container has access to the Database (MongoDB) container. 46 | 9. The production Dockerfiles have a non-root user created with specific permissions assigned to it. 47 | 48 | ### Architecture 49 | 1. Mounted volumes for both Frontend and the Backend for ease of development. 50 | 2. Seperate & Optimized Docker files for Development and Production. 51 | 52 | ### Backend 53 | 1. Environment files have been setup separately for development and production using [Dotenv](https://www.npmjs.com/package/dotenv). 54 | 2. [Mongoose](https://www.npmjs.com/package/mongoose) is used as an object modelling framework for MongoDB. 55 | 3. [Nodemon](https://www.npmjs.com/package/nodemon) is used to serve the Node application on the local environment for automatic reloading. 56 | 4. Docker setup folder structure: 57 | ``` 58 | docker-setup/ 59 | ├── mongo/ 60 | │ ├── mongo-volume 61 | │ └── db-init.js 62 | └── nodejs/ 63 | ├── development/ 64 | │ └── Dockerfile 65 | └── production/ 66 | └── Dockerfile 67 | ``` 68 | 69 | ### Frontend 70 | 1. Bootstrap used as the CSS library. 71 | 2. SCSS compatible. 72 | 3. React-Router enabled. 73 | 4. Font-Awesome used as the Icon library. 74 | 5. Axios enabled and configured as an custom interceptor that can send requests with a JWT token. 75 | 6. React-Tostify used for showing success / error messages. 76 | 7. Docker setup folder structure: 77 | ``` 78 | docker-setup/ 79 | ├── nginx/ 80 | │ ├── .conf 81 | └── react/ 82 | ├── development/ 83 | │ └── Dockerfile 84 | └── production/ 85 | └── Dockerfile 86 | ``` 87 | 88 | ## Local Development 89 |

90 | 91 |

92 | 93 | - Every container has a **external port** that can be used for communicating with them externally. 94 | - Any changes made to the codebase will automatically be reflected since the **volumes are mounted**. 95 | 96 | 1. Run the following command in both `frontend` & `backend` directories: 97 | ```bash 98 | npm install 99 | ``` 100 | 2. Run the `docker compose` command: 101 | ```bash 102 | docker compose up -d 103 | ``` 104 | 105 | 2. Check whether the 3 containers are running: 106 | ``` 107 | docker container ls 108 | ``` 109 | 110 | 3. The Backend APIs can be triggered by hitting the following URL: 111 | ``` 112 | http://localhost:5000 113 | ``` 114 | 115 | 4. The Frontend will be served on: 116 | ``` 117 | http://localhost:3000 118 | ``` 119 | 120 | 5. To connect any database UI software with the MongoDB container, use the following details: 121 | ``` 122 | Host: localhost 123 | Port: 27018 124 | Database Name: mern_docker_starter 125 | Database User: local_user 126 | Database Password: Password123 127 | ``` 128 | 129 | ## Production Setup 130 |

131 | 132 |

133 | 134 | - All the containers only have a internal port except the Frontend container which has ports `80` and `443` exposed. 135 | - The Frontend container is a `multi-stage` container that builds the production react build files first and then serves them using Nginx. 136 | - Nginx is responsible for `proxying` the requests based on the URL to either to the Frontend or the Backend containers. 137 | 138 | On your production setup, follow the steps given below to run the docker containers. 139 | 140 | 1. Change the environment variables in the `.env.production` file and accordingly change the database variables in the `docker-compose.production.yml` file. 141 | 142 | 2. Change the `localhost` mentioned as server in the `frontend/docker-setup/nginx/mern-template.conf` file to the domain you want. Example: 143 | ``` 144 | server_name example.com www.example.com; 145 | ``` 146 | 147 | 3. Run the `docker compose` command with the production compose file: 148 | ```bash 149 | docker compose -f ./docker-compose.production.yml up -d 150 | ``` 151 | 152 | The frontend container will be exposed on ports `80` and `443` for HTTPs. 153 | 154 | It also has `Certbot` installed on it, so you can create your free SSL certificate by following the next steps: 155 | 156 | 4. Access the frontend container's CLI 157 | ```bash 158 | docker exec -it bash 159 | ``` 160 | 161 | 5. Generate the SSL certificate using Certbot 162 | ```bash 163 | certbot --nginx -d www.example.com -d example.com 164 | ``` 165 | 166 | You are all set! You should be able to access your site through your domain. 167 | -------------------------------------------------------------------------------- /backend/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const passport = require("passport"); 4 | const jwt = require("jsonwebtoken"); 5 | 6 | const params_validator = require("../helpers/params-validator"); 7 | const jwt_validator = require("../helpers/user-jwt-validate"); 8 | const { errorLogger } = require("../helpers/logger"); 9 | 10 | const Joi = require("joi"); 11 | 12 | const User = require("../models/user"); 13 | 14 | router.post( 15 | "/signup", 16 | params_validator.validateParams({ 17 | email: Joi.string() 18 | .pattern( 19 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 20 | ) 21 | .required(), 22 | password: Joi.string() 23 | .pattern( 24 | /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&(=)<>.,/])[A-Za-z\d@$!%*#?&(=)<>.,/]{6,}$/ 25 | ) 26 | .max(20) 27 | .required(), 28 | name: Joi.string().min(2).max(40).required(), 29 | }), 30 | (req, res, next) => { 31 | let newUser = new User({ 32 | email: req.body.email, 33 | password: req.body.password, 34 | name: req.body.name, 35 | }); 36 | 37 | User.getUserByEmail(newUser.email, (err, user) => { 38 | if (err) { 39 | errorLogger.error(err); 40 | return res 41 | .status(422) 42 | .json({ success: false, msg: "Something went wrong." }); 43 | } 44 | if (user) { 45 | return res.status(422).json({ 46 | success: false, 47 | msg: "Email has already been registered with us.", 48 | }); 49 | } 50 | 51 | User.addUser(newUser, (err) => { 52 | if (err) { 53 | errorLogger.error(err); 54 | return res.status(422).json({ 55 | success: false, 56 | msg: "Something went wrong.", 57 | }); 58 | } 59 | res.status(200).json({ 60 | success: true, 61 | msg: "User registered successfully.", 62 | }); 63 | }); 64 | }); 65 | } 66 | ); 67 | 68 | router.post( 69 | "/login", 70 | params_validator.validateParams({ 71 | email: Joi.string().min(8).max(20).required(), 72 | password: Joi.string() 73 | .pattern( 74 | /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&(=)<>.,/])[A-Za-z\d@$!%*#?&(=)<>.,/]{6,}$/ 75 | ) 76 | .max(20) 77 | .required(), 78 | }), 79 | (req, res, next) => { 80 | const email = req.body.email; 81 | const password = req.body.password; 82 | 83 | User.getUserByEmail(email, (err, emailUser) => { 84 | if (err) { 85 | errorLogger.error(err); 86 | return res 87 | .status(422) 88 | .json({ success: false, msg: "Something went wrong." }); 89 | } 90 | if (!emailUser) { 91 | return res 92 | .status(422) 93 | .json({ success: false, msg: "Invalid credentials." }); 94 | } 95 | let finalUser = emailUser; 96 | User.comparePassword(password, finalUser.password, (err, isMatch) => { 97 | if (err) { 98 | errorLogger.error(err); 99 | return res 100 | .status(422) 101 | .json({ success: false, msg: "Something went wrong." }); 102 | } 103 | if (!isMatch) { 104 | return res 105 | .status(422) 106 | .json({ success: false, msg: "Invalid credentials." }); 107 | } 108 | 109 | const token = jwt.sign({ data: finalUser }, process.env.JWT_SECRET, {}); 110 | res.status(200).json({ 111 | msg: "Logged in Successfully.", 112 | success: true, 113 | token: "JWT " + token, 114 | user: { 115 | id: finalUser._id, 116 | email: finalUser.email, 117 | name: finalUser.studentName, 118 | admin: finalUser.admin, 119 | }, 120 | }); 121 | }); 122 | }); 123 | } 124 | ); 125 | 126 | router.get( 127 | "/profile", 128 | passport.authenticate("user", { session: false }), 129 | (req, res, next) => { 130 | res.status(200).json({ success: true, user: req.user }); 131 | } 132 | ); 133 | 134 | router.post( 135 | "/update-password", 136 | params_validator.validateParams({ 137 | email: Joi.string().max(20).required(), 138 | currentPassword: Joi.string().max(20).required(), 139 | newPassword: Joi.string() 140 | .pattern( 141 | /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&(=)<>.,/])[A-Za-z\d@$!%*#?&(=)<>.,/]{6,}$/ 142 | ) 143 | .max(20) 144 | .required(), 145 | newConfirmPassword: Joi.string().max(20).required(), 146 | }), 147 | (req, res, next) => { 148 | let user = jwt_validator.validateUserJWTToken(req.headers.authorization); 149 | if (!user) 150 | return res.status(422).json({ success: false, msg: "Invalid token." }); 151 | 152 | const newUser = { 153 | email: req.body.email, 154 | currentPassword: req.body.currentPassword, 155 | newPassword: req.body.newPassword, 156 | newConfirmPassword: req.body.newConfirmPassword, 157 | }; 158 | 159 | if (newUser.newPassword != newUser.newConfirmPassword) { 160 | return res.status(422).json({ 161 | success: false, 162 | msg: "Both password fields do not match.", 163 | }); 164 | } 165 | 166 | if (newUser.currentPassword == newUser.newPassword) { 167 | return res.status(422).json({ 168 | success: false, 169 | msg: "Current password matches with the new password.", 170 | }); 171 | } 172 | 173 | User.getUserByEmail(newUser.email, (err, user) => { 174 | if (err) { 175 | return res 176 | .status(422) 177 | .json({ success: false, msg: "Something went wrong." }); 178 | } 179 | if (!user) { 180 | return res.status(404).json({ success: false, msg: "User not found." }); 181 | } 182 | User.comparePassword( 183 | newUser.currentPassword, 184 | user.password, 185 | (err, isMatch) => { 186 | if (err) { 187 | errorLogger.error(err); 188 | 189 | return res 190 | .status(422) 191 | .json({ success: false, msg: "Something went wrong." }); 192 | } 193 | if (!isMatch) { 194 | return res 195 | .status(422) 196 | .json({ success: false, msg: "Incorrect password." }); 197 | } 198 | User.updatePassword(newUser, (err) => { 199 | if (err) { 200 | errorLogger.error(err); 201 | return res 202 | .status(422) 203 | .json({ success: false, msg: "Something went wrong." }); 204 | } 205 | return res 206 | .status(200) 207 | .json({ success: true, msg: "Password updated." }); 208 | }); 209 | } 210 | ); 211 | }); 212 | } 213 | ); 214 | 215 | module.exports = router; 216 | --------------------------------------------------------------------------------