├── .DS_Store ├── client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .DS_Store ├── src │ ├── .DS_Store │ ├── img │ │ └── showcase.jpg │ ├── components │ │ ├── .DS_Store │ │ ├── layout │ │ │ ├── spinner.gif │ │ │ ├── Spinner.js │ │ │ ├── NotFound.js │ │ │ ├── Alert.js │ │ │ ├── Landing.js │ │ │ └── Navbar.js │ │ ├── dashboard │ │ │ └── Dashboard.js │ │ ├── routing │ │ │ ├── PrivateRoute.js │ │ │ └── Routes.js │ │ └── auth │ │ │ ├── Login.js │ │ │ └── Register.js │ ├── reducers │ │ ├── index.js │ │ ├── alert.js │ │ └── auth.js │ ├── index.js │ ├── setupTests.js │ ├── utils │ │ └── setAuthToken.js │ ├── actions │ │ ├── alert.js │ │ ├── types.js │ │ └── auth.js │ ├── store.js │ ├── App.css │ └── App.js └── package.json ├── config ├── default.json ├── production.json └── db.js ├── models └── User.js ├── middleware └── auth.js ├── server.js ├── package.json ├── README.md ├── .gitignore └── routes └── api ├── users.js └── auth.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/.DS_Store -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/.DS_Store -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongoURI": "yourmongodburi", 3 | "jwtSecret": "mysecrettoken" 4 | } 5 | -------------------------------------------------------------------------------- /client/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/src/.DS_Store -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongoURI": "yourmongodburi", 3 | "jwtSecret": "mysecrettoken" 4 | } 5 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/src/img/showcase.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/src/img/showcase.jpg -------------------------------------------------------------------------------- /client/src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/src/components/.DS_Store -------------------------------------------------------------------------------- /client/src/components/layout/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/mern-boilerplate/HEAD/client/src/components/layout/spinner.gif -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import auth from "./auth"; 3 | import alert from "./alert"; 4 | 5 | export default combineReducers({ 6 | auth, 7 | alert 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "bootstrap/dist/css/bootstrap.css"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /client/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/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/utils/setAuthToken.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const setAuthToken = token => { 4 | if (token) { 5 | axios.defaults.headers.common["x-auth-token"] = token; 6 | } else { 7 | delete axios.defaults.headers.common["x-auth-token"]; 8 | } 9 | }; 10 | 11 | export default setAuthToken; 12 | -------------------------------------------------------------------------------- /client/src/components/layout/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import spinner from "./spinner.gif"; 3 | 4 | export default () => ( 5 | 6 | Loading... 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /client/src/components/layout/NotFound.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | 3 | const NotFound = () => { 4 | return ( 5 | 6 |

7 | Page Not Found 8 |

9 |

Sorry, this page does not exist

10 |
11 | ); 12 | }; 13 | 14 | export default NotFound; 15 | -------------------------------------------------------------------------------- /client/src/actions/alert.js: -------------------------------------------------------------------------------- 1 | import uuid from "uuid"; 2 | import { SET_ALERT, REMOVE_ALERT } from "./types"; 3 | 4 | export const setAlert = (msg, alertType, timeout = 3000) => dispatch => { 5 | const id = uuid.v4(); 6 | dispatch({ 7 | type: SET_ALERT, 8 | payload: { msg, alertType, id } 9 | }); 10 | 11 | setTimeout(() => dispatch({ type: REMOVE_ALERT, payload: id }), timeout); 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const SET_ALERT = "SET_ALERT"; 2 | export const REMOVE_ALERT = "REMOVE_ALERT"; 3 | export const REGISTER_SUCCESS = "REGISTER_SUCCESS"; 4 | export const REGISTER_FAIL = "REGISTER_FAIL"; 5 | export const USER_LOADED = "USER_LOADED"; 6 | export const AUTH_ERROR = "AUTH_ERROR"; 7 | export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; 8 | export const LOGIN_FAIL = "LOGIN_FAIL"; 9 | export const LOGOUT = "LOGOUT"; 10 | -------------------------------------------------------------------------------- /client/src/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import { SET_ALERT, REMOVE_ALERT } from "../actions/types"; 2 | 3 | const initialState = []; 4 | 5 | export default function(state = initialState, action) { 6 | const { type, payload } = action; 7 | 8 | switch (type) { 9 | case SET_ALERT: 10 | return [...state, payload]; 11 | case REMOVE_ALERT: 12 | return state.filter(alert => alert.id !== payload); 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import thunk from "redux-thunk"; 4 | import rootReducer from "./reducers"; 5 | 6 | const initialState = {}; 7 | 8 | const middleware = [thunk]; 9 | 10 | const store = createStore( 11 | rootReducer, 12 | initialState, 13 | composeWithDevTools(applyMiddleware(...middleware)) 14 | ); 15 | 16 | export default store; 17 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true 7 | }, 8 | email: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | password: { 14 | type: String, 15 | required: true 16 | }, 17 | date: { 18 | type: Date, 19 | default: Date.now 20 | } 21 | }); 22 | 23 | module.exports = User = mongoose.model("user", UserSchema); 24 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const config = require("config"); 3 | const db = config.get("mongoURI"); 4 | 5 | const connectDB = async () => { 6 | try { 7 | await mongoose.connect(db, { 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useFindAndModify: false, 11 | useUnifiedTopology: true 12 | }); 13 | 14 | console.log("MongoDB Connected..."); 15 | } catch (err) { 16 | console.error(err.message); 17 | // Exit process with failure 18 | process.exit(1); 19 | } 20 | }; 21 | 22 | module.exports = connectDB; 23 | -------------------------------------------------------------------------------- /client/src/components/layout/Alert.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | 5 | const Alert = ({ alerts }) => 6 | alerts !== null && 7 | alerts.length > 0 && 8 | alerts.map(alert => ( 9 |
10 | {alert.msg} 11 |
12 | )); 13 | 14 | Alert.propTypes = { 15 | alerts: PropTypes.array.isRequired 16 | }; 17 | 18 | const mapStateToProps = state => ({ 19 | alerts: state.alert 20 | }); 21 | 22 | export default connect(mapStateToProps)(Alert); 23 | -------------------------------------------------------------------------------- /client/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 | -------------------------------------------------------------------------------- /client/src/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | 5 | const Dashboard = ({ auth: { user } }) => { 6 | return ( 7 | 8 |

Dashboard

9 |

10 | Welcome {user && user.name} 11 |

12 |
13 | ); 14 | }; 15 | 16 | Dashboard.propTypes = { 17 | auth: PropTypes.object.isRequired 18 | }; 19 | 20 | const mapStateToProps = state => ({ 21 | auth: state.auth 22 | }); 23 | 24 | export default connect(mapStateToProps, null)(Dashboard); 25 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | /* Landing Page */ 2 | .landing { 3 | position: relative; 4 | background: url("./img/showcase.jpg") no-repeat center center/cover; 5 | height: 100vh; 6 | } 7 | 8 | .landing-inner { 9 | color: #fff; 10 | height: 100%; 11 | width: 80%; 12 | margin: auto; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | text-align: center; 18 | } 19 | 20 | .buttons { 21 | margin: 0 0.5rem; 22 | } 23 | 24 | /* Overlay */ 25 | .dark-overlay { 26 | background-color: rgba(0, 0, 0, 0.7); 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | } 33 | 34 | .container { 35 | padding: 1rem 0; 36 | } 37 | -------------------------------------------------------------------------------- /client/src/components/routing/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { connect } from "react-redux"; 5 | 6 | const PrivateRoute = ({ 7 | component: Component, 8 | auth: { isAuthenticated, loading }, 9 | ...rest 10 | }) => ( 11 | 14 | !isAuthenticated && !loading ? ( 15 | 16 | ) : ( 17 | 18 | ) 19 | } 20 | /> 21 | ); 22 | 23 | PrivateRoute.propTypes = { 24 | auth: PropTypes.object.isRequired 25 | }; 26 | 27 | const mapStateToProps = state => ({ 28 | auth: state.auth 29 | }); 30 | 31 | export default connect(mapStateToProps)(PrivateRoute); 32 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const config = require("config"); 3 | 4 | module.exports = async function(req, res, next) { 5 | // Get token from header 6 | const token = req.header("x-auth-token"); 7 | 8 | // Check if not token 9 | if (!token) { 10 | return res.status(401).json({ msg: "No token, authorization denied" }); 11 | } 12 | 13 | // Verify token 14 | try { 15 | await jwt.verify(token, config.get("jwtSecret"), (error, decoded) => { 16 | if (error) { 17 | res.status(401).json({ msg: "Token is not valid" }); 18 | } else { 19 | req.user = decoded.user; 20 | next(); 21 | } 22 | }); 23 | } catch (err) { 24 | console.error("something wrong with auth middleware"); 25 | res.status(500).json({ msg: "Server Error" }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/components/routing/Routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import Register from "../auth/Register"; 4 | import Login from "../auth/Login"; 5 | import Dashboard from "../dashboard/Dashboard"; 6 | import Alert from "../layout/Alert"; 7 | import NotFound from "../layout/NotFound"; 8 | import PrivateRoute from "../routing/PrivateRoute"; 9 | 10 | const Routes = () => { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default Routes; 25 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const connectDB = require("./config/db"); 3 | const path = require("path"); 4 | 5 | const app = express(); 6 | 7 | // Connect Database 8 | connectDB(); 9 | 10 | // Init Middleware 11 | app.use(express.json({ extended: false })); 12 | 13 | // Define Routes 14 | app.use("/api/users", require("./routes/api/users")); 15 | app.use("/api/auth", require("./routes/api/auth")); 16 | 17 | // Serve static assets in production 18 | if (process.env.NODE_ENV === "production") { 19 | // Set static folder 20 | app.use(express.static("client/build")); 21 | 22 | app.get("*", (req, res) => { 23 | res.sendFile(path.resolve(__dirname, "client", "build", "index.html")); 24 | }); 25 | } 26 | 27 | const PORT = process.env.PORT || 5000; 28 | 29 | app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-boilerplate", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server", 8 | "server": "nodemon server", 9 | "client": "npm start --prefix client", 10 | "dev": "concurrently \"npm run server\" \"npm run client\"", 11 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "bootstrap": "^4.4.1", 18 | "config": "^3.2.4", 19 | "express": "^4.17.1", 20 | "express-validator": "^6.3.0", 21 | "gravatar": "^1.8.0", 22 | "jsonwebtoken": "^8.5.1", 23 | "mongoose": "^5.8.0", 24 | "react-bootstrap": "^1.0.0-beta.16", 25 | "request": "^2.88.0" 26 | }, 27 | "devDependencies": { 28 | "concurrently": "^5.0.1", 29 | "nodemon": "^2.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 20 | 21 | 22 | MERN Stack Boilerplate 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import Navbar from "./components/layout/Navbar"; 4 | import Landing from "./components/layout/Landing"; 5 | import Routes from "./components/routing/Routes"; 6 | 7 | // Redux 8 | import { Provider } from "react-redux"; 9 | import store from "./store"; 10 | import { loadUser } from "./actions/auth"; 11 | import setAuthToken from "./utils/setAuthToken"; 12 | 13 | import "./App.css"; 14 | 15 | if (localStorage.token) { 16 | setAuthToken(localStorage.token); 17 | } 18 | 19 | const App = () => { 20 | useEffect(() => { 21 | store.dispatch(loadUser()); 22 | }, []); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /client/src/components/layout/Landing.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, Redirect } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | import { Button } from "react-bootstrap"; 6 | 7 | const Landing = ({ isAuthenticated }) => { 8 | if (isAuthenticated) { 9 | return ; 10 | } 11 | 12 | return ( 13 |
14 |
15 |
16 |

MERN Stack Boilerplate

17 |

A boilerplate for building web apps with MERN stack

18 |
19 | 22 | 25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | Landing.propTypes = { 33 | isAuthenticated: PropTypes.bool 34 | }; 35 | 36 | const mapStateToProps = state => ({ 37 | isAuthenticated: state.auth.isAuthenticated 38 | }); 39 | 40 | export default connect(mapStateToProps)(Landing); 41 | -------------------------------------------------------------------------------- /client/src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | USER_LOADED, 5 | AUTH_ERROR, 6 | LOGIN_SUCCESS, 7 | LOGIN_FAIL, 8 | LOGOUT, 9 | ACCOUNT_DELETED 10 | } from "../actions/types"; 11 | 12 | const initialState = { 13 | token: localStorage.getItem("token"), 14 | isAuthenticated: null, 15 | loading: true, 16 | user: null 17 | }; 18 | 19 | export default function(state = initialState, action) { 20 | const { type, payload } = action; 21 | 22 | switch (type) { 23 | case USER_LOADED: 24 | return { 25 | ...state, 26 | isAuthenticated: true, 27 | loading: false, 28 | user: payload 29 | }; 30 | case REGISTER_SUCCESS: 31 | case LOGIN_SUCCESS: 32 | localStorage.setItem("token", payload.token); 33 | return { 34 | ...state, 35 | ...payload, 36 | isAuthenticated: true, 37 | loading: false 38 | }; 39 | case REGISTER_FAIL: 40 | case AUTH_ERROR: 41 | case LOGIN_FAIL: 42 | case LOGOUT: 43 | localStorage.removeItem("token"); 44 | return { 45 | ...state, 46 | token: null, 47 | isAuthenticated: false, 48 | loading: false 49 | }; 50 | default: 51 | return state; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.19.0", 10 | "bootstrap": "^4.4.1", 11 | "moment": "^2.24.0", 12 | "react": "^16.12.0", 13 | "react-bootstrap": "^1.0.0-beta.16", 14 | "react-dom": "^16.12.0", 15 | "react-moment": "^0.9.7", 16 | "react-redux": "^7.1.3", 17 | "react-router-dom": "^5.1.2", 18 | "react-scripts": "3.3.0", 19 | "reactstrap": "^8.2.0", 20 | "redux": "^4.0.4", 21 | "redux-devtools-extension": "^2.13.8", 22 | "redux-thunk": "^2.3.0", 23 | "uuid": "^3.3.3" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "proxy": "http://localhost:5000" 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN stack boilerplate 2 | 3 | A boilerplate for building web app with the MERN stack, inspired by [this](https://github.com/bradtraversy/devconnector_2.0) project by Brad Traversy. 4 | 5 | ## Installation 6 | 7 | Use the npm package manager to install node modules. 8 | 9 | ```bash 10 | npm install # installs backend node modules 11 | cd client # switch to frontend client directory 12 | npm install # installs frontend node modules 13 | cd .. # return to project root directory 14 | ``` 15 | 16 | ## Add configurations 17 | 18 | Add two `.json` configuration files in the `config` directory, and save them as `default.json` and `production.json` 19 | 20 | The configuration files should contain the following: 21 | 22 | ```bash 23 | { 24 | "mongoURI": #Your MongoDB connection URI wrapped in double-quotes 25 | "jwtSecret": #Any string as your JSON web token secret 26 | } 27 | ``` 28 | 29 | ## Usage 30 | 31 | In the project root directory, run this command: 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | ## Heroku Deployment 38 | 39 | In the project root directory, run this command: 40 | 41 | ```bash 42 | heroku create #Create your app on Heroku 43 | git push heroku master #Deploy your app to Heroku 44 | 45 | ``` 46 | 47 | ## Contributing 48 | 49 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 50 | 51 | Please make sure to update tests as appropriate. 52 | 53 | ## License 54 | 55 | [MIT](https://choosealicense.com/licenses/mit/) 56 | -------------------------------------------------------------------------------- /client/src/components/layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { logout } from "../../actions/auth"; 5 | import { Navbar, Nav } from "react-bootstrap"; 6 | 7 | const AppNavbar = ({ auth: { isAuthenticated, loading }, logout }) => { 8 | const authLinks = ( 9 | 19 | ); 20 | 21 | const guestLinks = ( 22 | 26 | ); 27 | 28 | return ( 29 | 30 | 31 | MERN Stack Boilerplate 32 | 33 | 34 | 35 | {!loading && ( 36 | {isAuthenticated ? authLinks : guestLinks} 37 | )} 38 | 39 | 40 | ); 41 | }; 42 | 43 | AppNavbar.propTypes = { 44 | logout: PropTypes.func.isRequired, 45 | auth: PropTypes.object.isRequired 46 | }; 47 | 48 | const mapStateToProps = state => ({ 49 | auth: state.auth 50 | }); 51 | 52 | export default connect(mapStateToProps, { logout })(AppNavbar); 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | -------------------------------------------------------------------------------- /routes/api/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const gravatar = require("gravatar"); 4 | const bcrypt = require("bcryptjs"); 5 | const jwt = require("jsonwebtoken"); 6 | const config = require("config"); 7 | const { check, validationResult } = require("express-validator"); 8 | 9 | const User = require("../../models/User"); 10 | 11 | // @route POST api/users 12 | // @desc Register user 13 | // @access Public 14 | router.post( 15 | "/", 16 | [ 17 | check("name", "Name is required") 18 | .not() 19 | .isEmpty(), 20 | check("email", "Please include a valid email").isEmail(), 21 | check( 22 | "password", 23 | "Please enter a password with 6 or more characters" 24 | ).isLength({ min: 6 }) 25 | ], 26 | async (req, res) => { 27 | const errors = validationResult(req); 28 | if (!errors.isEmpty()) { 29 | return res.status(400).json({ errors: errors.array() }); 30 | } 31 | 32 | const { name, email, password } = req.body; 33 | 34 | try { 35 | let user = await User.findOne({ email }); 36 | 37 | if (user) { 38 | return res 39 | .status(400) 40 | .json({ errors: [{ msg: "User already exists" }] }); 41 | } 42 | 43 | const avatar = gravatar.url(email, { 44 | s: "200", 45 | r: "pg", 46 | d: "mm" 47 | }); 48 | 49 | user = new User({ 50 | name, 51 | email, 52 | avatar, 53 | password 54 | }); 55 | 56 | const salt = await bcrypt.genSalt(10); 57 | 58 | user.password = await bcrypt.hash(password, salt); 59 | 60 | await user.save(); 61 | 62 | const payload = { 63 | user: { 64 | id: user.id 65 | } 66 | }; 67 | 68 | jwt.sign( 69 | payload, 70 | config.get("jwtSecret"), 71 | { expiresIn: 360000 }, 72 | (err, token) => { 73 | if (err) throw err; 74 | res.json({ token }); 75 | } 76 | ); 77 | } catch (err) { 78 | console.error(err.message); 79 | res.status(500).send("Server error"); 80 | } 81 | } 82 | ); 83 | 84 | module.exports = router; 85 | -------------------------------------------------------------------------------- /routes/api/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const bcrypt = require("bcryptjs"); 4 | const auth = require("../../middleware/auth"); 5 | const jwt = require("jsonwebtoken"); 6 | const config = require("config"); 7 | const { check, validationResult } = require("express-validator"); 8 | 9 | const User = require("../../models/User"); 10 | 11 | // @route GET api/auth 12 | // @desc Test route 13 | // @access Public 14 | router.get("/", auth, async (req, res) => { 15 | try { 16 | const user = await User.findById(req.user.id).select("-password"); 17 | res.json(user); 18 | } catch (err) { 19 | console.error(err.message); 20 | res.status(500).send("Server Error"); 21 | } 22 | }); 23 | 24 | // @route POST api/auth 25 | // @desc Authenticate user & get token 26 | // @access Public 27 | router.post( 28 | "/", 29 | [ 30 | check("email", "Please include a valid email").isEmail(), 31 | check("password", "Password is required").exists() 32 | ], 33 | async (req, res) => { 34 | const errors = validationResult(req); 35 | if (!errors.isEmpty()) { 36 | return res.status(400).json({ errors: errors.array() }); 37 | } 38 | 39 | const { email, password } = req.body; 40 | 41 | try { 42 | let user = await User.findOne({ email }); 43 | 44 | if (!user) { 45 | return res 46 | .status(400) 47 | .json({ errors: [{ msg: "Invalid Credentials" }] }); 48 | } 49 | 50 | const isMatch = await bcrypt.compare(password, user.password); 51 | 52 | if (!isMatch) { 53 | return res 54 | .status(400) 55 | .json({ errors: [{ msg: "Invalid Credentials" }] }); 56 | } 57 | 58 | const payload = { 59 | user: { 60 | id: user.id 61 | } 62 | }; 63 | 64 | jwt.sign( 65 | payload, 66 | config.get("jwtSecret"), 67 | { expiresIn: 360000 }, 68 | (err, token) => { 69 | if (err) throw err; 70 | res.json({ token }); 71 | } 72 | ); 73 | } catch (err) { 74 | console.error(err.message); 75 | res.status(500).send("Server error"); 76 | } 77 | } 78 | ); 79 | 80 | module.exports = router; 81 | -------------------------------------------------------------------------------- /client/src/components/auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import { Link, Redirect } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | import { login } from "../../actions/auth"; 6 | import { Form, Button } from "react-bootstrap"; 7 | 8 | const Login = ({ login, isAuthenticated }) => { 9 | const [formData, setFormData] = useState({ 10 | email: "", 11 | password: "" 12 | }); 13 | 14 | const { email, password } = formData; 15 | 16 | const onChange = e => 17 | setFormData({ ...formData, [e.target.name]: e.target.value }); 18 | 19 | const onSubmit = async e => { 20 | e.preventDefault(); 21 | login(email, password); 22 | }; 23 | 24 | if (isAuthenticated) { 25 | return ; 26 | } 27 | 28 | return ( 29 | 30 |

Sign In

31 |

32 | Sign Into Your Account 33 |

34 |
onSubmit(e)}> 35 | 36 | Email address 37 | onChange(e)} 43 | required 44 | /> 45 | 46 | 47 | Password 48 | onChange(e)} 54 | minLength="6" 55 | /> 56 | 57 | 60 |
61 |

62 | Don't have an account? Register 63 |

64 |
65 | ); 66 | }; 67 | 68 | Login.propTypes = { 69 | login: PropTypes.func.isRequired, 70 | isAuthenticated: PropTypes.bool 71 | }; 72 | 73 | const mapStateToProps = state => ({ 74 | isAuthenticated: state.auth.isAuthenticated 75 | }); 76 | 77 | export default connect(mapStateToProps, { login })(Login); 78 | -------------------------------------------------------------------------------- /client/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { setAlert } from "./alert"; 3 | import { 4 | REGISTER_SUCCESS, 5 | REGISTER_FAIL, 6 | USER_LOADED, 7 | AUTH_ERROR, 8 | LOGIN_SUCCESS, 9 | LOGIN_FAIL, 10 | LOGOUT 11 | } from "./types"; 12 | import setAuthToken from "../utils/setAuthToken"; 13 | 14 | // Load User 15 | export const loadUser = () => async dispatch => { 16 | if (localStorage.token) { 17 | setAuthToken(localStorage.token); 18 | } 19 | 20 | try { 21 | const res = await axios.get("/api/auth"); 22 | 23 | dispatch({ 24 | type: USER_LOADED, 25 | payload: res.data 26 | }); 27 | } catch (err) { 28 | dispatch({ 29 | type: AUTH_ERROR 30 | }); 31 | } 32 | }; 33 | 34 | // Register User 35 | export const register = ({ name, email, password }) => async dispatch => { 36 | const config = { 37 | headers: { 38 | "Content-Type": "application/json" 39 | } 40 | }; 41 | 42 | const body = JSON.stringify({ name, email, password }); 43 | 44 | try { 45 | const res = await axios.post("/api/users", body, config); 46 | 47 | dispatch({ 48 | type: REGISTER_SUCCESS, 49 | payload: res.data 50 | }); 51 | 52 | dispatch(loadUser()); 53 | } catch (err) { 54 | const errors = err.response.data.errors; 55 | 56 | if (errors) { 57 | errors.forEach(error => dispatch(setAlert(error.msg, "danger"))); 58 | } 59 | 60 | dispatch({ 61 | type: REGISTER_FAIL 62 | }); 63 | } 64 | }; 65 | 66 | // Login User 67 | export const login = (email, password) => async dispatch => { 68 | const config = { 69 | headers: { 70 | "Content-Type": "application/json" 71 | } 72 | }; 73 | 74 | const body = JSON.stringify({ email, password }); 75 | 76 | try { 77 | const res = await axios.post("/api/auth", body, config); 78 | 79 | dispatch({ 80 | type: LOGIN_SUCCESS, 81 | payload: res.data 82 | }); 83 | 84 | dispatch(loadUser()); 85 | } catch (err) { 86 | const errors = err.response.data.errors; 87 | 88 | if (errors) { 89 | errors.forEach(error => dispatch(setAlert(error.msg, "danger"))); 90 | } 91 | 92 | dispatch({ 93 | type: LOGIN_FAIL 94 | }); 95 | } 96 | }; 97 | 98 | // Logout / Clear Profile 99 | export const logout = () => dispatch => { 100 | dispatch({ type: LOGOUT }); 101 | }; 102 | -------------------------------------------------------------------------------- /client/src/components/auth/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | import { setAlert } from "../../actions/alert"; 5 | import { register } from "../../actions/auth"; 6 | import PropTypes from "prop-types"; 7 | import { Form, Button, Row, Col } from "react-bootstrap"; 8 | 9 | const Register = ({ setAlert, register, isAuthenticated }) => { 10 | const [formData, setFormData] = useState({ 11 | name: "", 12 | email: "", 13 | password: "", 14 | password2: "" 15 | }); 16 | 17 | const { name, email, password, password2 } = formData; 18 | 19 | const onChange = e => 20 | setFormData({ ...formData, [e.target.name]: e.target.value }); 21 | 22 | const onSubmit = async e => { 23 | e.preventDefault(); 24 | if (password !== password2) { 25 | setAlert("Passwords do not match", "danger"); 26 | } else { 27 | register({ name, email, password }); 28 | } 29 | }; 30 | 31 | if (isAuthenticated) { 32 | return ; 33 | } 34 | 35 | return ( 36 | 37 |

Register

38 |

39 | Create Your Account 40 |

41 | 42 |
onSubmit(e)}> 43 | 44 | 45 | Name 46 | 47 | 48 | onChange(e)} 54 | /> 55 | 56 | 57 | 58 | 59 | Email 60 | 61 | 62 | onChange(e)} 68 | /> 69 | 70 | 71 | 72 | 73 | Password 74 | 75 | 76 | onChange(e)} 82 | /> 83 | 84 | 85 | 86 | 87 | Confirm Password 88 | 89 | 90 | onChange(e)} 96 | /> 97 | 98 | 99 | 102 |
103 |

104 | Already have an account? Sign In 105 |

106 |
107 | ); 108 | }; 109 | 110 | Register.propTypes = { 111 | setAlert: PropTypes.func.isRequired, 112 | register: PropTypes.func.isRequired, 113 | isAuthenticated: PropTypes.bool 114 | }; 115 | 116 | const mapStateToProps = state => ({ 117 | isAuthenticated: state.auth.isAuthenticated 118 | }); 119 | 120 | export default connect(mapStateToProps, { setAlert, register })(Register); 121 | --------------------------------------------------------------------------------