├── .editorconfig
├── .gitignore
├── LICENCE
├── README.md
├── part-1-creating-components
├── .eslintrc.json
├── README.md
├── client
│ └── src
│ │ ├── app.jsx
│ │ ├── components
│ │ ├── Base.jsx
│ │ ├── HomePage.jsx
│ │ ├── LoginForm.jsx
│ │ └── SignUpForm.jsx
│ │ ├── containers
│ │ ├── LoginPage.jsx
│ │ └── SignUpPage.jsx
│ │ └── routes.js
├── index.js
├── package.json
├── server
│ ├── routes
│ │ └── auth.js
│ └── static
│ │ ├── css
│ │ └── style.css
│ │ └── index.html
└── webpack.config.js
├── part-2-json-web-token
├── .eslintrc.json
├── README.md
├── client
│ └── src
│ │ ├── app.jsx
│ │ ├── components
│ │ ├── Base.jsx
│ │ ├── Dashboard.jsx
│ │ ├── HomePage.jsx
│ │ ├── LoginForm.jsx
│ │ └── SignUpForm.jsx
│ │ ├── containers
│ │ ├── DashboardPage.jsx
│ │ ├── LoginPage.jsx
│ │ └── SignUpPage.jsx
│ │ ├── modules
│ │ └── Auth.js
│ │ └── routes.js
├── config
│ └── index.json
├── index.js
├── package.json
├── server
│ ├── middleware
│ │ └── auth-check.js
│ ├── models
│ │ ├── index.js
│ │ └── user.js
│ ├── passport
│ │ ├── local-login.js
│ │ └── local-signup.js
│ ├── routes
│ │ ├── api.js
│ │ └── auth.js
│ └── static
│ │ ├── css
│ │ └── style.css
│ │ └── index.html
└── webpack.config.js
└── screenshot.png
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.swp
4 | .idea
5 | part-1-creating-components/client/dist/
6 | part-1-creating-components/node_modules/
7 | part-2-json-web-token/client/dist/
8 | part-2-json-web-token/node_modules/
9 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Vladimir Ponomarev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Authentication in React Applications
2 | ====================================
3 | The source code for a two-part tutorial for beginners who want to start to build applications using React and add an authentication layer to it. It was written for my blog, [https://vladimirponomarev.com](http://vladimirponomarev.com).
4 |
5 | Parts of the tutorial
6 | ---------------------
7 | 1. [Creating Components](https://vladimirponomarev.com/blog/authentication-in-react-apps-creating-components) - in this part, we will create a basic application server, learn to bundle scripts using Webpack, get acquainted with basics of React (components, JSX syntax, props, states).
8 | 2. [Authentication Using JSON Web Token (JWT)](https://vladimirponomarev.com/blog/authentication-in-react-apps-jwt) - in this part, we will continue to improve the application and add authentication to it.
9 |
10 | Screenshot
11 | ----------
12 | 
13 |
--------------------------------------------------------------------------------
/part-1-creating-components/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "env": {
4 | "node": true,
5 | "browser": true
6 | },
7 | "installedESLint": true,
8 | "plugins": [
9 | "react",
10 | "jsx-a11y",
11 | "import"
12 | ],
13 | "rules": {
14 | "semi": [1, "always"],
15 | "comma-dangle": [0, "never"],
16 | "no-underscore-dangle": 0
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/part-1-creating-components/README.md:
--------------------------------------------------------------------------------
1 | Creating Components
2 | ===================
3 | in this part, we will create a basic application server, learn to bundle scripts using Webpack, get acquainted with basics of React (components, JSX syntax, props, states).
4 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | import injectTapEventPlugin from 'react-tap-event-plugin';
4 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
6 | import { browserHistory, Router } from 'react-router';
7 | import routes from './routes.js';
8 |
9 | // remove tap delay, essential for MaterialUI to work properly
10 | injectTapEventPlugin();
11 |
12 | ReactDom.render((
13 |
14 |
15 | ), document.getElementById('react-app'));
16 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/components/Base.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 |
5 | const Base = ({ children }) => (
6 |
7 |
8 |
9 | React App
10 |
11 |
12 |
13 | Log in
14 | Sign up
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 | );
23 |
24 | Base.propTypes = {
25 | children: PropTypes.object.isRequired
26 | };
27 |
28 | export default Base;
29 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/components/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardTitle } from 'material-ui/Card';
3 |
4 |
5 | const HomePage = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default HomePage;
12 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { Card, CardText } from 'material-ui/Card';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import TextField from 'material-ui/TextField';
6 |
7 |
8 | const LoginForm = ({
9 | onSubmit,
10 | onChange,
11 | errors,
12 | user
13 | }) => (
14 |
15 |
47 |
48 | );
49 |
50 | LoginForm.propTypes = {
51 | onSubmit: PropTypes.func.isRequired,
52 | onChange: PropTypes.func.isRequired,
53 | errors: PropTypes.object.isRequired,
54 | user: PropTypes.object.isRequired
55 | };
56 |
57 | export default LoginForm;
58 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/components/SignUpForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { Card, CardText } from 'material-ui/Card';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import TextField from 'material-ui/TextField';
6 |
7 |
8 | const SignUpForm = ({
9 | onSubmit,
10 | onChange,
11 | errors,
12 | user,
13 | }) => (
14 |
15 |
57 |
58 | );
59 |
60 | SignUpForm.propTypes = {
61 | onSubmit: PropTypes.func.isRequired,
62 | onChange: PropTypes.func.isRequired,
63 | errors: PropTypes.object.isRequired,
64 | user: PropTypes.object.isRequired
65 | };
66 |
67 | export default SignUpForm;
68 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/containers/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import LoginForm from '../components/LoginForm.jsx';
3 |
4 |
5 | class LoginPage extends React.Component {
6 |
7 | /**
8 | * Class constructor.
9 | */
10 | constructor(props) {
11 | super(props);
12 |
13 | // set the initial component state
14 | this.state = {
15 | errors: {},
16 | user: {
17 | email: '',
18 | password: ''
19 | }
20 | };
21 |
22 | this.processForm = this.processForm.bind(this);
23 | this.changeUser = this.changeUser.bind(this);
24 | }
25 |
26 | /**
27 | * Process the form.
28 | *
29 | * @param {object} event - the JavaScript event object
30 | */
31 | processForm(event) {
32 | // prevent default action. in this case, action is the form submission event
33 | event.preventDefault();
34 |
35 | // create a string for an HTTP body message
36 | const email = encodeURIComponent(this.state.user.email);
37 | const password = encodeURIComponent(this.state.user.password);
38 | const formData = `email=${email}&password=${password}`;
39 |
40 | // create an AJAX request
41 | const xhr = new XMLHttpRequest();
42 | xhr.open('post', '/auth/login');
43 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
44 | xhr.responseType = 'json';
45 | xhr.addEventListener('load', () => {
46 | if (xhr.status === 200) {
47 | // success
48 |
49 | // change the component-container state
50 | this.setState({
51 | errors: {}
52 | });
53 |
54 | console.log('The form is valid');
55 | } else {
56 | // failure
57 |
58 | // change the component state
59 | const errors = xhr.response.errors ? xhr.response.errors : {};
60 | errors.summary = xhr.response.message;
61 |
62 | this.setState({
63 | errors
64 | });
65 | }
66 | });
67 | xhr.send(formData);
68 | }
69 |
70 | /**
71 | * Change the user object.
72 | *
73 | * @param {object} event - the JavaScript event object
74 | */
75 | changeUser(event) {
76 | const field = event.target.name;
77 | const user = this.state.user;
78 | user[field] = event.target.value;
79 |
80 | this.setState({
81 | user
82 | });
83 | }
84 |
85 | /**
86 | * Render the component.
87 | */
88 | render() {
89 | return (
90 |
96 | );
97 | }
98 |
99 | }
100 |
101 | export default LoginPage;
102 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/containers/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import SignUpForm from '../components/SignUpForm.jsx';
3 |
4 |
5 | class SignUpPage extends React.Component {
6 |
7 | /**
8 | * Class constructor.
9 | */
10 | constructor(props) {
11 | super(props);
12 |
13 | // set the initial component state
14 | this.state = {
15 | errors: {},
16 | user: {
17 | email: '',
18 | name: '',
19 | password: ''
20 | }
21 | };
22 |
23 | this.processForm = this.processForm.bind(this);
24 | this.changeUser = this.changeUser.bind(this);
25 | }
26 |
27 | /**
28 | * Change the user object.
29 | *
30 | * @param {object} event - the JavaScript event object
31 | */
32 | changeUser(event) {
33 | const field = event.target.name;
34 | const user = this.state.user;
35 | user[field] = event.target.value;
36 |
37 | this.setState({
38 | user
39 | });
40 | }
41 |
42 | /**
43 | * Process the form.
44 | *
45 | * @param {object} event - the JavaScript event object
46 | */
47 | processForm(event) {
48 | // prevent default action. in this case, action is the form submission event
49 | event.preventDefault();
50 |
51 | // create a string for an HTTP body message
52 | const name = encodeURIComponent(this.state.user.name);
53 | const email = encodeURIComponent(this.state.user.email);
54 | const password = encodeURIComponent(this.state.user.password);
55 | const formData = `name=${name}&email=${email}&password=${password}`;
56 |
57 | // create an AJAX request
58 | const xhr = new XMLHttpRequest();
59 | xhr.open('post', '/auth/signup');
60 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
61 | xhr.responseType = 'json';
62 | xhr.addEventListener('load', () => {
63 | if (xhr.status === 200) {
64 | // success
65 |
66 | // change the component-container state
67 | this.setState({
68 | errors: {}
69 | });
70 |
71 | console.log('The form is valid');
72 | } else {
73 | // failure
74 |
75 | const errors = xhr.response.errors ? xhr.response.errors : {};
76 | errors.summary = xhr.response.message;
77 |
78 | this.setState({
79 | errors
80 | });
81 | }
82 | });
83 | xhr.send(formData);
84 | }
85 |
86 | /**
87 | * Render the component.
88 | */
89 | render() {
90 | return (
91 |
97 | );
98 | }
99 |
100 | }
101 |
102 | export default SignUpPage;
103 |
--------------------------------------------------------------------------------
/part-1-creating-components/client/src/routes.js:
--------------------------------------------------------------------------------
1 | import Base from './components/Base.jsx';
2 | import HomePage from './components/HomePage.jsx';
3 | import LoginPage from './containers/LoginPage.jsx';
4 | import SignUpPage from './containers/SignUpPage.jsx';
5 |
6 |
7 | const routes = {
8 | // base component (wrapper for the whole application).
9 | component: Base,
10 | childRoutes: [
11 |
12 | {
13 | path: '/',
14 | component: HomePage
15 | },
16 |
17 | {
18 | path: '/login',
19 | component: LoginPage
20 | },
21 |
22 | {
23 | path: '/signup',
24 | component: SignUpPage
25 | }
26 |
27 | ]
28 | };
29 |
30 | export default routes;
31 |
--------------------------------------------------------------------------------
/part-1-creating-components/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const app = express();
5 | // tell the app to look for static files in these directories
6 | app.use(express.static('./server/static/'));
7 | app.use(express.static('./client/dist/'));
8 | // tell the app to parse HTTP body messages
9 | app.use(bodyParser.urlencoded({ extended: false }));
10 |
11 | // routes
12 | const authRoutes = require('./server/routes/auth');
13 | app.use('/auth', authRoutes);
14 |
15 | // start the server
16 | app.listen(3000, () => {
17 | console.log('Server is running on http://localhost:3000 or http://127.0.0.1:3000');
18 | });
19 |
20 |
--------------------------------------------------------------------------------
/part-1-creating-components/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "authentication-in-react-apps",
3 | "version": "1.0.0",
4 | "description": "Authentication in React Applications, Part 1: Creating Components",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon --use_strict index.js",
8 | "bundle": "webpack"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/vladimirponomarev/authentication-in-react-apps.git"
13 | },
14 | "author": "Vladimir Ponomarev",
15 | "license": "MIT",
16 | "dependencies": {
17 | "body-parser": "^1.15.2",
18 | "express": "^4.14.0",
19 | "material-ui": "^0.16.5",
20 | "react": "^15.4.1",
21 | "react-dom": "^15.4.1",
22 | "react-router": "^3.0.0",
23 | "react-tap-event-plugin": "^2.0.1",
24 | "validator": "^6.2.0"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.21.0",
28 | "babel-loader": "^6.2.10",
29 | "babel-preset-es2015": "^6.18.0",
30 | "babel-preset-react": "^6.16.0",
31 | "nodemon": "^1.11.0",
32 | "webpack": "^1.14.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part-1-creating-components/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const validator = require('validator');
3 |
4 | const router = new express.Router();
5 |
6 | /**
7 | * Validate the sign up form
8 | *
9 | * @param {object} payload - the HTTP body message
10 | * @returns {object} The result of validation. Object contains a boolean validation result,
11 | * errors tips, and a global message for the whole form.
12 | */
13 | function validateSignupForm(payload) {
14 | const errors = {};
15 | let isFormValid = true;
16 | let message = '';
17 |
18 | if (!payload || typeof payload.email !== 'string' || !validator.isEmail(payload.email)) {
19 | isFormValid = false;
20 | errors.email = 'Please provide a correct email address.';
21 | }
22 |
23 | if (!payload || typeof payload.password !== 'string' || payload.password.trim().length < 8) {
24 | isFormValid = false;
25 | errors.password = 'Password must have at least 8 characters.';
26 | }
27 |
28 | if (!payload || typeof payload.name !== 'string' || payload.name.trim().length === 0) {
29 | isFormValid = false;
30 | errors.name = 'Please provide your name.';
31 | }
32 |
33 | if (!isFormValid) {
34 | message = 'Check the form for errors.';
35 | }
36 |
37 | return {
38 | success: isFormValid,
39 | message,
40 | errors
41 | };
42 | }
43 |
44 | /**
45 | * Validate the login form
46 | *
47 | * @param {object} payload - the HTTP body message
48 | * @returns {object} The result of validation. Object contains a boolean validation result,
49 | * errors tips, and a global message for the whole form.
50 | */
51 | function validateLoginForm(payload) {
52 | const errors = {};
53 | let isFormValid = true;
54 | let message = '';
55 |
56 | if (!payload || typeof payload.email !== 'string' || payload.email.trim().length === 0) {
57 | isFormValid = false;
58 | errors.email = 'Please provide your email address.';
59 | }
60 |
61 | if (!payload || typeof payload.password !== 'string' || payload.password.trim().length === 0) {
62 | isFormValid = false;
63 | errors.password = 'Please provide your password.';
64 | }
65 |
66 | if (!isFormValid) {
67 | message = 'Check the form for errors.';
68 | }
69 |
70 | return {
71 | success: isFormValid,
72 | message,
73 | errors
74 | };
75 | }
76 |
77 | router.post('/signup', (req, res) => {
78 | const validationResult = validateSignupForm(req.body);
79 | if (!validationResult.success) {
80 | return res.status(400).json({
81 | success: false,
82 | message: validationResult.message,
83 | errors: validationResult.errors
84 | });
85 | }
86 |
87 | return res.status(200).end();
88 | });
89 |
90 | router.post('/login', (req, res) => {
91 | const validationResult = validateLoginForm(req.body);
92 | if (!validationResult.success) {
93 | return res.status(400).json({
94 | success: false,
95 | message: validationResult.message,
96 | errors: validationResult.errors
97 | });
98 | }
99 |
100 | return res.status(200).end();
101 | });
102 |
103 |
104 | module.exports = router;
105 |
--------------------------------------------------------------------------------
/part-1-creating-components/server/static/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-size: 16px;
4 | }
5 |
6 | a {
7 | color: #00bcd4;
8 | font-weight: bold;
9 | text-decoration: none;
10 | transition: color 0.4s;
11 | }
12 |
13 | a:hover {
14 | color: #1976d2;
15 | }
16 |
17 | .text-center {
18 | text-align: center;
19 | }
20 |
21 | .top-bar {
22 | padding: 10px 15px;
23 | margin-bottom: 50px;
24 | }
25 | .top-bar::after {
26 | content: '';
27 | display: block;
28 | clear: both;
29 | }
30 |
31 | .top-bar-left {
32 | float: left;
33 | font-size: 1.5em;
34 | }
35 |
36 | .top-bar-right {
37 | float: right;
38 | }
39 |
40 | .top-bar a,
41 | .nav a {
42 | margin: 0 8px;
43 | }
44 |
45 | .container {
46 | margin: 0 auto;
47 | text-align: center;
48 | width: 700px;
49 | }
50 |
51 | .card-heading {
52 | padding: 16px;
53 | }
54 |
55 | .field-line, .button-line {
56 | padding: 16px;
57 | }
58 |
59 | .error-message {
60 | padding: 0 16px;
61 | color: tomato;
62 | }
63 |
64 | .success-message {
65 | padding: 0 16px;
66 | color: green;
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/part-1-creating-components/server/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React App
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/part-1-creating-components/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 |
4 | module.exports = {
5 | // the entry file for the bundle
6 | entry: path.join(__dirname, '/client/src/app.jsx'),
7 |
8 | // the bundle file we will get in the result
9 | output: {
10 | path: path.join(__dirname, '/client/dist/js'),
11 | filename: 'app.js',
12 | },
13 |
14 | module: {
15 |
16 | // apply loaders to files that meet given conditions
17 | loaders: [{
18 | test: /\.jsx?$/,
19 | include: path.join(__dirname, '/client/src'),
20 | loader: 'babel',
21 | query: {
22 | presets: ["react", "es2015"]
23 | }
24 | }],
25 | },
26 |
27 | // start Webpack in a watch mode, so Webpack will rebuild the bundle on changes
28 | watch: true
29 | };
30 |
--------------------------------------------------------------------------------
/part-2-json-web-token/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "env": {
4 | "node": true,
5 | "browser": true
6 | },
7 | "installedESLint": true,
8 | "plugins": [
9 | "react",
10 | "jsx-a11y",
11 | "import"
12 | ],
13 | "rules": {
14 | "semi": [1, "always"],
15 | "comma-dangle": [0, "never"],
16 | "no-underscore-dangle": 0
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/part-2-json-web-token/README.md:
--------------------------------------------------------------------------------
1 | Authentication Using JSON Web Token (JWT)
2 | =========================================
3 | In this part, we will continue to improve the application and add authentication to it.
4 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | import injectTapEventPlugin from 'react-tap-event-plugin';
4 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
6 | import { browserHistory, Router } from 'react-router';
7 | import routes from './routes.js';
8 |
9 | // remove tap delay, essential for MaterialUI to work properly
10 | injectTapEventPlugin();
11 |
12 | ReactDom.render((
13 |
14 |
15 | ), document.getElementById('react-app'));
16 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/components/Base.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 | import Auth from '../modules/Auth';
4 |
5 |
6 | const Base = ({ children }) => (
7 |
8 |
9 |
10 | React App
11 |
12 |
13 | {Auth.isUserAuthenticated() ? (
14 |
15 | Log out
16 |
17 | ) : (
18 |
19 | Log in
20 | Sign up
21 |
22 | )}
23 |
24 |
25 |
26 | { /* child component will be rendered here */ }
27 | {children}
28 |
29 |
30 | );
31 |
32 | Base.propTypes = {
33 | children: PropTypes.object.isRequired
34 | };
35 |
36 | export default Base;
37 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/components/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Card, CardTitle, CardText } from 'material-ui/Card';
3 |
4 |
5 | const Dashboard = ({ secretData }) => (
6 |
7 |
11 |
12 | {secretData && {secretData}}
13 |
14 | );
15 |
16 | Dashboard.propTypes = {
17 | secretData: PropTypes.string.isRequired
18 | };
19 |
20 | export default Dashboard;
21 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/components/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardTitle } from 'material-ui/Card';
3 |
4 |
5 | const HomePage = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default HomePage;
12 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { Card, CardText } from 'material-ui/Card';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import TextField from 'material-ui/TextField';
6 |
7 |
8 | const LoginForm = ({
9 | onSubmit,
10 | onChange,
11 | errors,
12 | successMessage,
13 | user
14 | }) => (
15 |
16 |
49 |
50 | );
51 |
52 | LoginForm.propTypes = {
53 | onSubmit: PropTypes.func.isRequired,
54 | onChange: PropTypes.func.isRequired,
55 | errors: PropTypes.object.isRequired,
56 | successMessage: PropTypes.string.isRequired,
57 | user: PropTypes.object.isRequired
58 | };
59 |
60 | export default LoginForm;
61 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/components/SignUpForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { Card, CardText } from 'material-ui/Card';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import TextField from 'material-ui/TextField';
6 |
7 |
8 | const SignUpForm = ({
9 | onSubmit,
10 | onChange,
11 | errors,
12 | user,
13 | }) => (
14 |
15 |
57 |
58 | );
59 |
60 | SignUpForm.propTypes = {
61 | onSubmit: PropTypes.func.isRequired,
62 | onChange: PropTypes.func.isRequired,
63 | errors: PropTypes.object.isRequired,
64 | user: PropTypes.object.isRequired
65 | };
66 |
67 | export default SignUpForm;
68 |
69 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/containers/DashboardPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Auth from '../modules/Auth';
3 | import Dashboard from '../components/Dashboard.jsx';
4 |
5 |
6 | class DashboardPage extends React.Component {
7 |
8 | /**
9 | * Class constructor.
10 | */
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | secretData: ''
16 | };
17 | }
18 |
19 | /**
20 | * This method will be executed after initial rendering.
21 | */
22 | componentDidMount() {
23 | const xhr = new XMLHttpRequest();
24 | xhr.open('get', '/api/dashboard');
25 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
26 | // set the authorization HTTP header
27 | xhr.setRequestHeader('Authorization', `bearer ${Auth.getToken()}`);
28 | xhr.responseType = 'json';
29 | xhr.addEventListener('load', () => {
30 | if (xhr.status === 200) {
31 | this.setState({
32 | secretData: xhr.response.message
33 | });
34 | }
35 | });
36 | xhr.send();
37 | }
38 |
39 | /**
40 | * Render the component.
41 | */
42 | render() {
43 | return ();
44 | }
45 |
46 | }
47 |
48 | export default DashboardPage;
49 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/containers/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import Auth from '../modules/Auth';
3 | import LoginForm from '../components/LoginForm.jsx';
4 |
5 |
6 | class LoginPage extends React.Component {
7 |
8 | /**
9 | * Class constructor.
10 | */
11 | constructor(props, context) {
12 | super(props, context);
13 |
14 | const storedMessage = localStorage.getItem('successMessage');
15 | let successMessage = '';
16 |
17 | if (storedMessage) {
18 | successMessage = storedMessage;
19 | localStorage.removeItem('successMessage');
20 | }
21 |
22 | // set the initial component state
23 | this.state = {
24 | errors: {},
25 | successMessage,
26 | user: {
27 | email: '',
28 | password: ''
29 | }
30 | };
31 |
32 | this.processForm = this.processForm.bind(this);
33 | this.changeUser = this.changeUser.bind(this);
34 | }
35 |
36 | /**
37 | * Process the form.
38 | *
39 | * @param {object} event - the JavaScript event object
40 | */
41 | processForm(event) {
42 | // prevent default action. in this case, action is the form submission event
43 | event.preventDefault();
44 |
45 | // create a string for an HTTP body message
46 | const email = encodeURIComponent(this.state.user.email);
47 | const password = encodeURIComponent(this.state.user.password);
48 | const formData = `email=${email}&password=${password}`;
49 |
50 | // create an AJAX request
51 | const xhr = new XMLHttpRequest();
52 | xhr.open('post', '/auth/login');
53 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
54 | xhr.responseType = 'json';
55 | xhr.addEventListener('load', () => {
56 | if (xhr.status === 200) {
57 | // success
58 |
59 | // change the component-container state
60 | this.setState({
61 | errors: {}
62 | });
63 |
64 | // save the token
65 | Auth.authenticateUser(xhr.response.token);
66 |
67 |
68 | // change the current URL to /
69 | this.context.router.replace('/');
70 | } else {
71 | // failure
72 |
73 | // change the component state
74 | const errors = xhr.response.errors ? xhr.response.errors : {};
75 | errors.summary = xhr.response.message;
76 |
77 | this.setState({
78 | errors
79 | });
80 | }
81 | });
82 | xhr.send(formData);
83 | }
84 |
85 | /**
86 | * Change the user object.
87 | *
88 | * @param {object} event - the JavaScript event object
89 | */
90 | changeUser(event) {
91 | const field = event.target.name;
92 | const user = this.state.user;
93 | user[field] = event.target.value;
94 |
95 | this.setState({
96 | user
97 | });
98 | }
99 |
100 | /**
101 | * Render the component.
102 | */
103 | render() {
104 | return (
105 |
112 | );
113 | }
114 |
115 | }
116 |
117 | LoginPage.contextTypes = {
118 | router: PropTypes.object.isRequired
119 | };
120 |
121 | export default LoginPage;
122 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/containers/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import SignUpForm from '../components/SignUpForm.jsx';
3 |
4 |
5 | class SignUpPage extends React.Component {
6 |
7 | /**
8 | * Class constructor.
9 | */
10 | constructor(props, context) {
11 | super(props, context);
12 |
13 | // set the initial component state
14 | this.state = {
15 | errors: {},
16 | user: {
17 | email: '',
18 | name: '',
19 | password: ''
20 | }
21 | };
22 |
23 | this.processForm = this.processForm.bind(this);
24 | this.changeUser = this.changeUser.bind(this);
25 | }
26 |
27 | /**
28 | * Process the form.
29 | *
30 | * @param {object} event - the JavaScript event object
31 | */
32 | processForm(event) {
33 | // prevent default action. in this case, action is the form submission event
34 | event.preventDefault();
35 |
36 | // create a string for an HTTP body message
37 | const name = encodeURIComponent(this.state.user.name);
38 | const email = encodeURIComponent(this.state.user.email);
39 | const password = encodeURIComponent(this.state.user.password);
40 | const formData = `name=${name}&email=${email}&password=${password}`;
41 |
42 | // create an AJAX request
43 | const xhr = new XMLHttpRequest();
44 | xhr.open('post', '/auth/signup');
45 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
46 | xhr.responseType = 'json';
47 | xhr.addEventListener('load', () => {
48 | if (xhr.status === 200) {
49 | // success
50 |
51 | // change the component-container state
52 | this.setState({
53 | errors: {}
54 | });
55 |
56 | // set a message
57 | localStorage.setItem('successMessage', xhr.response.message);
58 |
59 | // make a redirect
60 | this.context.router.replace('/login');
61 | } else {
62 | // failure
63 |
64 | const errors = xhr.response.errors ? xhr.response.errors : {};
65 | errors.summary = xhr.response.message;
66 |
67 | this.setState({
68 | errors
69 | });
70 | }
71 | });
72 | xhr.send(formData);
73 | }
74 |
75 | /**
76 | * Change the user object.
77 | *
78 | * @param {object} event - the JavaScript event object
79 | */
80 | changeUser(event) {
81 | const field = event.target.name;
82 | const user = this.state.user;
83 | user[field] = event.target.value;
84 |
85 | this.setState({
86 | user
87 | });
88 | }
89 |
90 | /**
91 | * Render the component.
92 | */
93 | render() {
94 | return (
95 |
101 | );
102 | }
103 |
104 | }
105 |
106 | SignUpPage.contextTypes = {
107 | router: PropTypes.object.isRequired
108 | };
109 |
110 | export default SignUpPage;
111 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/modules/Auth.js:
--------------------------------------------------------------------------------
1 | class Auth {
2 |
3 | /**
4 | * Authenticate a user. Save a token string in Local Storage
5 | *
6 | * @param {string} token
7 | */
8 | static authenticateUser(token) {
9 | localStorage.setItem('token', token);
10 | }
11 |
12 | /**
13 | * Check if a user is authenticated - check if a token is saved in Local Storage
14 | *
15 | * @returns {boolean}
16 | */
17 | static isUserAuthenticated() {
18 | return localStorage.getItem('token') !== null;
19 | }
20 |
21 | /**
22 | * Deauthenticate a user. Remove a token from Local Storage.
23 | *
24 | */
25 | static deauthenticateUser() {
26 | localStorage.removeItem('token');
27 | }
28 |
29 | /**
30 | * Get a token value.
31 | *
32 | * @returns {string}
33 | */
34 |
35 | static getToken() {
36 | return localStorage.getItem('token');
37 | }
38 |
39 | }
40 |
41 | export default Auth;
42 |
--------------------------------------------------------------------------------
/part-2-json-web-token/client/src/routes.js:
--------------------------------------------------------------------------------
1 | import Base from './components/Base.jsx';
2 | import HomePage from './components/HomePage.jsx';
3 | import DashboardPage from './containers/DashboardPage.jsx';
4 | import LoginPage from './containers/LoginPage.jsx';
5 | import SignUpPage from './containers/SignUpPage.jsx';
6 | import Auth from './modules/Auth';
7 |
8 |
9 | const routes = {
10 | // base component (wrapper for the whole application).
11 | component: Base,
12 | childRoutes: [
13 |
14 | {
15 | path: '/',
16 | getComponent: (location, callback) => {
17 | if (Auth.isUserAuthenticated()) {
18 | callback(null, DashboardPage);
19 | } else {
20 | callback(null, HomePage);
21 | }
22 | }
23 | },
24 |
25 | {
26 | path: '/login',
27 | component: LoginPage
28 | },
29 |
30 | {
31 | path: '/signup',
32 | component: SignUpPage
33 | },
34 |
35 | {
36 | path: '/logout',
37 | onEnter: (nextState, replace) => {
38 | Auth.deauthenticateUser();
39 |
40 | // change the current URL to /
41 | replace('/');
42 | }
43 | }
44 |
45 | ]
46 | };
47 |
48 | export default routes;
49 |
--------------------------------------------------------------------------------
/part-2-json-web-token/config/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "dbUri": "mongodb://localhost/react_app",
3 | "jwtSecret": "a secret phrase!!"
4 | }
5 |
--------------------------------------------------------------------------------
/part-2-json-web-token/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const passport = require('passport');
4 | const config = require('./config');
5 |
6 | // connect to the database and load models
7 | require('./server/models').connect(config.dbUri);
8 |
9 | const app = express();
10 | // tell the app to look for static files in these directories
11 | app.use(express.static('./server/static/'));
12 | app.use(express.static('./client/dist/'));
13 | // tell the app to parse HTTP body messages
14 | app.use(bodyParser.urlencoded({ extended: false }));
15 | // pass the passport middleware
16 | app.use(passport.initialize());
17 |
18 | // load passport strategies
19 | const localSignupStrategy = require('./server/passport/local-signup');
20 | const localLoginStrategy = require('./server/passport/local-login');
21 | passport.use('local-signup', localSignupStrategy);
22 | passport.use('local-login', localLoginStrategy);
23 |
24 | // pass the authorization checker middleware
25 | const authCheckMiddleware = require('./server/middleware/auth-check');
26 | app.use('/api', authCheckMiddleware);
27 |
28 | // routes
29 | const authRoutes = require('./server/routes/auth');
30 | const apiRoutes = require('./server/routes/api');
31 | app.use('/auth', authRoutes);
32 | app.use('/api', apiRoutes);
33 |
34 |
35 | // start the server
36 | app.listen(3000, () => {
37 | console.log('Server is running on http://localhost:3000 or http://127.0.0.1:3000');
38 | });
39 |
--------------------------------------------------------------------------------
/part-2-json-web-token/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "authentication-in-react-apps",
3 | "version": "1.0.0",
4 | "description": "Authentication in React Applications, Part 2: JSON Web Token (JWT)",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon --use-strict index.js",
8 | "bundle": "webpack"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/vladimirponomarev/authentication-in-react-apps.git"
13 | },
14 | "author": "Vladimir Ponomarev",
15 | "license": "MIT",
16 | "dependencies": {
17 | "bcrypt": "^1.0.1",
18 | "body-parser": "^1.15.2",
19 | "express": "^4.14.0",
20 | "jsonwebtoken": "^7.2.1",
21 | "material-ui": "^0.16.5",
22 | "mongoose": "^4.7.3",
23 | "passport": "^0.3.2",
24 | "passport-local": "^1.0.0",
25 | "react": "^15.4.1",
26 | "react-dom": "^15.4.1",
27 | "react-router": "^3.0.0",
28 | "react-tap-event-plugin": "^2.0.1",
29 | "validator": "^6.2.0"
30 | },
31 | "devDependencies": {
32 | "babel-core": "^6.21.0",
33 | "babel-loader": "^6.2.10",
34 | "babel-preset-es2015": "^6.18.0",
35 | "babel-preset-react": "^6.16.0",
36 | "nodemon": "^1.11.0",
37 | "webpack": "^1.14.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/middleware/auth-check.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const User = require('mongoose').model('User');
3 | const config = require('../../config');
4 |
5 |
6 | /**
7 | * The Auth Checker middleware function.
8 | */
9 | module.exports = (req, res, next) => {
10 | if (!req.headers.authorization) {
11 | return res.status(401).end();
12 | }
13 |
14 | // get the last part from a authorization header string like "bearer token-value"
15 | const token = req.headers.authorization.split(' ')[1];
16 |
17 | // decode the token using a secret key-phrase
18 | return jwt.verify(token, config.jwtSecret, (err, decoded) => {
19 | // the 401 code is for unauthorized status
20 | if (err) { return res.status(401).end(); }
21 |
22 | const userId = decoded.sub;
23 |
24 | // check if a user exists
25 | return User.findById(userId, (userErr, user) => {
26 | if (userErr || !user) {
27 | return res.status(401).end();
28 | }
29 |
30 | return next();
31 | });
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/models/index.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | module.exports.connect = (uri) => {
4 | mongoose.connect(uri);
5 | // plug in the promise library:
6 | mongoose.Promise = global.Promise;
7 |
8 |
9 | mongoose.connection.on('error', (err) => {
10 | console.error(`Mongoose connection error: ${err}`);
11 | process.exit(1);
12 | });
13 |
14 | // load models
15 | require('./user');
16 | };
17 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const bcrypt = require('bcrypt');
3 |
4 | // define the User model schema
5 | const UserSchema = new mongoose.Schema({
6 | email: {
7 | type: String,
8 | index: { unique: true }
9 | },
10 | password: String,
11 | name: String
12 | });
13 |
14 |
15 | /**
16 | * Compare the passed password with the value in the database. A model method.
17 | *
18 | * @param {string} password
19 | * @returns {object} callback
20 | */
21 | UserSchema.methods.comparePassword = function comparePassword(password, callback) {
22 | bcrypt.compare(password, this.password, callback);
23 | };
24 |
25 |
26 | /**
27 | * The pre-save hook method.
28 | */
29 | UserSchema.pre('save', function saveHook(next) {
30 | const user = this;
31 |
32 | // proceed further only if the password is modified or the user is new
33 | if (!user.isModified('password')) return next();
34 |
35 |
36 | return bcrypt.genSalt((saltError, salt) => {
37 | if (saltError) { return next(saltError); }
38 |
39 | return bcrypt.hash(user.password, salt, (hashError, hash) => {
40 | if (hashError) { return next(hashError); }
41 |
42 | // replace a password string with hash value
43 | user.password = hash;
44 |
45 | return next();
46 | });
47 | });
48 | });
49 |
50 |
51 | module.exports = mongoose.model('User', UserSchema);
52 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/passport/local-login.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const User = require('mongoose').model('User');
3 | const PassportLocalStrategy = require('passport-local').Strategy;
4 | const config = require('../../config');
5 |
6 |
7 | /**
8 | * Return the Passport Local Strategy object.
9 | */
10 | module.exports = new PassportLocalStrategy({
11 | usernameField: 'email',
12 | passwordField: 'password',
13 | session: false,
14 | passReqToCallback: true
15 | }, (req, email, password, done) => {
16 | const userData = {
17 | email: email.trim(),
18 | password: password.trim()
19 | };
20 |
21 | // find a user by email address
22 | return User.findOne({ email: userData.email }, (err, user) => {
23 | if (err) { return done(err); }
24 |
25 | if (!user) {
26 | const error = new Error('Incorrect email or password');
27 | error.name = 'IncorrectCredentialsError';
28 |
29 | return done(error);
30 | }
31 |
32 | // check if a hashed user's password is equal to a value saved in the database
33 | return user.comparePassword(userData.password, (passwordErr, isMatch) => {
34 | if (err) { return done(err); }
35 |
36 | if (!isMatch) {
37 | const error = new Error('Incorrect email or password');
38 | error.name = 'IncorrectCredentialsError';
39 |
40 | return done(error);
41 | }
42 |
43 | const payload = {
44 | sub: user._id
45 | };
46 |
47 | // create a token string
48 | const token = jwt.sign(payload, config.jwtSecret);
49 | const data = {
50 | name: user.name
51 | };
52 |
53 | return done(null, token, data);
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/passport/local-signup.js:
--------------------------------------------------------------------------------
1 | const User = require('mongoose').model('User');
2 | const PassportLocalStrategy = require('passport-local').Strategy;
3 |
4 |
5 | /**
6 | * Return the Passport Local Strategy object.
7 | */
8 | module.exports = new PassportLocalStrategy({
9 | usernameField: 'email',
10 | passwordField: 'password',
11 | session: false,
12 | passReqToCallback: true
13 | }, (req, email, password, done) => {
14 | const userData = {
15 | email: email.trim(),
16 | password: password.trim(),
17 | name: req.body.name.trim()
18 | };
19 |
20 | const newUser = new User(userData);
21 | newUser.save((err) => {
22 | if (err) { return done(err); }
23 |
24 | return done(null);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/routes/api.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 |
4 | const router = new express.Router();
5 |
6 | router.get('/dashboard', (req, res) => {
7 | res.status(200).json({
8 | message: "You're authorized to see this secret message."
9 | });
10 | });
11 |
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const validator = require('validator');
3 | const passport = require('passport');
4 |
5 | const router = new express.Router();
6 |
7 | /**
8 | * Validate the sign up form
9 | *
10 | * @param {object} payload - the HTTP body message
11 | * @returns {object} The result of validation. Object contains a boolean validation result,
12 | * errors tips, and a global message for the whole form.
13 | */
14 | function validateSignupForm(payload) {
15 | const errors = {};
16 | let isFormValid = true;
17 | let message = '';
18 |
19 | if (!payload || typeof payload.email !== 'string' || !validator.isEmail(payload.email)) {
20 | isFormValid = false;
21 | errors.email = 'Please provide a correct email address.';
22 | }
23 |
24 | if (!payload || typeof payload.password !== 'string' || payload.password.trim().length < 8) {
25 | isFormValid = false;
26 | errors.password = 'Password must have at least 8 characters.';
27 | }
28 |
29 | if (!payload || typeof payload.name !== 'string' || payload.name.trim().length === 0) {
30 | isFormValid = false;
31 | errors.name = 'Please provide your name.';
32 | }
33 |
34 | if (!isFormValid) {
35 | message = 'Check the form for errors.';
36 | }
37 |
38 | return {
39 | success: isFormValid,
40 | message,
41 | errors
42 | };
43 | }
44 |
45 | /**
46 | * Validate the login form
47 | *
48 | * @param {object} payload - the HTTP body message
49 | * @returns {object} The result of validation. Object contains a boolean validation result,
50 | * errors tips, and a global message for the whole form.
51 | */
52 | function validateLoginForm(payload) {
53 | const errors = {};
54 | let isFormValid = true;
55 | let message = '';
56 |
57 | if (!payload || typeof payload.email !== 'string' || payload.email.trim().length === 0) {
58 | isFormValid = false;
59 | errors.email = 'Please provide your email address.';
60 | }
61 |
62 | if (!payload || typeof payload.password !== 'string' || payload.password.trim().length === 0) {
63 | isFormValid = false;
64 | errors.password = 'Please provide your password.';
65 | }
66 |
67 | if (!isFormValid) {
68 | message = 'Check the form for errors.';
69 | }
70 |
71 | return {
72 | success: isFormValid,
73 | message,
74 | errors
75 | };
76 | }
77 |
78 | router.post('/signup', (req, res, next) => {
79 | const validationResult = validateSignupForm(req.body);
80 | if (!validationResult.success) {
81 | return res.status(400).json({
82 | success: false,
83 | message: validationResult.message,
84 | errors: validationResult.errors
85 | });
86 | }
87 |
88 |
89 | return passport.authenticate('local-signup', (err) => {
90 | if (err) {
91 | if (err.name === 'MongoError' && err.code === 11000) {
92 | // the 11000 Mongo code is for a duplication email error
93 | // the 409 HTTP status code is for conflict error
94 | return res.status(409).json({
95 | success: false,
96 | message: 'Check the form for errors.',
97 | errors: {
98 | email: 'This email is already taken.'
99 | }
100 | });
101 | }
102 |
103 | return res.status(400).json({
104 | success: false,
105 | message: 'Could not process the form.'
106 | });
107 | }
108 |
109 | return res.status(200).json({
110 | success: true,
111 | message: 'You have successfully signed up! Now you should be able to log in.'
112 | });
113 | })(req, res, next);
114 | });
115 |
116 | router.post('/login', (req, res, next) => {
117 | const validationResult = validateLoginForm(req.body);
118 | if (!validationResult.success) {
119 | return res.status(400).json({
120 | success: false,
121 | message: validationResult.message,
122 | errors: validationResult.errors
123 | });
124 | }
125 |
126 |
127 | return passport.authenticate('local-login', (err, token, userData) => {
128 | if (err) {
129 | if (err.name === 'IncorrectCredentialsError') {
130 | return res.status(400).json({
131 | success: false,
132 | message: err.message
133 | });
134 | }
135 |
136 | return res.status(400).json({
137 | success: false,
138 | message: 'Could not process the form.'
139 | });
140 | }
141 |
142 |
143 | return res.json({
144 | success: true,
145 | message: 'You have successfully logged in!',
146 | token,
147 | user: userData
148 | });
149 | })(req, res, next);
150 | });
151 |
152 |
153 | module.exports = router;
154 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/static/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-size: 16px;
4 | }
5 |
6 | a {
7 | color: #00bcd4;
8 | font-weight: bold;
9 | text-decoration: none;
10 | transition: color 0.4s;
11 | }
12 |
13 | a:hover {
14 | color: #1976d2;
15 | }
16 |
17 | .text-center {
18 | text-align: center;
19 | }
20 |
21 | .top-bar {
22 | padding: 10px 15px;
23 | margin-bottom: 50px;
24 | }
25 | .top-bar::after {
26 | content: '';
27 | display: block;
28 | clear: both;
29 | }
30 |
31 | .top-bar-left {
32 | float: left;
33 | font-size: 1.5em;
34 | }
35 |
36 | .top-bar-right {
37 | float: right;
38 | }
39 |
40 | .top-bar a,
41 | .nav a {
42 | margin: 0 8px;
43 | }
44 |
45 | .container {
46 | margin: 0 auto;
47 | text-align: center;
48 | width: 700px;
49 | }
50 |
51 | .card-heading {
52 | padding: 16px;
53 | }
54 |
55 | .field-line, .button-line {
56 | padding: 16px;
57 | }
58 |
59 | .error-message {
60 | padding: 0 16px;
61 | color: tomato;
62 | }
63 |
64 | .success-message {
65 | padding: 0 16px;
66 | color: green;
67 | }
68 |
--------------------------------------------------------------------------------
/part-2-json-web-token/server/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React App
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/part-2-json-web-token/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 |
4 | module.exports = {
5 | // the entry file for the bundle
6 | entry: path.join(__dirname, '/client/src/app.jsx'),
7 |
8 | // the bundle file we will get in the result
9 | output: {
10 | path: path.join(__dirname, '/client/dist/js'),
11 | filename: 'app.js',
12 | },
13 |
14 | module: {
15 |
16 | // apply loaders to files that meet given conditions
17 | loaders: [{
18 | test: /\.jsx?$/,
19 | include: path.join(__dirname, '/client/src'),
20 | loader: 'babel',
21 | query: {
22 | presets: ["react", "es2015"]
23 | }
24 | }],
25 | },
26 |
27 | // start Webpack in a watch mode, so Webpack will rebuild the bundle on changes
28 | watch: true
29 | };
30 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladimirponomarev/authentication-in-react-apps/5d32c763747c162ec4509aa81056b614ccd9bc26/screenshot.png
--------------------------------------------------------------------------------