├── .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 |
55 | 56 | 57 | 64 | 67 | 68 |
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 |
75 | 76 | 77 | 85 | 86 | 87 | 95 | 103 | 104 |
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 |
86 | 87 | 88 | 96 | 97 | 98 | 106 | 107 | 108 | 116 | 119 | 120 |
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 | --------------------------------------------------------------------------------