├── .gitignore
├── .prettierrc
├── README.md
├── app.js
├── client
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── components
│ │ ├── AppNavbar.tsx
│ │ ├── ItemModal.tsx
│ │ ├── ShoppingList.tsx
│ │ └── auth
│ │ │ ├── LoginModal.tsx
│ │ │ ├── Logout.tsx
│ │ │ └── RegisterModal.tsx
│ ├── flux
│ │ ├── actions
│ │ │ ├── authActions.ts
│ │ │ ├── errorActions.ts
│ │ │ ├── itemActions.ts
│ │ │ └── types.ts
│ │ ├── reducers
│ │ │ ├── authReducer.ts
│ │ │ ├── errorReducer.ts
│ │ │ ├── index.ts
│ │ │ └── itemReducer.ts
│ │ └── store.ts
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ ├── setupTests.ts
│ └── types
│ │ ├── enum.ts
│ │ └── interfaces.ts
├── tsconfig.json
└── yarn.lock
├── config
├── default.json
└── index.js
├── middleware
└── auth.js
├── models
├── Item.js
└── User.js
├── package.json
├── routes
└── api
│ ├── auth.js
│ ├── items.js
│ └── users.js
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | .env
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MERN Shopping List
2 |
3 | > Shopping list app built with the MERN stack along with Redux for state management, Reactstrap and react-transition-group.
4 |
5 | ## Quick Start
6 |
7 | Add your MONGO_URI to the default.json file. Make sure you set an env var for that and the jwtSecret on deployment
8 |
9 | ```bash
10 | # Install dependencies for server
11 | npm install
12 |
13 | # Install dependencies for client
14 | npm run client-install
15 |
16 | # Run the client & server with concurrently
17 | npm run dev
18 |
19 | # Run the Express server only
20 | npm run server
21 |
22 | # Run the React client only
23 | npm run client
24 |
25 | # Server runs on http://localhost:5000 and client on http://localhost:3000
26 | ```
27 |
28 | ## Deployment
29 |
30 | There is a Heroku post build script so that you do not have to compile your React frontend manually, it is done on the server. Simply push to Heroku and it will build and load the client index.html page
31 |
32 | ## App Info
33 |
34 | ### Author
35 |
36 | Brad Traversy
37 | [Traversy Media](http://www.traversymedia.com)
38 |
39 | ### Version
40 |
41 | 1.0.0
42 |
43 | ### License
44 |
45 | This project is licensed under the MIT License
46 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import mongoose from 'mongoose';
3 | import path from 'path';
4 | import cors from 'cors';
5 | import bodyParser from 'body-parser';
6 | import morgan from 'morgan';
7 | import config from './config';
8 |
9 | // routes
10 | import authRoutes from './routes/api/auth';
11 | import itemRoutes from './routes/api/items';
12 | import userRoutes from './routes/api/users';
13 |
14 | const { MONGO_URI, MONGO_DB_NAME } = config;
15 |
16 | const app = express();
17 |
18 | // CORS Middleware
19 | app.use(cors());
20 | // Logger Middleware
21 | app.use(morgan('dev'));
22 | // Bodyparser Middleware
23 | app.use(bodyParser.json());
24 |
25 | // DB Config
26 | const db = `${MONGO_URI}/${MONGO_DB_NAME}`;
27 |
28 | // Connect to Mongo
29 | mongoose
30 | .connect(db, {
31 | useNewUrlParser: true,
32 | useCreateIndex: true,
33 | useUnifiedTopology: true
34 | }) // Adding new mongo url parser
35 | .then(() => console.log('MongoDB Connected...'))
36 | .catch(err => console.log(err));
37 |
38 | // Use Routes
39 | app.use('/api/items', itemRoutes);
40 | app.use('/api/users', userRoutes);
41 | app.use('/api/auth', authRoutes);
42 |
43 | // Serve static assets if in production
44 | if (process.env.NODE_ENV === 'production') {
45 | // Set static folder
46 | app.use(express.static('client/build'));
47 |
48 | app.get('*', (req, res) => {
49 | res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
50 | });
51 | }
52 |
53 | export default app;
54 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.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 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | 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.
35 |
36 | 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.
37 |
38 | 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.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client-ts",
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 | "@types/axios": "^0.14.0",
10 | "@types/bootstrap": "^4.3.1",
11 | "@types/jest": "^24.0.0",
12 | "@types/node": "^12.0.0",
13 | "@types/react-redux": "^7.1.7",
14 | "@types/react-transition-group": "^4.2.3",
15 | "@types/reactstrap": "^8.4.1",
16 | "@types/redux": "^3.6.0",
17 | "@types/redux-saga": "^0.10.5",
18 | "@types/redux-thunk": "^2.1.0",
19 | "@types/uuid": "^3.4.7",
20 | "axios": "^0.19.2",
21 | "bootstrap": "^4.4.1",
22 | "react": "^16.12.0",
23 | "react-dom": "^16.12.0",
24 | "react-redux": "^7.2.0",
25 | "react-scripts": "3.4.0",
26 | "react-transition-group": "^4.3.0",
27 | "reactstrap": "^8.4.1",
28 | "redux": "^4.0.5",
29 | "redux-saga": "^1.1.3",
30 | "redux-thunk": "^2.3.0",
31 | "typescript": "~3.7.2",
32 | "uuid": "^3.4.0"
33 | },
34 | "scripts": {
35 | "start": "react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject"
39 | },
40 | "eslintConfig": {
41 | "extends": "react-app"
42 | },
43 | "browserslist": {
44 | "production": [
45 | ">0.2%",
46 | "not dead",
47 | "not op_mini all"
48 | ],
49 | "development": [
50 | "last 1 chrome version",
51 | "last 1 firefox version",
52 | "last 1 safari version"
53 | ]
54 | },
55 | "devDependencies": {
56 | "@types/react": "^16.9.20",
57 | "@types/react-dom": "^16.9.5"
58 | },
59 | "proxy": "http://localhost:5000/"
60 | }
61 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/mern_shopping_list/1a47f490b5d0003e136acf059c9d6be88f9a6e2f/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/mern_shopping_list/1a47f490b5d0003e136acf059c9d6be88f9a6e2f/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/mern_shopping_list/1a47f490b5d0003e136acf059c9d6be88f9a6e2f/client/public/logo512.png
--------------------------------------------------------------------------------
/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/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .remove-btn {
2 | margin-right: 0.5rem;
3 | }
4 |
5 | .fade-enter {
6 | opacity: 0.01;
7 | }
8 |
9 | .fade-enter-active {
10 | opacity: 1;
11 | transition: opacity 1000ms ease-in;
12 | }
13 |
14 | .fade-exit {
15 | opacity: 1;
16 | }
17 |
18 | .fade-exit-active {
19 | opacity: 0.01;
20 | transition: opacity 1000ms ease-in;
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import AppNavbar from './components/AppNavbar';
3 | import ShoppingList from './components/ShoppingList';
4 | import ItemModal from './components/ItemModal';
5 | import { Container } from 'reactstrap';
6 |
7 | import { Provider } from 'react-redux';
8 | import store from './flux/store';
9 | import { loadUser } from './flux/actions/authActions';
10 |
11 | import 'bootstrap/dist/css/bootstrap.min.css';
12 | import './App.css';
13 |
14 | const App = () => {
15 | useEffect(() => {
16 | store.dispatch(loadUser());
17 | }, []);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default App;
33 |
--------------------------------------------------------------------------------
/client/src/components/AppNavbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from 'react';
2 | import {
3 | Collapse,
4 | Navbar,
5 | NavbarToggler,
6 | NavbarBrand,
7 | Nav,
8 | NavItem,
9 | Container
10 | } from 'reactstrap';
11 | import { connect } from 'react-redux';
12 | import RegisterModal from './auth/RegisterModal';
13 | import LoginModal from './auth/LoginModal';
14 | import Logout from './auth/Logout';
15 | import { IAppNavbar, IAuthReduxProps } from '../types/interfaces';
16 |
17 | const AppNavbar = ({ auth }: IAppNavbar) => {
18 | const [isOpen, setIsOpen] = useState(false);
19 |
20 | const handleToggle = () => setIsOpen(!isOpen);
21 |
22 | const authLinks = (
23 |
24 |
25 |
26 |
27 | {auth && auth.user ? `Welcome ${auth.user.name}` : ''}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 |
37 | const guestLinks = (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 |
48 | return (
49 |
50 |
51 |
52 | ShoppingList
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | const mapStateToProps = (state: IAuthReduxProps) => ({
66 | auth: state.auth
67 | });
68 |
69 | export default connect(mapStateToProps, null)(AppNavbar);
70 |
--------------------------------------------------------------------------------
/client/src/components/ItemModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Button,
4 | Modal,
5 | ModalHeader,
6 | ModalBody,
7 | Form,
8 | FormGroup,
9 | Label,
10 | Input
11 | } from 'reactstrap';
12 | import { connect } from 'react-redux';
13 | import { addItem } from '../flux/actions/itemActions';
14 | import { IItemReduxProps, IItemModal, ITarget } from '../types/interfaces';
15 |
16 | const ItemModal = ({ isAuthenticated, addItem }: IItemModal) => {
17 | const [modal, setModal] = useState(false);
18 | const [name, setName] = useState('');
19 |
20 | const handleToggle = () => setModal(!modal);
21 |
22 | const handleChangeName = (e: ITarget) => setName(e.target.value);
23 |
24 | const handleOnSubmit = (e: any) => {
25 | e.preventDefault();
26 |
27 | const newItem = {
28 | name
29 | };
30 |
31 | // Add item via addItem action
32 | addItem(newItem);
33 | // Close modal
34 | handleToggle();
35 | };
36 |
37 | return (
38 |
39 | {isAuthenticated ? (
40 |
47 | ) : (
48 |
Please log in to manage items
49 | )}
50 |
51 |
52 | Add To Shopping List
53 |
54 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | const mapStateToProps = (state: IItemReduxProps) => ({
76 | item: state.item,
77 | isAuthenticated: state.auth.isAuthenticated
78 | });
79 |
80 | export default connect(mapStateToProps, { addItem })(ItemModal);
81 |
--------------------------------------------------------------------------------
/client/src/components/ShoppingList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Container, ListGroup, ListGroupItem, Button } from 'reactstrap';
3 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
4 | import { connect } from 'react-redux';
5 | import { getItems, deleteItem } from '../flux/actions/itemActions';
6 | import { IItemReduxProps, IShoppingList } from '../types/interfaces';
7 |
8 | const ShoppingList = ({
9 | getItems,
10 | item,
11 | isAuthenticated,
12 | deleteItem
13 | }: IShoppingList) => {
14 | useEffect(() => {
15 | getItems();
16 | }, [getItems]);
17 |
18 | const handleDelete = (id: string) => {
19 | deleteItem(id);
20 | };
21 |
22 | const { items } = item;
23 | return (
24 |
25 |
26 |
27 | {items.map(({ _id, name }) => (
28 |
29 |
30 | {isAuthenticated ? (
31 |
39 | ) : null}
40 | {name}
41 |
42 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | const mapStateToProps = (state: IItemReduxProps) => ({
51 | item: state.item,
52 | isAuthenticated: state.auth.isAuthenticated
53 | });
54 |
55 | export default connect(mapStateToProps, { getItems, deleteItem })(ShoppingList);
56 |
--------------------------------------------------------------------------------
/client/src/components/auth/LoginModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import {
3 | Button,
4 | Modal,
5 | ModalHeader,
6 | ModalBody,
7 | Form,
8 | FormGroup,
9 | Label,
10 | Input,
11 | NavLink,
12 | Alert
13 | } from 'reactstrap';
14 | import { connect } from 'react-redux';
15 | import { login } from '../../flux/actions/authActions';
16 | import { clearErrors } from '../../flux/actions/errorActions';
17 | import { ILoginModal, ITarget, IAuthReduxProps } from '../../types/interfaces';
18 |
19 | const LoginModal = ({
20 | isAuthenticated,
21 | error,
22 | login,
23 | clearErrors
24 | }: ILoginModal) => {
25 | const [modal, setModal] = useState(false);
26 | const [email, setEmail] = useState('');
27 | const [password, setPassword] = useState('');
28 | const [msg, setMsg] = useState(null);
29 |
30 | const handleToggle = useCallback(() => {
31 | // Clear errors
32 | clearErrors();
33 | setModal(!modal);
34 | }, [clearErrors, modal]);
35 |
36 | const handleChangeEmail = (e: ITarget) => setEmail(e.target.value);
37 | const handleChangePassword = (e: ITarget) => setPassword(e.target.value);
38 |
39 | const handleOnSubmit = (e: any) => {
40 | e.preventDefault();
41 |
42 | const user = { email, password };
43 |
44 | // Attempt to login
45 | login(user);
46 | };
47 |
48 | useEffect(() => {
49 | // Check for register error
50 | if (error.id === 'LOGIN_FAIL') {
51 | setMsg(error.msg.msg);
52 | } else {
53 | setMsg(null);
54 | }
55 |
56 | // If authenticated, close modal
57 | if (modal) {
58 | if (isAuthenticated) {
59 | handleToggle();
60 | }
61 | }
62 | }, [error, handleToggle, isAuthenticated, modal]);
63 |
64 | return (
65 |
66 |
67 | Login
68 |
69 |
70 |
71 | Login
72 |
73 | {msg ? {msg} : null}
74 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | const mapStateToProps = (state: IAuthReduxProps) => ({
112 | isAuthenticated: state.auth.isAuthenticated,
113 | error: state.error
114 | });
115 |
116 | export default connect(mapStateToProps, { login, clearErrors })(LoginModal);
117 |
--------------------------------------------------------------------------------
/client/src/components/auth/Logout.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { NavLink } from 'reactstrap';
3 | import { connect } from 'react-redux';
4 | import { logout } from '../../flux/actions/authActions';
5 | import { ILogoutProps } from '../../types/interfaces';
6 |
7 | export const Logout = ({ logout }: ILogoutProps) => {
8 | return (
9 |
10 |
11 | Logout
12 |
13 |
14 | );
15 | };
16 |
17 | export default connect(null, { logout })(Logout);
18 |
--------------------------------------------------------------------------------
/client/src/components/auth/RegisterModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect } from 'react';
2 | import {
3 | Button,
4 | Modal,
5 | ModalHeader,
6 | ModalBody,
7 | Form,
8 | FormGroup,
9 | Label,
10 | Input,
11 | NavLink,
12 | Alert
13 | } from 'reactstrap';
14 | import { connect } from 'react-redux';
15 | import { register } from '../../flux/actions/authActions';
16 | import { clearErrors } from '../../flux/actions/errorActions';
17 | import {
18 | IRegisterModal,
19 | ITarget,
20 | IAuthReduxProps
21 | } from '../../types/interfaces';
22 |
23 | const RegisterModal = ({
24 | isAuthenticated,
25 | error,
26 | register,
27 | clearErrors
28 | }: IRegisterModal) => {
29 | const [modal, setModal] = useState(false);
30 | const [name, setName] = useState('');
31 | const [email, setEmail] = useState('');
32 | const [password, setPassword] = useState('');
33 | const [msg, setMsg] = useState(null);
34 |
35 | const handleToggle = useCallback(() => {
36 | // Clear errors
37 | clearErrors();
38 | setModal(!modal);
39 | }, [clearErrors, modal]);
40 |
41 | const handleChangeName = (e: ITarget) => setName(e.target.value);
42 | const handleChangeEmail = (e: ITarget) => setEmail(e.target.value);
43 | const handleChangePassword = (e: ITarget) => setPassword(e.target.value);
44 |
45 | const handleOnSubmit = (e: any) => {
46 | e.preventDefault();
47 |
48 | // Create user object
49 | const user = {
50 | name,
51 | email,
52 | password
53 | };
54 |
55 | // Attempt to login
56 | register(user);
57 | };
58 |
59 | useEffect(() => {
60 | // Check for register error
61 | if (error.id === 'REGISTER_FAIL') {
62 | setMsg(error.msg.msg);
63 | } else {
64 | setMsg(null);
65 | }
66 |
67 | // If authenticated, close modal
68 | if (modal) {
69 | if (isAuthenticated) {
70 | handleToggle();
71 | }
72 | }
73 | }, [error, handleToggle, isAuthenticated, modal]);
74 |
75 | return (
76 |
77 |
78 | Register
79 |
80 |
81 |
82 | Register
83 |
84 | {msg ? {msg} : null}
85 |
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | const mapStateToProps = (state: IAuthReduxProps) => ({
128 | isAuthenticated: state.auth.isAuthenticated,
129 | error: state.error
130 | });
131 |
132 | export default connect(mapStateToProps, { register, clearErrors })(
133 | RegisterModal
134 | );
135 |
--------------------------------------------------------------------------------
/client/src/flux/actions/authActions.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { returnErrors } from './errorActions';
3 | import {
4 | USER_LOADED,
5 | USER_LOADING,
6 | AUTH_ERROR,
7 | LOGIN_SUCCESS,
8 | LOGIN_FAIL,
9 | LOGOUT_SUCCESS,
10 | REGISTER_SUCCESS,
11 | REGISTER_FAIL
12 | } from './types';
13 | import { IAuthFunction, IConfigHeaders } from '../../types/interfaces';
14 |
15 | // Check token & load user
16 | export const loadUser = () => (dispatch: Function, getState: Function) => {
17 | // User loading
18 | dispatch({ type: USER_LOADING });
19 |
20 | axios
21 | .get('/api/auth/user', tokenConfig(getState))
22 | .then(res =>
23 | dispatch({
24 | type: USER_LOADED,
25 | payload: res.data
26 | })
27 | )
28 | .catch(err => {
29 | dispatch(returnErrors(err.response.data, err.response.status));
30 | dispatch({
31 | type: AUTH_ERROR
32 | });
33 | });
34 | };
35 |
36 | // Register User
37 | export const register = ({ name, email, password }: IAuthFunction) => (
38 | dispatch: Function
39 | ) => {
40 | // Headers
41 | const config = {
42 | headers: {
43 | 'Content-Type': 'application/json'
44 | }
45 | };
46 |
47 | // Request body
48 | const body = JSON.stringify({ name, email, password });
49 |
50 | axios
51 | .post('/api/auth/register', body, config)
52 | .then(res =>
53 | dispatch({
54 | type: REGISTER_SUCCESS,
55 | payload: res.data
56 | })
57 | )
58 | .catch(err => {
59 | dispatch(
60 | returnErrors(err.response.data, err.response.status, 'REGISTER_FAIL')
61 | );
62 | dispatch({
63 | type: REGISTER_FAIL
64 | });
65 | });
66 | };
67 |
68 | // Login User
69 | export const login = ({ email, password }: IAuthFunction) => (
70 | dispatch: Function
71 | ) => {
72 | // Headers
73 | const config = {
74 | headers: {
75 | 'Content-Type': 'application/json'
76 | }
77 | };
78 |
79 | // Request body
80 | const body = JSON.stringify({ email, password });
81 |
82 | axios
83 | .post('/api/auth/login', body, config)
84 | .then(res =>
85 | dispatch({
86 | type: LOGIN_SUCCESS,
87 | payload: res.data
88 | })
89 | )
90 | .catch(err => {
91 | dispatch(
92 | returnErrors(err.response.data, err.response.status, 'LOGIN_FAIL')
93 | );
94 | dispatch({
95 | type: LOGIN_FAIL
96 | });
97 | });
98 | };
99 |
100 | // Logout User
101 | export const logout = () => {
102 | return {
103 | type: LOGOUT_SUCCESS
104 | };
105 | };
106 |
107 | // Setup config/headers and token
108 | export const tokenConfig = (getState: Function) => {
109 | // Get token from localstorage
110 | const token = getState().auth.token;
111 |
112 | // Headers
113 | const config: IConfigHeaders = {
114 | headers: {
115 | 'Content-type': 'application/json'
116 | }
117 | };
118 |
119 | // If token, add to headers
120 | if (token) {
121 | config.headers['x-auth-token'] = token;
122 | }
123 |
124 | return config;
125 | };
126 |
--------------------------------------------------------------------------------
/client/src/flux/actions/errorActions.ts:
--------------------------------------------------------------------------------
1 | import { GET_ERRORS, CLEAR_ERRORS } from './types';
2 | import { IMsg } from '../../types/interfaces';
3 |
4 | // RETURN ERRORS
5 | export const returnErrors = (msg: IMsg, status: number, id: any = null) => {
6 | return {
7 | type: GET_ERRORS,
8 | payload: { msg, status, id }
9 | };
10 | };
11 |
12 | // CLEAR ERRORS
13 | export const clearErrors = () => {
14 | return {
15 | type: CLEAR_ERRORS
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/flux/actions/itemActions.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { GET_ITEMS, ADD_ITEM, DELETE_ITEM, ITEMS_LOADING } from './types';
3 | import { tokenConfig } from './authActions';
4 | import { returnErrors } from './errorActions';
5 | import { IItem } from '../../types/interfaces';
6 |
7 | export const getItems = () => (dispatch: Function) => {
8 | dispatch(setItemsLoading());
9 | axios
10 | .get('/api/items')
11 | .then(res =>
12 | dispatch({
13 | type: GET_ITEMS,
14 | payload: res.data
15 | })
16 | )
17 | .catch(err =>
18 | dispatch(returnErrors(err.response.data, err.response.status))
19 | );
20 | };
21 |
22 | export const addItem = (item: IItem) => (
23 | dispatch: Function,
24 | getState: Function
25 | ) => {
26 | axios
27 | .post('/api/items', item, tokenConfig(getState))
28 | .then(res =>
29 | dispatch({
30 | type: ADD_ITEM,
31 | payload: res.data
32 | })
33 | )
34 | .catch(err =>
35 | dispatch(returnErrors(err.response.data, err.response.status))
36 | );
37 | };
38 |
39 | export const deleteItem = (id: string) => (
40 | dispatch: Function,
41 | getState: Function
42 | ) => {
43 | axios
44 | .delete(`/api/items/${id}`, tokenConfig(getState))
45 | .then(res =>
46 | dispatch({
47 | type: DELETE_ITEM,
48 | payload: id
49 | })
50 | )
51 | .catch(err =>
52 | dispatch(returnErrors(err.response.data, err.response.status))
53 | );
54 | };
55 |
56 | export const setItemsLoading = () => {
57 | return {
58 | type: ITEMS_LOADING
59 | };
60 | };
61 |
--------------------------------------------------------------------------------
/client/src/flux/actions/types.ts:
--------------------------------------------------------------------------------
1 | export const GET_ITEMS = 'GET_ITEMS';
2 | export const ADD_ITEM = 'ADD_ITEM';
3 | export const DELETE_ITEM = 'DELETE_ITEM';
4 | export const ITEMS_LOADING = 'ITEMS_LOADING';
5 | export const USER_LOADING = "USER_LOADING";
6 | export const USER_LOADED = "USER_LOADED";
7 | export const AUTH_ERROR = "AUTH_ERROR";
8 | export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
9 | export const LOGIN_FAIL = "LOGIN_FAIL";
10 | export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS";
11 | export const REGISTER_SUCCESS = "REGISTER_SUCCESS";
12 | export const REGISTER_FAIL = "REGISTER_FAIL";
13 | export const GET_ERRORS = 'GET_ERRORS';
14 | export const CLEAR_ERRORS = 'CLEAR_ERRORS';
15 |
--------------------------------------------------------------------------------
/client/src/flux/reducers/authReducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | USER_LOADED,
3 | USER_LOADING,
4 | AUTH_ERROR,
5 | LOGIN_SUCCESS,
6 | LOGIN_FAIL,
7 | LOGOUT_SUCCESS,
8 | REGISTER_SUCCESS,
9 | REGISTER_FAIL
10 | } from '../actions/types';
11 |
12 | const initialState = {
13 | token: localStorage.getItem('token'),
14 | isAuthenticated: false,
15 | isLoading: false,
16 | user: null
17 | };
18 |
19 | export default function(state = initialState, action: any) {
20 | switch (action.type) {
21 | case USER_LOADING:
22 | return {
23 | ...state,
24 | isLoading: true
25 | };
26 | case USER_LOADED:
27 | return {
28 | ...state,
29 | isAuthenticated: true,
30 | isLoading: false,
31 | user: action.payload
32 | };
33 | case LOGIN_SUCCESS:
34 | case REGISTER_SUCCESS:
35 | localStorage.setItem('token', action.payload.token);
36 | return {
37 | ...state,
38 | ...action.payload,
39 | isAuthenticated: true,
40 | isLoading: false
41 | };
42 | case AUTH_ERROR:
43 | case LOGIN_FAIL:
44 | case LOGOUT_SUCCESS:
45 | case REGISTER_FAIL:
46 | localStorage.removeItem('token');
47 | return {
48 | ...state,
49 | token: null,
50 | user: null,
51 | isAuthenticated: false,
52 | isLoading: false
53 | };
54 | default:
55 | return state;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/client/src/flux/reducers/errorReducer.ts:
--------------------------------------------------------------------------------
1 | import { GET_ERRORS, CLEAR_ERRORS } from '../actions/types';
2 | import { IAction } from '../../types/interfaces';
3 |
4 | const initialState = {
5 | msg: {},
6 | status: null,
7 | id: null
8 | };
9 |
10 | export default function(state = initialState, action: IAction) {
11 | switch (action.type) {
12 | case GET_ERRORS:
13 | return {
14 | msg: action.payload.msg,
15 | status: action.payload.status,
16 | id: action.payload.id
17 | };
18 | case CLEAR_ERRORS:
19 | return {
20 | msg: {},
21 | status: null,
22 | id: null
23 | };
24 | default:
25 | return state;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/flux/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import itemReducer from './itemReducer';
3 | import errorReducer from './errorReducer';
4 | import authReducer from './authReducer';
5 |
6 | export default combineReducers({
7 | item: itemReducer,
8 | error: errorReducer,
9 | auth: authReducer
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/flux/reducers/itemReducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GET_ITEMS,
3 | ADD_ITEM,
4 | DELETE_ITEM,
5 | ITEMS_LOADING
6 | } from '../actions/types';
7 | import { IAction, IItem } from '../../types/interfaces';
8 |
9 | const initialState = {
10 | items: [],
11 | loading: false
12 | };
13 |
14 | interface IState {
15 | items: IItem[];
16 | }
17 |
18 | export default function(state: IState = initialState, action: IAction) {
19 | switch (action.type) {
20 | case GET_ITEMS:
21 | return {
22 | ...state,
23 | items: action.payload,
24 | loading: false
25 | };
26 | case DELETE_ITEM:
27 | return {
28 | ...state,
29 | items: state.items.filter(item => item._id !== action.payload)
30 | };
31 | case ADD_ITEM:
32 | return {
33 | ...state,
34 | items: [action.payload, ...state.items]
35 | };
36 | case ITEMS_LOADING:
37 | return {
38 | ...state,
39 | loading: true
40 | };
41 | default:
42 | return state;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/flux/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from './reducers';
4 |
5 | const initialState = {};
6 |
7 | const middleWare = [thunk];
8 |
9 | declare global {
10 | interface Window {
11 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
12 | }
13 | }
14 |
15 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
16 | const store = createStore(
17 | rootReducer,
18 | initialState,
19 | composeEnhancers(applyMiddleware(...middleWare))
20 | );
21 |
22 | export default store;
23 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import * as serviceWorker from './serviceWorker';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
8 | // If you want your app to work offline and load faster, you can change
9 | // unregister() to register() below. Note this comes with some pitfalls.
10 | // Learn more about service workers: https://bit.ly/CRA-PWA
11 | serviceWorker.unregister();
12 |
--------------------------------------------------------------------------------
/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready
142 | .then(registration => {
143 | registration.unregister();
144 | })
145 | .catch(error => {
146 | console.error(error.message);
147 | });
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/client/src/setupTests.ts:
--------------------------------------------------------------------------------
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/types/enum.ts:
--------------------------------------------------------------------------------
1 | export enum E_ERROR {
2 | LOGIN_FAIL = 'LOGIN_FAIL',
3 | REGISTER_FAIL = 'REGISTER_FAIL'
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/types/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { E_ERROR } from './enum';
2 |
3 | // REACT
4 | export interface ITarget {
5 | target: {
6 | value: React.SetStateAction;
7 | };
8 | preventDefault(): void;
9 | }
10 |
11 | // ERRORS
12 | export interface IMsg {
13 | msg: string | any;
14 | }
15 |
16 | // AUTH
17 | export interface IUser {
18 | name?: string;
19 | email: string;
20 | password: string;
21 | }
22 |
23 | export interface IAuthForm {
24 | isAuthenticated?: boolean;
25 | error: IError;
26 | clearErrors(): void;
27 | }
28 |
29 | export interface ILoginModal extends IAuthForm {
30 | login(user: IUser): void;
31 | }
32 |
33 | export interface IRegisterModal extends IAuthForm {
34 | register(user: IUser): void;
35 | }
36 |
37 | export interface ILogoutProps {
38 | logout(): void;
39 | }
40 |
41 | export interface IError {
42 | id: E_ERROR;
43 | msg: IMsg;
44 | }
45 |
46 | export interface IAuthReduxProps {
47 | auth: { isAuthenticated: boolean };
48 | error: IError;
49 | }
50 |
51 | export interface IConfigHeaders {
52 | headers: {
53 | [index: string]: string;
54 | };
55 | }
56 |
57 | // NAVBAR
58 | export interface IAppNavbar {
59 | auth?: {
60 | isAuthenticated: boolean;
61 | user: IUser;
62 | };
63 | }
64 |
65 | // ITEMS
66 | export interface IExistingItem {
67 | _id: string;
68 | name: string;
69 | }
70 |
71 | export interface IItem {
72 | _id?: string;
73 | name: string;
74 | }
75 |
76 | export interface IItemModal {
77 | isAuthenticated: boolean;
78 | addItem(item: IItem): void;
79 | }
80 |
81 | export interface IItemReduxProps extends IAuthReduxProps {
82 | item: {
83 | items: IExistingItem[];
84 | };
85 | }
86 |
87 | export interface IShoppingList {
88 | item: {
89 | items: IExistingItem[];
90 | };
91 | getItems(): void;
92 | deleteItem(id: string): void;
93 | isAuthenticated: boolean;
94 | }
95 |
96 | // <<<<<<<<<<<>>>>>>>>>>>>
97 | // <<<<<<<< FLUX >>>>>>>>>
98 | // <<<<<<<<<<<>>>>>>>>>>>>
99 |
100 | export interface IAuthFunction {
101 | name?: string;
102 | email: string;
103 | password: string;
104 | }
105 |
106 | export interface IReturnErrors {
107 | msg: {
108 | msg: string | any;
109 | };
110 | status: string;
111 | id: any;
112 | }
113 |
114 | export interface IAction {
115 | type: string;
116 | payload?: any;
117 | }
118 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "mongoURI": "PUT_YOUR_MONGO_URI",
3 | "jwtSecret": "sl_myJwtSecret"
4 | }
5 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 |
3 | dotenv.config();
4 |
5 | export default {
6 | PORT: process.env.PORT,
7 | MONGO_URI: process.env.MONGO_URI,
8 | MONGO_DB_NAME: process.env.MONGO_DB_NAME,
9 | JWT_SECRET: process.env.JWT_SECRET
10 | };
11 |
--------------------------------------------------------------------------------
/middleware/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import config from '../config';
3 |
4 | const { JWT_SECRET } = config;
5 |
6 | export default (req, res, next) => {
7 | const token = req.header('x-auth-token');
8 |
9 | // Check for token
10 | if (!token)
11 | return res.status(401).json({ msg: 'No token, authorization denied' });
12 |
13 | try {
14 | // Verify token
15 | const decoded = jwt.verify(token, JWT_SECRET);
16 | // Add user from payload
17 | req.user = decoded;
18 | next();
19 | } catch (e) {
20 | res.status(400).json({ msg: 'Token is not valid' });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/models/Item.js:
--------------------------------------------------------------------------------
1 | import { Schema, model } from 'mongoose';
2 |
3 | // Create Schema
4 | const ItemSchema = new Schema({
5 | name: {
6 | type: String,
7 | required: true
8 | },
9 | date: {
10 | type: Date,
11 | default: Date.now
12 | }
13 | });
14 |
15 | const Item = model('item', ItemSchema);
16 |
17 | export default Item;
18 |
--------------------------------------------------------------------------------
/models/User.js:
--------------------------------------------------------------------------------
1 | import { Schema, model } from 'mongoose';
2 |
3 | // Create Schema
4 | const UserSchema = new Schema({
5 | name: {
6 | type: String,
7 | required: true
8 | },
9 | email: {
10 | type: String,
11 | required: true,
12 | unique: true
13 | },
14 | password: {
15 | type: String,
16 | required: true
17 | },
18 | register_date: {
19 | type: Date,
20 | default: Date.now
21 | }
22 | });
23 |
24 | const User = model('user', UserSchema);
25 |
26 | export default User;
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern_typescript",
3 | "version": "1.0.0",
4 | "description": "MERN Stack with ES6 and Typescript Boilerplate",
5 | "main": "server.js",
6 | "scripts": {
7 | "client-install": "npm install --prefix client",
8 | "start": "node server.js",
9 | "server": "nodemon server.js --exec babel-node --presets babel-preset-env",
10 | "client": "npm start --prefix client",
11 | "dev": "concurrently \"npm run server\" \"npm run client\"",
12 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
13 | },
14 | "author": "Brad Traversy",
15 | "license": "MIT",
16 | "dependencies": {
17 | "bcryptjs": "^2.4.3",
18 | "body-parser": "^1.19.0",
19 | "concurrently": "^3.6.0",
20 | "config": "^3.0.1",
21 | "cors": "^2.8.5",
22 | "dotenv": "^8.2.0",
23 | "express": "^4.16.3",
24 | "jsonwebtoken": "^8.5.0",
25 | "mongoose": "^5.1.6"
26 | },
27 | "devDependencies": {
28 | "babel-cli": "^6.26.0",
29 | "babel-core": "^6.26.3",
30 | "babel-loader": "^8.0.6",
31 | "babel-preset-env": "^1.7.0",
32 | "morgan": "^1.9.1",
33 | "nodemon": "^1.17.5"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/routes/api/auth.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import bcrypt from 'bcryptjs';
3 | import config from '../../config';
4 | import jwt from 'jsonwebtoken';
5 | import auth from '../../middleware/auth';
6 | // User Model
7 | import User from '../../models/User';
8 |
9 | const { JWT_SECRET } = config;
10 | const router = Router();
11 |
12 | /**
13 | * @route POST api/auth/login
14 | * @desc Login user
15 | * @access Public
16 | */
17 |
18 | router.post('/login', async (req, res) => {
19 | const { email, password } = req.body;
20 |
21 | // Simple validation
22 | if (!email || !password) {
23 | return res.status(400).json({ msg: 'Please enter all fields' });
24 | }
25 |
26 | try {
27 | // Check for existing user
28 | const user = await User.findOne({ email });
29 | if (!user) throw Error('User does not exist');
30 |
31 | const isMatch = await bcrypt.compare(password, user.password);
32 | if (!isMatch) throw Error('Invalid credentials');
33 |
34 | const token = jwt.sign({ id: user._id }, JWT_SECRET, { expiresIn: 3600 });
35 | if (!token) throw Error('Couldnt sign the token');
36 |
37 | res.status(200).json({
38 | token,
39 | user: {
40 | id: user._id,
41 | name: user.name,
42 | email: user.email
43 | }
44 | });
45 | } catch (e) {
46 | res.status(400).json({ msg: e.message });
47 | }
48 | });
49 |
50 | /**
51 | * @route POST api/users
52 | * @desc Register new user
53 | * @access Public
54 | */
55 |
56 | router.post('/register', async (req, res) => {
57 | const { name, email, password } = req.body;
58 |
59 | // Simple validation
60 | if (!name || !email || !password) {
61 | return res.status(400).json({ msg: 'Please enter all fields' });
62 | }
63 |
64 | try {
65 | const user = await User.findOne({ email });
66 | if (user) throw Error('User already exists');
67 |
68 | const salt = await bcrypt.genSalt(10);
69 | if (!salt) throw Error('Something went wrong with bcrypt');
70 |
71 | const hash = await bcrypt.hash(password, salt);
72 | if (!hash) throw Error('Something went wrong hashing the password');
73 |
74 | const newUser = new User({
75 | name,
76 | email,
77 | password: hash
78 | });
79 |
80 | const savedUser = await newUser.save();
81 | if (!savedUser) throw Error('Something went wrong saving the user');
82 |
83 | const token = jwt.sign({ id: savedUser._id }, JWT_SECRET, {
84 | expiresIn: 3600
85 | });
86 |
87 | res.status(200).json({
88 | token,
89 | user: {
90 | id: savedUser.id,
91 | name: savedUser.name,
92 | email: savedUser.email
93 | }
94 | });
95 | } catch (e) {
96 | res.status(400).json({ error: e.message });
97 | }
98 | });
99 |
100 | /**
101 | * @route GET api/auth/user
102 | * @desc Get user data
103 | * @access Private
104 | */
105 |
106 | router.get('/user', auth, async (req, res) => {
107 | try {
108 | const user = await User.findById(req.user.id).select('-password');
109 | if (!user) throw Error('User does not exist');
110 | res.json(user);
111 | } catch (e) {
112 | res.status(400).json({ msg: e.message });
113 | }
114 | });
115 |
116 | export default router;
117 |
--------------------------------------------------------------------------------
/routes/api/items.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import auth from '../../middleware/auth';
3 | // Item Model
4 | import Item from '../../models/Item';
5 |
6 | const router = Router();
7 |
8 | /**
9 | * @route GET api/items
10 | * @desc Get All Items
11 | * @access Public
12 | */
13 |
14 | router.get('/', async (req, res) => {
15 | try {
16 | const items = await Item.find();
17 | if (!items) throw Error('No items');
18 |
19 | res.status(200).json(items);
20 | } catch (e) {
21 | res.status(400).json({ msg: e.message });
22 | }
23 | });
24 |
25 | /**
26 | * @route POST api/items
27 | * @desc Create An Item
28 | * @access Private
29 | */
30 |
31 | router.post('/', auth, async (req, res) => {
32 | const newItem = new Item({
33 | name: req.body.name
34 | });
35 |
36 | try {
37 | const item = await newItem.save();
38 | if (!item) throw Error('Something went wrong saving the item');
39 |
40 | res.status(200).json(item);
41 | } catch (e) {
42 | res.status(400).json({ msg: e.message });
43 | }
44 | });
45 |
46 | /**
47 | * @route DELETE api/items/:id
48 | * @desc Delete A Item
49 | * @access Private
50 | */
51 |
52 | router.delete('/:id', auth, async (req, res) => {
53 | try {
54 | const item = await Item.findById(req.params.id);
55 | if (!item) throw Error('No item found');
56 |
57 | const removed = await item.remove();
58 | if (!removed)
59 | throw Error('Something went wrong while trying to delete the item');
60 |
61 | res.status(200).json({ success: true });
62 | } catch (e) {
63 | res.status(400).json({ msg: e.message, success: false });
64 | }
65 | });
66 |
67 | export default router;
68 |
--------------------------------------------------------------------------------
/routes/api/users.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | // User Model
3 | import User from '../../models/User';
4 |
5 | const router = Router();
6 |
7 | /**
8 | * @route GET api/users
9 | * @desc Get all users
10 | * @access Private
11 | */
12 |
13 | router.get('/', async (req, res) => {
14 | try {
15 | const users = await User.find();
16 | if (!users) throw Error('No users exist');
17 | res.json(users);
18 | } catch (e) {
19 | res.status(400).json({ msg: e.message });
20 | }
21 | });
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import app from './app';
2 | import config from './config';
3 |
4 | const { PORT } = config;
5 |
6 | app.listen(PORT, () => console.log(`Server started on PORT ${PORT}`));
7 |
--------------------------------------------------------------------------------