├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------