├── .babelrc
├── .gitignore
├── app.js
├── browser
└── src
│ ├── react-redux
│ ├── App
│ │ ├── App.react.js
│ │ ├── ForgotForm
│ │ │ └── ForgotForm.react.js
│ │ ├── LoginForm
│ │ │ └── LoginForm.react.js
│ │ ├── Navigation
│ │ │ └── Navigation.react.js
│ │ ├── ProfileForm
│ │ │ └── ProfileForm.react.js
│ │ ├── ResetForm
│ │ │ └── ResetForm.react.js
│ │ ├── SignupForm
│ │ │ └── SignupForm.react.js
│ │ ├── User.actions.js
│ │ └── User.reducer.js
│ ├── index.js
│ ├── index.scss
│ ├── rootReducer.js
│ └── store.js
│ └── sass
│ ├── application.scss
│ ├── base
│ ├── _base.scss
│ ├── _index.scss
│ └── _normalize.scss
│ ├── layout
│ ├── _authentication.scss
│ ├── _extends.scss
│ ├── _index.scss
│ ├── _login.scss
│ └── _signup.scss
│ ├── modules
│ ├── _extends.scss
│ └── _index.scss
│ ├── states
│ ├── _extends.scss
│ └── _index.scss
│ └── utilities
│ ├── _config.scss
│ ├── _functions.scss
│ ├── _helpers.scss
│ ├── _index.scss
│ └── _mixins.scss
├── dist
└── index.html
├── package-lock.json
├── package.json
├── server
├── models
│ └── user.js
└── routes
│ ├── api
│ ├── forgot.js
│ ├── index.js
│ ├── login.js
│ ├── logout.js
│ ├── reset.js
│ ├── user.js
│ └── whoami.js
│ └── index.js
├── test
├── api-forgot.route.js
├── api-login-route.js
├── api-logout-route.js
├── api-reset.route.js
├── api-user-route.js
├── api-whoami-route.js
└── base-route.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "@babel/plugin-proposal-class-properties",
4 | "react-hot-loader/babel"
5 | ],
6 | "presets": [
7 | "@babel/preset-env",
8 | "@babel/preset-react"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # bundles
61 | application.css
62 | bundle.js
63 | bundle.js.map
64 |
65 | # public
66 | browser/dist
67 |
68 | .vscode
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const bodyParser = require('body-parser');
3 | const chalk = require('chalk');
4 | const express = require('express'),
5 | app = express();
6 | const mongoose = require('mongoose');
7 | const morgan = require('morgan');
8 | const session = require('express-session'),
9 | MongoStore = require('connect-mongo')(session);
10 | const router = require('./server/routes');
11 |
12 | // TODO: put as env variable
13 | mongoose.connect('mongodb://localhost:27017/fstemplate');
14 | const db = mongoose.connection;
15 | db.on('error', console.error.bind(console, 'connection error:'));
16 |
17 | app.use(session({
18 | maxAge: 1000 * 60 * 60 * 24 * 7,
19 | resave: true,
20 | saveUnitialized: false,
21 | // TODO: put this in the env
22 | secret: 'TODO: make this an env var',
23 | store: new MongoStore({
24 | mongooseConnection: db
25 | })
26 | }));
27 |
28 | // middleware
29 | app.use(morgan('dev'));
30 |
31 | app.use(bodyParser.urlencoded({ extended: true }));
32 | app.use(bodyParser.json());
33 |
34 | // TODO: Outside of webpack-dev-server, index is looking for assets in the wrong place. Where SHOULD we put assets?
35 | app.use(express.static('browser/assets'));
36 |
37 | app.use('/', router);
38 |
39 | app.listen(process.env.PORT || 3000, function() {
40 | console.log(chalk.green(`App is listening on port ${this.address().port}`));
41 | });
42 |
43 | module.exports = app;
--------------------------------------------------------------------------------
/browser/src/react-redux/App/App.react.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import {connect} from 'react-redux';
4 | import {Switch, Redirect} from 'react-router';
5 | import {Route} from 'react-router-dom';
6 | import ForgotForm from './ForgotForm/ForgotForm.react';
7 | import LoginForm from './LoginForm/LoginForm.react';
8 | import Navigation from './Navigation/Navigation.react';
9 | import ProfileForm from './ProfileForm/ProfileForm.react';
10 | import ResetForm from './ResetForm/ResetForm.react';
11 | import SignupForm from './SignupForm/SignupForm.react';
12 | import {logout, updateStoreWithUser} from './User.actions';
13 |
14 | const AuthenticatedRoute = ({isAllowed, ...props}) =>
15 | isAllowed
16 | ?
If an account with the email you provided exists, you will receive instructions to reset your password.
58 | Go to login page 59 |51 | This is an invalid token. Please go back to the forgot password page and request a new token. 52 |
53 | ); 54 | } 55 | 56 | handleInputChange = ({target: {name, value}}) => { 57 | this.setState({ 58 | [name]: value, 59 | }); 60 | this.updateFieldErrors({ 61 | [name]: '' 62 | }); 63 | }; 64 | 65 | handleSubmit = (e) => { 66 | e.preventDefault(); 67 | 68 | const {confirmPassword, password} = this.state; 69 | const passwordsMatch = password === confirmPassword; 70 | const fieldErrors = {}; 71 | if (password && !passwordsMatch) { 72 | this.updateFieldErrors({ 73 | confirmPassword: 'Passwords must match' 74 | }) 75 | } else { 76 | this.setState({fieldErrors}, () => this.changePassword()); 77 | } 78 | } 79 | 80 | updateFieldErrors(fieldErrors) { 81 | this.setState({ 82 | fieldErrors: { 83 | ...this.state.fieldErrors, 84 | ...fieldErrors 85 | } 86 | }) 87 | } 88 | 89 | verifyToken() { 90 | axios.get(`/api/reset/${this.props.match.params.token}`) 91 | .then(() => { 92 | this.setState({ 93 | tokenIsValid: true 94 | }); 95 | }); 96 | } 97 | 98 | render() { 99 | if (!this.state.tokenIsValid) { 100 | return this.generateInvalidTokenMessage(); 101 | } 102 | 103 | const {confirmPassword, fieldErrors, password} = this.state; 104 | const confirmPasswordError = fieldErrors.confirmPassword; 105 | const passwordError = fieldErrors.password; 106 | return ( 107 | 136 | ); 137 | } 138 | } 139 | 140 | export default connect(null, {updateStoreWithUser})(ResetForm); -------------------------------------------------------------------------------- /browser/src/react-redux/App/SignupForm/SignupForm.react.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import PropTypes from 'prop-types'; 3 | import React, {Component} from 'react'; 4 | import {connect} from 'react-redux'; 5 | import {updateStoreWithUser} from '../User.actions'; 6 | 7 | class SignupForm extends Component { 8 | 9 | static propTypes = { 10 | user: PropTypes.shape({ 11 | _id: PropTypes.string, 12 | email: PropTypes.string 13 | }) 14 | }; 15 | 16 | state = { 17 | confirmPassword: '', 18 | email: '', 19 | fieldErrors: {}, 20 | genericError: '', 21 | password: '', 22 | }; 23 | 24 | // todo: dont need to pass email and password, just grab state 25 | createUser() { 26 | const {email, password} = this.state; 27 | axios.post('/api/user', {email, password}) 28 | .then((response) => { 29 | this.props.updateStoreWithUser(response.data.user); 30 | }) 31 | .catch((error) => { 32 | const errorBody = error.response.data.error; 33 | if (error.response.status === 400) { 34 | const fieldErrors = errorBody.errors; 35 | if (fieldErrors) { 36 | const fieldErrorsState = Object.entries(fieldErrors).reduce((fieldErrorsState, [fieldName, fieldError]) => { 37 | fieldErrorsState[fieldName] = fieldError.message; 38 | return fieldErrorsState; 39 | }, {}); 40 | this.updateFieldErrors(fieldErrorsState); 41 | } else { 42 | this.setState({ 43 | genericError: errorBody.message 44 | }); 45 | } 46 | } 47 | }) 48 | } 49 | 50 | handleInputChange = ({target: {name, value}}) => { 51 | this.setState({ 52 | [name]: value, 53 | }); 54 | this.updateFieldErrors({ 55 | [name]: '' 56 | }); 57 | }; 58 | 59 | handleSubmit = (e) => { 60 | e.preventDefault(); 61 | 62 | const {confirmPassword, email, password} = this.state; 63 | const passwordsMatch = password === confirmPassword; 64 | const fieldErrors = {}; 65 | if (password && !passwordsMatch) { 66 | this.updateFieldErrors({ 67 | confirmPassword: 'Passwords must match' 68 | }) 69 | } else { 70 | this.setState({fieldErrors}, () => this.createUser()); 71 | } 72 | } 73 | 74 | updateFieldErrors(fieldErrors) { 75 | this.setState({ 76 | fieldErrors: { 77 | ...this.state.fieldErrors, 78 | ...fieldErrors 79 | } 80 | }) 81 | } 82 | 83 | render() { 84 | const {confirmPassword, email, fieldErrors, genericError, password} = this.state; 85 | const confirmPasswordError = fieldErrors.confirmPassword; 86 | const emailError = fieldErrors.email; 87 | const passwordError = fieldErrors.password; 88 | 89 | return ( 90 | 137 | ); 138 | } 139 | } 140 | 141 | const mapStateToProps = (state) => ({ 142 | user: state.user 143 | }); 144 | 145 | export default connect(mapStateToProps, {updateStoreWithUser})(SignupForm); -------------------------------------------------------------------------------- /browser/src/react-redux/App/User.actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const SET_USER = 'SET_USER'; 4 | 5 | const _setUser = (user) => ({ 6 | type: SET_USER, 7 | user 8 | }); 9 | 10 | export const updateStoreWithUser = (user) => 11 | dispatch => { 12 | if (user) { 13 | return dispatch(_setUser(user)); 14 | } 15 | return axios.get('/api/whoami') 16 | .then((response) => dispatch(_setUser(response.data.user))) 17 | .catch((error) => console.error(error)) 18 | } 19 | 20 | export const logout = () => 21 | dispatch => 22 | axios.get('/api/logout') 23 | .then(() => { 24 | dispatch(updateStoreWithUser()); 25 | }) 26 | .catch((err) => { 27 | console.error(err); 28 | }); -------------------------------------------------------------------------------- /browser/src/react-redux/App/User.reducer.js: -------------------------------------------------------------------------------- 1 | const SET_USER = 'SET_USER'; 2 | const CLEAR_USER = 'CLEAR_USER'; 3 | 4 | const initialState = null; 5 | 6 | export default (state=initialState, action) => { 7 | switch(action.type) { 8 | case SET_USER: 9 | const user = action.user; 10 | return !!user ? {...user} : initialState; 11 | default: 12 | return state; 13 | } 14 | } -------------------------------------------------------------------------------- /browser/src/react-redux/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Route, BrowserRouter} from 'react-router-dom'; 4 | import {Provider} from 'react-redux'; 5 | import store from './store'; 6 | import './index.scss'; 7 | 8 | import App from './App/App.react'; 9 | 10 | ReactDOM.render( 11 |You are receiving this because you, or someone else, requested a password reset. Click here to finish resetting your password.
` 36 | }, (err, info) => { 37 | console.error(err); 38 | }) 39 | 40 | res.sendStatus(200); 41 | } catch(e) { 42 | console.error(e); 43 | } 44 | }); 45 | } else { 46 | res.sendStatus(200); 47 | } 48 | } catch(e) { 49 | // TODO: send something to the catchall 500 50 | console.error(e); 51 | } 52 | }); 53 | 54 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/api/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const express = require('express'); 3 | const router = express.Router(); 4 | 5 | const forgotApi = require('./forgot'); 6 | const loginApi = require('./login'); 7 | const logoutApi = require('./logout'); 8 | const resetApi = require('./reset'); 9 | const userApi = require('./user'); 10 | const whoamiApi = require('./whoami'); 11 | 12 | router.use('/forgot', forgotApi); 13 | router.use('/login', loginApi); 14 | router.use('/logout', logoutApi); 15 | router.use('/reset', resetApi); 16 | router.use('/user', userApi); 17 | router.use('/whoami', whoamiApi); 18 | 19 | // API Catchall Error Handlers 20 | 21 | router.use((err, req, res, next) => { 22 | if (err.name === 'ValidationError' || err.name === 'AuthenticationError') { 23 | err.status = 400; 24 | } 25 | next(err); 26 | }); 27 | 28 | router.use((req, res, next) => { 29 | const err = new Error('Route not found.'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | router.use((err, req, res, next) => { 35 | res.status(err.status || 500); 36 | res.json({error: err}); 37 | }); 38 | 39 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/api/login.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const User = require('../../models/user'); 4 | 5 | router.post('/', (req, res, next) => { 6 | const {email, password} = req.body; 7 | 8 | // todo: we should probably validate empty email and password and send validation error back 9 | // if we do this, remember to update login form with field errors 10 | 11 | return User.authenticate({email, password}) 12 | .then((user) => { 13 | const userObj = user.toObject(); 14 | const {password, resetPassword, ...trimmedUser} = userObj; 15 | req.session.userId = user._id; 16 | res.json({ 17 | user: trimmedUser 18 | }); 19 | }) 20 | .catch((err) => { 21 | return next(err); 22 | }); 23 | }); 24 | 25 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/api/logout.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | router.get('/', (req, res) => { 5 | req.session.destroy(); 6 | res.sendStatus(200); 7 | }); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/api/reset.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const express = require('express'), 3 | router = express.Router(); 4 | const User = require('../../models/user'); 5 | 6 | router.get('/:token', (req, res, next) => { 7 | return User.findOne({ 8 | 'resetPassword.token': req.params.token, 9 | 'resetPassword.expiration': { 10 | $gt: Date.now() 11 | } 12 | }).select('-password') 13 | .then((user) => { 14 | if (!user) { 15 | return next({ 16 | message: `Invalid or expired token.`, 17 | name: 'ValidationError' 18 | }); 19 | } 20 | res.json(user); 21 | }) 22 | .catch((err) => { 23 | next(err); 24 | }); 25 | }); 26 | 27 | router.post('/:token', async (req, res, next) => { 28 | let user = await User.findOne({ 29 | 'resetPassword.token': req.params.token, 30 | 'resetPassword.expiration': { 31 | $gt: Date.now() 32 | } 33 | }).select('-password'); 34 | 35 | if (!user) { 36 | return next({ 37 | message: `Invalid or expired token.`, 38 | name: 'ValidationError' 39 | }); 40 | } 41 | 42 | user.password = req.body.password; 43 | try { 44 | user = await user.save(); 45 | req.session.userId = user._id; // todo: this needs a test 46 | } catch(e) { 47 | return next(e); 48 | } 49 | 50 | try { 51 | user = await User.findById(user.id).select('-password'); 52 | } catch(e) { 53 | return next(e); 54 | } 55 | 56 | user.resetPassword = undefined; 57 | return user.save() 58 | .then((user) => { 59 | res.json({user}); 60 | }) 61 | .catch((err) => { 62 | next(err); 63 | }); 64 | }); 65 | 66 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/api/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | router = express.Router(); 3 | const mongoose = require('mongoose'); 4 | const User = require('../../models/user'); 5 | 6 | router.patch('/:userId', async (req, res, next) => { 7 | const {email, password} = req.body; 8 | const oldPassword = req.body.old_password; 9 | const userId = req.params.userId; 10 | 11 | if (!mongoose.Types.ObjectId.isValid(userId)) { 12 | const error = { 13 | message: 'Invalid User ID.', 14 | name: 'ValidationError' // TODO: should this be a "validation" error? 15 | }; 16 | return next(error); 17 | } 18 | 19 | const fields = {}; 20 | if (req.body.hasOwnProperty('email')) { 21 | fields.email = email; 22 | } 23 | 24 | const user = await User.findById(userId).select('-password -resetPassword'); 25 | if (!user) { 26 | // TODO: should we even tell them the user exists? maybe just throw a 500 27 | // TODO: why aren't we simply passing this through to next? 28 | res.status(404); 29 | // TODO: shouldn't this just pass it through next 30 | return res.json({ 31 | error: { 32 | message: `User doesn't exist.` 33 | // TODO: label this with a custom name. What should it be? Authentication? 34 | } 35 | }); 36 | } 37 | 38 | if (req.body.hasOwnProperty('email')) { 39 | user.email = email; 40 | } 41 | 42 | if (req.body.hasOwnProperty('old_password')) { 43 | user.old_password = oldPassword; 44 | } 45 | 46 | if (req.body.hasOwnProperty('password')) { 47 | user.password = password; 48 | } 49 | 50 | return user 51 | .save() 52 | .then((user) => { 53 | return res.json({user}); 54 | }) 55 | .catch((err) => { 56 | return next(err); 57 | }); 58 | }); 59 | 60 | router.post('/', (req, res, next) => { 61 | const {email, password} = req.body; 62 | 63 | return User.create({email, password}) 64 | .then((user) => { 65 | const userObj = user.toObject(); 66 | const {password, resetPassword, ...trimmedUser} = userObj; 67 | return res.status(201).json({ 68 | user: trimmedUser 69 | }); 70 | }) 71 | .catch((err) => { 72 | return next(err); 73 | }); 74 | }); 75 | 76 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/api/whoami.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const User = require('../../models/user'); 4 | 5 | router.get('/', (req, res, next) => { 6 | const session = req.session; 7 | const userId = session ? req.session.userId : null; 8 | if (!userId) { 9 | return res.json({user: null}); 10 | } 11 | 12 | return User.findById(userId).select('-password') 13 | .exec((error, user) => { 14 | if (error) { 15 | return next(error); 16 | } 17 | return user ? res.json({user}) : res.json({user: null}); 18 | }); 19 | }); 20 | 21 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const router = express.Router(); 4 | 5 | const apiRouter = require('./api'); 6 | 7 | router.use('/api', apiRouter); 8 | 9 | router.get('*', (req, res) => { 10 | res.sendFile(path.join(__dirname, '../../dist/index.html')); 11 | }); 12 | 13 | module.exports = router; -------------------------------------------------------------------------------- /test/api-forgot.route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const app = require('../app'); 4 | const expect = chai.expect; 5 | chai.use(chaiHttp); 6 | const User = require('../server/models/user'); 7 | 8 | // TODO: re-run tests whenever a code change happens in the app 9 | 10 | describe('\'/api/forget\' Route', () => { 11 | describe('POST Request', () => { 12 | const email = 'thisisnotarealemail@gmail.com'; 13 | const emailThatDoesntExist = 'thisemaildoesntexist@gmail.com'; 14 | const password = 'rightpassword'; 15 | 16 | beforeEach(function() { 17 | return User.create({email, password}); 18 | }); 19 | 20 | describe(`sent with an email that doesn't exist`, function() { 21 | const status = 200; 22 | it(`responds with status ${status}`, function(done) { 23 | chai.request(app) 24 | .post('/api/forgot') 25 | .type('form') 26 | .send({emailThatDoesntExist}) 27 | .end((err, res) => { 28 | expect(res).to.have.status(status); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('made with an email that does exist', function() { 35 | const status = 200; 36 | it(`responds with status ${status}`, function(done) { 37 | chai.request(app) 38 | .post('/api/forgot') 39 | .type('form') 40 | .send({email}) 41 | .end((err, res) => { 42 | expect(res).to.have.status(200); 43 | User.findOne({email}) 44 | .then((user) => { 45 | expect(user).to.be.an('object'); 46 | expect(user.email).to.equal(email); 47 | expect(user.resetPassword.token).to.be.string; 48 | expect(user.resetPassword.token).to.not.be.empty; 49 | expect(user.resetPassword.expiration.getTime()).to.be.lessThan(Date.now() + 1000 * 60 * 60); 50 | done(); 51 | }) 52 | .catch((err) => { 53 | done(err); 54 | }) 55 | }); 56 | }); 57 | }); 58 | 59 | afterEach(function() { 60 | return User.remove({email}); 61 | }); 62 | }); 63 | }); -------------------------------------------------------------------------------- /test/api-login-route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const app = require('../app'); 4 | const expect = chai.expect; 5 | chai.use(chaiHttp); 6 | const User = require('../server/models/user'); 7 | 8 | // TODO: re-run tests whenever a code change happens in the app 9 | 10 | describe('\'/api/login\' Route', () => { 11 | describe('POST Request', () => { 12 | const email = 'thisisnotarealemail@gmail.com'; 13 | const password = 'rightpassword'; 14 | const wrongPassword = 'wrongpassword'; 15 | 16 | beforeEach(function() { 17 | return User.create({email, password}); 18 | }); 19 | 20 | describe('made with an invalid email and password', function() { 21 | const errorMessage = 'Incorrect username and password combination.'; 22 | const status = 400; 23 | it(`responds with status ${status} and includes message '${errorMessage}'`, function(done) { 24 | chai.request(app) 25 | .post('/api/login') 26 | .type('form') 27 | .send({ 28 | email, 29 | password: wrongPassword 30 | }) 31 | .end((err, res) => { 32 | expect(res).to.have.status(status); 33 | expect(res.body.error.message).to.equal(errorMessage); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('made with a valid email and password', function() { 40 | const status = 200; 41 | it(`responds with a user object and status ${status}`, function(done) { 42 | chai.request(app) 43 | .post('/api/login') 44 | .type('form') 45 | .send({email, password}) 46 | .end((err, res) => { 47 | expect(res).to.have.status(200); 48 | expect(res.body).to.have.property('user'); 49 | expect(res.body.user).to.be.an('object'); 50 | expect(res.body.user.email).to.equal(email); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | 56 | afterEach(function() { 57 | return User.remove({email}); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /test/api-logout-route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const app = require('../app'); 4 | const User = require('../server/models/user'); 5 | const expect = chai.expect; 6 | chai.use(chaiHttp); 7 | 8 | describe('\'/api/logout\' Route', function() { 9 | const user = { 10 | email: 'thisisnotarealemail@gmail.com', 11 | password: 'password' 12 | }; 13 | 14 | beforeEach(function() { 15 | return User.create(user) 16 | }); 17 | 18 | afterEach(function() { 19 | return User.remove({email: user.email}); 20 | }); 21 | 22 | describe('GET Request', function() { 23 | const status = 200; 24 | const agent = chai.request.agent(app); 25 | it(`logs out user and responds with a status of ${status}`, function(done) { 26 | agent.post('/api/login') 27 | .type('form') 28 | .send({ 29 | email: 'thisisnotarealemail@gmail.com', 30 | password: 'rightpassword' 31 | }) 32 | .then((res) => { 33 | agent.get('/api/logout') 34 | .then((res) => { 35 | expect(res).to.have.status(status); 36 | agent.get('/api/whoami') 37 | .then((res) => { 38 | expect(res.body.user).to.be.null; 39 | done(); 40 | }) 41 | .catch((err) => done(err)); 42 | }) 43 | }); 44 | }); 45 | }); 46 | }); -------------------------------------------------------------------------------- /test/api-reset.route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const chalk = require('chalk'); 4 | const app = require('../app'); 5 | const expect = chai.expect; 6 | chai.use(chaiHttp); 7 | const User = require('../server/models/user'); 8 | 9 | // TODO: re-run tests whenever a code change happens in the app 10 | 11 | describe('\'/api/reset/:token\' Route', () => { 12 | const email = 'thisisnotarealemail@gmail.com'; 13 | const tokenErrorMessage = 'Invalid or expired token.'; 14 | const invalidToken = 'heyImanInValiDToken'; 15 | const password = 'rightpassword'; 16 | 17 | describe('GET Request', () => { 18 | 19 | beforeEach(function() { 20 | return User.create({email, password}); 21 | }); 22 | 23 | describe(`send with a token that doesn't exist or is invalid`, function() { 24 | const status = 400; 25 | it(`responds with status ${status}`, function(done) { 26 | chai.request(app) 27 | .get(`/api/reset/${invalidToken}`) 28 | .end((err, res) => { 29 | expect(res).to.have.status(status); 30 | expect(res.body.error.message).to.equal(tokenErrorMessage); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe(`send with a token that is expired`, function() { 37 | const status = 400; 38 | it(`responds with status ${status} and error message ${tokenErrorMessage}`, function(done) { 39 | chai.request(app) 40 | .post('/api/forgot') 41 | .type('form') 42 | .send({email}) 43 | .end((err, res) => { 44 | User.findOne({email}) 45 | .then((user) => { 46 | user.resetPassword.expiration = Date.now(); // << culprit (monkaS) 47 | user.save() 48 | .then((user) => { 49 | chai.request(app) 50 | .get(`/api/reset/${user.resetPassword.token}`) 51 | .end((err, res) => { 52 | expect(res).to.have.status(status); 53 | expect(res.body.error.message).to.equal(tokenErrorMessage); 54 | done(); 55 | }); 56 | }) 57 | }) 58 | .catch((err) => { 59 | console.error(err); 60 | }); 61 | }); 62 | }) 63 | }); 64 | 65 | describe(`send a valid token`, function() { 66 | const status = 200; 67 | it(`responds with a status ${status}`, function(done) { 68 | chai.request(app) 69 | .post('/api/forgot') 70 | .type('form') 71 | .send({email}) 72 | .end((err, res) => { 73 | User.findOne({email}) 74 | .then((user) => { 75 | chai.request(app) 76 | .get(`/api/reset/${user.resetPassword.token}`) 77 | .end((err, res) => { 78 | expect(res).to.have.status(status); 79 | done(); 80 | }); 81 | }) 82 | .catch((err) => { 83 | console.error(err); 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | afterEach(function() { 90 | return User.remove({email}); 91 | }); 92 | }); 93 | 94 | describe('POST request', function() { 95 | beforeEach(function() { 96 | return User.create({email, password}); 97 | }); 98 | 99 | describe(`send with a token that doesn't exist or is invalid`, function() { 100 | const status = 400; 101 | it(`responds with status ${status}`, function(done) { 102 | chai.request(app) 103 | .post(`/api/reset/${invalidToken}`) 104 | .type('form') 105 | .send({email}) 106 | .end((err, res) => { 107 | expect(res).to.have.status(status); 108 | expect(res.body.error.message).to.equal(tokenErrorMessage); 109 | done(); 110 | }); 111 | }); 112 | }); 113 | 114 | describe(`send with a token that is expired`, function() { 115 | const status = 400; 116 | it(`responds with status ${status} and error message ${tokenErrorMessage}`, function(done) { 117 | chai.request(app) 118 | .post('/api/forgot') 119 | .type('form') 120 | .send({email}) 121 | .end((err, res) => { 122 | User.findOne({email}) 123 | .then((user) => { 124 | user.resetPassword.expiration = Date.now(); 125 | user.save() 126 | .then((user) => { 127 | chai.request(app) 128 | .post(`/api/reset/${user.resetPassword.token}`) 129 | .type('form') 130 | .end((err, res) => { 131 | expect(res).to.have.status(status); 132 | expect(res.body.error.message).to.equal(tokenErrorMessage); 133 | done(); 134 | }); 135 | }) 136 | }) 137 | .catch((err) => { 138 | console.error(err); 139 | }); 140 | }); 141 | }) 142 | }); 143 | 144 | describe(`send with a token that is valid, with a blank password`, function() { 145 | const passwordEmptyErrorMessage = 'You must provide a password.'; 146 | const status = 400; 147 | const validationErrorName = 'ValidationError'; 148 | it(`responds with status ${status} and error message ${tokenErrorMessage}`, function(done) { 149 | chai.request(app) 150 | .post('/api/forgot') 151 | .type('form') 152 | .send({email}) 153 | .end((err, res) => { 154 | User.findOne({email}) 155 | .then((user) => { 156 | chai.request(app) 157 | .post(`/api/reset/${user.resetPassword.token}`) 158 | .type('form') 159 | .end((err, res) => { 160 | expect(res).to.have.status(status); 161 | expect(res.body.error.name).to.equal(validationErrorName); 162 | expect(res.body.error.errors.password.message).to.equal(passwordEmptyErrorMessage); 163 | done(); 164 | }); 165 | }) 166 | .catch((err) => { 167 | console.error(err); 168 | }); 169 | }); 170 | }); 171 | }); 172 | 173 | describe(`send with a token that is valid, with a password`, function() { 174 | const newPassword = 'thisisanewpassword'; 175 | const status = 200; 176 | it(`responds with status ${status} and include user`, function(done) { 177 | chai.request(app) 178 | .post('/api/forgot') 179 | .type('form') 180 | .send({email}) 181 | .end((err, res) => { 182 | User.findOne({email}) 183 | .then((user) => { 184 | chai.request(app) 185 | .post(`/api/reset/${user.resetPassword.token}`) 186 | .type('form') 187 | .send({password: newPassword}) 188 | .end((err, res) => { 189 | expect(res).to.have.status(status); 190 | expect(res.body.user.email).to.equal(email); 191 | expect(res.body.user).to.not.have.property('resetPassword'); 192 | done(); 193 | }); 194 | }) 195 | .catch((err) => { 196 | console.error(err); 197 | }); 198 | }); 199 | }); 200 | }); 201 | 202 | afterEach(function() { 203 | return User.remove({email}); 204 | }); 205 | }) 206 | }); -------------------------------------------------------------------------------- /test/api-user-route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const app = require('../app'); 4 | const expect = chai.expect; 5 | chai.use(chaiHttp); 6 | const User = require('../server/models/user'); 7 | // TODO: re-run tests whenever a code change happens in the app 8 | 9 | describe('\'/api/user\' Route', function() { 10 | const emailDuplicateErrorMessage = 'This email address is already taken.'; 11 | const emailEmptyErrorMessage = 'You must provide an email address.'; 12 | const validationErrorName = 'ValidationError'; 13 | describe('POST Request', function() { 14 | const email = 'thisisnotarealemail@gmail.com'; 15 | const password = 'password'; 16 | const passwordEmptyErrorMessage = 'You must provide a password.'; 17 | 18 | describe('made with an empty payload', function() { 19 | const status = 400; 20 | it(`responds with status ${status} and includes error messages '${emailEmptyErrorMessage}' & '${passwordEmptyErrorMessage}'`, function(done) { 21 | chai.request(app) 22 | .post('/api/user') 23 | .type('form') 24 | .send() 25 | .end(function(err, res) { 26 | expect(res).to.have.status(status); 27 | expect(res.body.error.name).to.equal(validationErrorName); 28 | expect(res.body.error.errors.email.message).to.equal(emailEmptyErrorMessage); 29 | expect(res.body.error.errors.password.message).to.equal(passwordEmptyErrorMessage); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('made without an email address', function() { 36 | const status = 400; 37 | it(`responds with status ${status} and includes message '${emailEmptyErrorMessage}'`, function(done) { 38 | chai.request(app) 39 | .post('/api/user') 40 | .type('form') 41 | .send({password}) 42 | .end(function(err, res) { 43 | expect(res).to.have.status(status); 44 | expect(res.body.error.name).to.equal(validationErrorName); 45 | expect(res.body.error.errors.email.message).to.equal(emailEmptyErrorMessage); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('made without a password', function() { 52 | const status = 400; 53 | it(`responds with status ${status} and includes message '${passwordEmptyErrorMessage}'`, function(done) { 54 | chai.request(app) 55 | .post('/api/user') 56 | .type('form') 57 | .send({email}) 58 | .end(function(err, res) { 59 | expect(res).to.have.status(status); 60 | expect(res.body.error.name).to.equal(validationErrorName); 61 | expect(res.body.error.errors.password.message).to.equal(passwordEmptyErrorMessage); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('made with all necessary fields', function() { 68 | const status = 201; 69 | it(`responds with status ${status}`, async function() { 70 | const userCreate = await chai.request(app) 71 | .post('/api/user') 72 | .type('form') 73 | .send({email, password}); 74 | expect(userCreate).to.have.status(status); 75 | 76 | const user = await User.findOne({email}); 77 | expect(user).to.include({email}); 78 | }); 79 | }); 80 | 81 | describe('made with duplicate email, which should be unique', function() { 82 | const status = 400; 83 | it(`responds with a status of ${status} and includes message '${emailDuplicateErrorMessage}'`, async function() { 84 | await User.create({email, password}); 85 | 86 | return chai.request(app) 87 | .post('/api/user') 88 | .type('form') 89 | .send({email, password}) 90 | .catch((error) => { 91 | expect(error).to.have.status(status); 92 | expect(error.response.body.error.name).to.equal(validationErrorName); 93 | expect(error.response.body.error.errors.email.message).to.equal(emailDuplicateErrorMessage); 94 | }); 95 | }); 96 | }); 97 | 98 | afterEach(function() { 99 | return User.remove({email}); 100 | }); 101 | }); 102 | 103 | describe('PATCH Request', function() { 104 | const emailToChangeTo = 'thisisnotarealemail3@gmail.com'; 105 | const password = 'test'; 106 | const oldPasswordThatDoesntWork = 'oldPasswordThatDoesntWork'; 107 | const user1 = { 108 | email: 'thisisnotarealemail@gmail.com', 109 | password: 'password' 110 | } 111 | const user2 = { 112 | email: 'thisisnotarealemail2@gmail.com', 113 | password: 'password' 114 | } 115 | 116 | beforeEach(function() { 117 | return Promise.all([ 118 | User.create(user1), 119 | User.create(user2) 120 | ]); 121 | }); 122 | 123 | // test for user not found (404) 124 | describe('made with an invalid user id', function() { 125 | const errorMessage = 'Invalid User ID.'; 126 | const status = 400; 127 | it(`responds with status ${status} and includes message '${errorMessage}'`, function(done) { 128 | chai.request(app) 129 | .patch(`/api/user/thisiddoesntexist`) 130 | .type('form') 131 | .send() 132 | .end(function(err, res) { 133 | expect(res).to.have.status(status); 134 | expect(res.body.error).to.have.property('name').eql(validationErrorName); 135 | expect(res.body.error).to.have.property('message').eql(errorMessage); 136 | done(); 137 | }); 138 | }); 139 | }); 140 | 141 | describe('made with a non-existing user id', function() { 142 | const errorMessage = `User doesn't exist.`; 143 | const status = 404; 144 | it(`responds with status ${status} and includes message '${errorMessage}'`, function(done) { 145 | User.findOne({email: user1.email}) 146 | .then((user) => { 147 | User.remove({email: user1.email}) 148 | .then(() => { 149 | chai.request(app) 150 | .patch(`/api/user/${user._id}`) 151 | .type('form') 152 | .send() 153 | .end(function(err, res) { 154 | expect(res).to.have.status(status); 155 | expect(res.body.error.message).to.equal(errorMessage); 156 | done(); 157 | }); 158 | }) 159 | .catch((err) => { 160 | done(err); 161 | }); 162 | }) 163 | .catch((err) => { 164 | done(err); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('made with an empty payload', function() { 170 | const status = 200; 171 | it(`responds with status ${status} and includes user.email with value of ${user1.email}`, function(done) { 172 | User.findOne({email: user1.email}) 173 | .then((user) => { 174 | chai.request(app) 175 | .patch(`/api/user/${user._id}`) 176 | .type('form') 177 | .send() 178 | .end(function(err, res) { 179 | expect(res).to.have.status(status); 180 | expect(res.body).to.have.property('user'); 181 | expect(res.body.user).to.be.an('object'); 182 | expect(res.body.user.email).to.equal(user1.email); 183 | done(); 184 | }); 185 | }) 186 | .catch((err) => { 187 | done(err); 188 | }); 189 | }); 190 | }); 191 | 192 | describe('made with an empty email address', function() { 193 | const status = 400; 194 | it(`responds with status ${status} and includes message ${emailEmptyErrorMessage}`, function(done) { 195 | User.findOne({email: user1.email}) 196 | .then((user) => { 197 | chai.request(app) 198 | .patch(`/api/user/${user._id}`) 199 | .type('form') 200 | .send({ 201 | email: '' 202 | }) 203 | .end(function(err, res) { 204 | expect(res).to.have.status(status); 205 | expect(res.body.error).to.have.property('name').eql(validationErrorName); 206 | expect(res.body.error.errors.email.message).to.equal(emailEmptyErrorMessage); 207 | done(); 208 | }); 209 | }) 210 | .catch((err) => { 211 | done(err); 212 | }); 213 | }); 214 | }); 215 | 216 | // // TODO: get async await to work in tests 217 | // describe('made with an empty payload', function() { 218 | // const status = 200; 219 | // it(`responds with status ${status} and includes user.email with value of ${user1.email}`, async function() { 220 | // const user = await User.findOne({email: user1.email}); 221 | 222 | // chai.request(app) 223 | // .patch(`/api/user/${user._id}`) 224 | // .type('form') 225 | // .send() 226 | // .then((res) => { 227 | // expect(res).to.have.status(status); 228 | // expect(res.body).to.have.property('user'); 229 | // expect(res.body.user).to.be.an('object'); 230 | // expect(res.body.user.email).to.equal(user1.email); 231 | // }, (err) => { 232 | // console.log('IS SOMETIMES RECEIVING A 404. WHY U NO WORK!?'); 233 | // }); 234 | // }); 235 | // }); 236 | 237 | describe('made with a new email address', function() { 238 | const status = 200; 239 | it(`responds with status ${status} and includes user.email with value of ${emailToChangeTo}`, function(done) { 240 | User.findOne({email: user1.email}) 241 | .then((user) => { 242 | chai.request(app) 243 | .patch(`/api/user/${user._id}`) 244 | .type('form') 245 | .send({email: emailToChangeTo}) 246 | .end(function(err, res) { 247 | expect(res).to.have.status(status); 248 | expect(res.body).to.have.property('user'); 249 | expect(res.body.user).to.be.an('object'); 250 | expect(res.body.user.email).to.equal(emailToChangeTo); 251 | done(); 252 | }); 253 | }) 254 | .catch((err) => { 255 | done(err); 256 | }); 257 | }); 258 | }); 259 | 260 | describe('made with a duplicate email address to update', function() { 261 | const status = 400; 262 | it(`responds with status ${status} and includes message '${emailDuplicateErrorMessage}'`, function(done) { 263 | User.findOne({email: user1.email}) 264 | .then((user) => { 265 | chai.request(app) 266 | .patch(`/api/user/${user._id}`) 267 | .type('form') 268 | .send({email: user2.email}) 269 | .end(function(err, res) { 270 | expect(res).to.have.status(status); 271 | expect(res.body.error.name).to.equal(validationErrorName); 272 | expect(res.body.error.errors.email.message).to.equal(emailDuplicateErrorMessage); 273 | done(); 274 | }); 275 | }) 276 | .catch((err) => { 277 | done(err); 278 | }); 279 | }); 280 | }); 281 | 282 | describe('made with a new password, without providing a current (old) password', function() { 283 | const status = 400; 284 | const errorMessage = 'Old password required in order to change password.'; 285 | it(`responds with status ${status} and includes message '${errorMessage}`, function(done) { 286 | User.findOne({email: user1.email}) 287 | .then((user) => { 288 | chai.request(app) 289 | .patch(`/api/user/${user._id}`) 290 | .type('form') 291 | .send({ 292 | email: emailToChangeTo, 293 | password 294 | }) 295 | .end(function(err, res) { 296 | expect(res).to.have.status(status); 297 | expect(res.body.error.name).to.equal(validationErrorName); 298 | expect(res.body.error.errors.old_password.message).to.equal(errorMessage); 299 | done(); 300 | }); 301 | }) 302 | .catch((err) => { 303 | done(err); 304 | }); 305 | }); 306 | }); 307 | 308 | describe('Providing a current (old) password, but not including a new password', function() { 309 | const status = 400; 310 | const errorMessage = 'If your old password is provided, it is assumed that you are trying to change your password. Please provide a new password.'; 311 | it(`responds with status ${status} and includes message '${errorMessage}`, function(done) { 312 | User.findOne({email: user1.email}) 313 | .then((user) => { 314 | chai.request(app) 315 | .patch(`/api/user/${user._id}`) 316 | .type('form') 317 | .send({ 318 | email: emailToChangeTo, 319 | old_password: user1.password 320 | }) 321 | .end(function(err, res) { 322 | expect(res).to.have.status(status); 323 | expect(res.body.error.name).to.equal(validationErrorName); 324 | expect(res.body.error.errors.password.message).to.equal(errorMessage); 325 | done(); 326 | }); 327 | }) 328 | .catch((err) => { 329 | done(err); 330 | }); 331 | }); 332 | }); 333 | 334 | describe('Providing an incorrect old password, and a new password', function() { 335 | const status = 400; 336 | const errorMessage = 'Invalid old password.'; 337 | it(`responds with status ${status} and includes message '${errorMessage}`, function(done) { 338 | User.findOne({email: user1.email}) 339 | .then((user) => { 340 | chai.request(app) 341 | .patch(`/api/user/${user._id}`) 342 | .type('form') 343 | .send({ 344 | email: emailToChangeTo, 345 | old_password: oldPasswordThatDoesntWork, 346 | password 347 | }) 348 | .end(function(err, res) { 349 | expect(res).to.have.status(status); 350 | expect(res.body.error.name).to.equal(validationErrorName); 351 | expect(res.body.error.errors.old_password.message).to.equal(errorMessage); 352 | done(); 353 | }); 354 | }) 355 | .catch((err) => { 356 | done(err); 357 | }); 358 | }); 359 | }); 360 | 361 | describe('Providing a correct old password, and a new password', function() { 362 | const status = 200; 363 | it(`responds with status ${status}`, function(done) { 364 | User.findOne({email: user1.email}) 365 | .then((user) => { 366 | chai.request(app) 367 | .patch(`/api/user/${user._id}`) 368 | .type('form') 369 | .send({ 370 | old_password: user1.password, 371 | password 372 | }) 373 | .end(function(err, res) { 374 | expect(res).to.have.status(status); 375 | expect(res.body).to.have.property('user'); 376 | expect(res.body.user).to.be.an('object'); 377 | done(); 378 | }); 379 | }) 380 | .catch((err) => { 381 | done(err); 382 | }); 383 | }); 384 | }); 385 | 386 | afterEach(function() { 387 | return Promise.all([ 388 | User.remove({email: user1.email}), 389 | User.remove({email: user2.email}), 390 | User.remove({email: emailToChangeTo}) 391 | ]); 392 | }); 393 | }); 394 | }); -------------------------------------------------------------------------------- /test/api-whoami-route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const app = require('../app'); 4 | const expect = chai.expect; 5 | chai.use(chaiHttp); 6 | const User = require('../server/models/user'); 7 | 8 | describe('\'/api/whoami\' Route', function() { 9 | const email = 'thisisnotarealemail@gmail.com'; 10 | const password = 'rightpassword'; 11 | const agent = chai.request.agent(app); 12 | 13 | beforeEach(function() { 14 | return User.create({email, password}) 15 | .then(() => { 16 | return agent.post('/api/login') 17 | .type('form') 18 | .send({ 19 | email: 'thisisnotarealemail@gmail.com', 20 | password: 'rightpassword' 21 | }) 22 | }); 23 | }); 24 | 25 | describe('GET Request', function() { 26 | const status = 200; 27 | it(`responds with a status of ${status} and a proper user object`, function(done) { 28 | User.findOne({email}) 29 | .then(({_id}) => { 30 | agent.get('/api/whoami') 31 | .then((res) => { 32 | expect(res).to.have.status(status); 33 | expect(res.body.user).to.be.an('object'); 34 | expect(res.body.user).to.include({ 35 | email, 36 | _id: _id.toString() 37 | }); 38 | done(); 39 | }) 40 | .catch((err) => { 41 | done(err); 42 | }); 43 | }) 44 | .catch((err) => { 45 | done(err); 46 | }) 47 | }); 48 | }); 49 | 50 | afterEach(function() { 51 | return User.remove({email}) 52 | }) 53 | }); -------------------------------------------------------------------------------- /test/base-route.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | const app = require('../app'); 4 | const expect = chai.expect; 5 | chai.use(chaiHttp); 6 | 7 | // TODO: re-run tests whenever a code change happens in the app 8 | // TODO: test any route outside of root and api and make sure it serves a 200 (index html with react) 9 | 10 | describe('\'/\' Route', function() { 11 | beforeEach(function() { 12 | // webpack-dev-server doesn't build index 13 | if (process.env.NODE_ENV === 'DEVELOPMENT') { 14 | this.skip(); 15 | } 16 | }); 17 | 18 | describe('GET Request', function() { 19 | const status = 200; 20 | it(`responds with status ${status}`, function(done) { 21 | chai.request(app) 22 | .get('/') 23 | .end(function(err, res) { 24 | expect(res).to.have.status(status); 25 | done(); 26 | }); 27 | }); 28 | }); 29 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const config = { 5 | entry: './browser/src/react-redux/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | }, 17 | { 18 | test: /\.scss$/, 19 | use: [ 20 | 'style-loader', 21 | 'css-loader', 22 | 'sass-loader' 23 | ] 24 | } 25 | ] 26 | }, 27 | resolve: { 28 | extensions: [ 29 | '.js', 30 | '.jsx' 31 | ] 32 | }, 33 | devServer: { 34 | contentBase: './dist', 35 | proxy: { 36 | '/': `http://localhost:${process.env.PORT || 3000}` 37 | } 38 | } 39 | } 40 | 41 | module.exports = config; --------------------------------------------------------------------------------