[
  {
    "path": ".babelrc",
    "content": "{\n    \"presets\": [\n        \"@babel/preset-react\",\n        \"@babel/preset-env\"\n    ]\n}"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules\njspm_packages\ntypings\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Jason Watmore\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# react-jwt-authentication-example\n\nReact (without Redux) - JWT Authentication Tutorial & Example\n\nTo see a demo and further details go to http://jasonwatmore.com/post/2019/04/06/react-jwt-authentication-tutorial-example"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"react-jwt-authentication-example\",\n    \"version\": \"1.0.0\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/cornflourblue/react-jwt-authentication-example.git\"\n    },\n    \"license\": \"MIT\",\n    \"scripts\": {\n        \"start\": \"webpack-dev-server --open\"\n    },\n    \"dependencies\": {\n        \"formik\": \"^1.5.2\",\n        \"history\": \"^4.9.0\",\n        \"react\": \"^16.8.6\",\n        \"react-dom\": \"^16.8.6\",\n        \"react-router-dom\": \"^5.0.0\",\n        \"rxjs\": \"^6.3.3\",\n        \"yup\": \"^0.27.0\"\n    },\n    \"devDependencies\": {\n        \"@babel/core\": \"^7.4.3\",\n        \"@babel/preset-env\": \"^7.4.3\",\n        \"@babel/preset-react\": \"^7.0.0\",\n        \"babel-loader\": \"^8.0.5\",\n        \"html-webpack-plugin\": \"^3.2.0\",\n        \"path\": \"^0.12.7\",\n        \"webpack\": \"^4.29.6\",\n        \"webpack-cli\": \"^3.3.0\",\n        \"webpack-dev-server\": \"^3.2.1\"\n    }\n}"
  },
  {
    "path": "src/App/App.jsx",
    "content": "import React from 'react';\nimport { Router, Route, Link } from 'react-router-dom';\n\nimport { history } from '@/_helpers';\nimport { authenticationService } from '@/_services';\nimport { PrivateRoute } from '@/_components';\nimport { HomePage } from '@/HomePage';\nimport { LoginPage } from '@/LoginPage';\n\nclass App extends React.Component {\n    constructor(props) {\n        super(props);\n\n        this.state = {\n            currentUser: null\n        };\n    }\n\n    componentDidMount() {\n        authenticationService.currentUser.subscribe(x => this.setState({ currentUser: x }));\n    }\n\n    logout() {\n        authenticationService.logout();\n        history.push('/login');\n    }\n\n    render() {\n        const { currentUser } = this.state;\n        return (\n            <Router history={history}>\n                <div>\n                    {currentUser &&\n                        <nav className=\"navbar navbar-expand navbar-dark bg-dark\">\n                            <div className=\"navbar-nav\">\n                                <Link to=\"/\" className=\"nav-item nav-link\">Home</Link>\n                                <a onClick={this.logout} className=\"nav-item nav-link\">Logout</a>\n                            </div>\n                        </nav>\n                    }\n                    <div className=\"jumbotron\">\n                        <div className=\"container\">\n                            <div className=\"row\">\n                                <div className=\"col-md-6 offset-md-3\">\n                                    <PrivateRoute exact path=\"/\" component={HomePage} />\n                                    <Route path=\"/login\" component={LoginPage} />\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </Router>\n        );\n    }\n}\n\nexport { App }; "
  },
  {
    "path": "src/App/index.js",
    "content": "export * from './App';"
  },
  {
    "path": "src/HomePage/HomePage.jsx",
    "content": "import React from 'react';\n\nimport { userService, authenticationService } from '@/_services';\n\nclass HomePage extends React.Component {\n    constructor(props) {\n        super(props);\n\n        this.state = {\n            currentUser: authenticationService.currentUserValue,\n            users: null\n        };\n    }\n\n    componentDidMount() {\n        userService.getAll().then(users => this.setState({ users }));\n    }\n\n    render() {\n        const { currentUser, users } = this.state;\n        return (\n            <div>\n                <h1>Hi {currentUser.firstName}!</h1>\n                <p>You're logged in with React & JWT!!</p>\n                <h3>Users from secure api end point:</h3>\n                {users &&\n                    <ul>\n                        {users.map(user =>\n                            <li key={user.id}>{user.firstName} {user.lastName}</li>\n                        )}\n                    </ul>\n                }\n            </div>\n        );\n    }\n}\n\nexport { HomePage };"
  },
  {
    "path": "src/HomePage/index.js",
    "content": "export * from './HomePage';"
  },
  {
    "path": "src/LoginPage/LoginPage.jsx",
    "content": "import React from 'react';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { authenticationService } from '@/_services';\n\nclass LoginPage extends React.Component {\n    constructor(props) {\n        super(props);\n\n        // redirect to home if already logged in\n        if (authenticationService.currentUserValue) { \n            this.props.history.push('/');\n        }\n    }\n\n    render() {\n        return (\n            <div>\n                <div className=\"alert alert-info\">\n                    Username: test<br />\n                    Password: test\n                </div>\n                <h2>Login</h2>\n                <Formik\n                    initialValues={{\n                        username: '',\n                        password: ''\n                    }}\n                    validationSchema={Yup.object().shape({\n                        username: Yup.string().required('Username is required'),\n                        password: Yup.string().required('Password is required')\n                    })}\n                    onSubmit={({ username, password }, { setStatus, setSubmitting }) => {\n                        setStatus();\n                        authenticationService.login(username, password)\n                            .then(\n                                user => {\n                                    const { from } = this.props.location.state || { from: { pathname: \"/\" } };\n                                    this.props.history.push(from);\n                                },\n                                error => {\n                                    setSubmitting(false);\n                                    setStatus(error);\n                                }\n                            );\n                    }}\n                    render={({ errors, status, touched, isSubmitting }) => (\n                        <Form>\n                            <div className=\"form-group\">\n                                <label htmlFor=\"username\">Username</label>\n                                <Field name=\"username\" type=\"text\" className={'form-control' + (errors.username && touched.username ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"username\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group\">\n                                <label htmlFor=\"password\">Password</label>\n                                <Field name=\"password\" type=\"password\" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"password\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group\">\n                                <button type=\"submit\" className=\"btn btn-primary\" disabled={isSubmitting}>Login</button>\n                                {isSubmitting &&\n                                    <img src=\"data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==\" />\n                                }\n                            </div>\n                            {status &&\n                                <div className={'alert alert-danger'}>{status}</div>\n                            }\n                        </Form>\n                    )}\n                />\n            </div>\n        )\n    }\n}\n\nexport { LoginPage }; "
  },
  {
    "path": "src/LoginPage/index.js",
    "content": "export * from './LoginPage';"
  },
  {
    "path": "src/_components/PrivateRoute.jsx",
    "content": "import React from 'react';\r\nimport { Route, Redirect } from 'react-router-dom';\r\n\r\nimport { authenticationService } from '@/_services';\r\n\r\nexport const PrivateRoute = ({ component: Component, ...rest }) => (\r\n    <Route {...rest} render={props => {\r\n        const currentUser = authenticationService.currentUserValue;\r\n        if (!currentUser) {\r\n            // not logged in so redirect to login page with the return url\r\n            return <Redirect to={{ pathname: '/login', state: { from: props.location } }} />\r\n        }\r\n\r\n        // authorised so return component\r\n        return <Component {...props} />\r\n    }} />\r\n)"
  },
  {
    "path": "src/_components/index.js",
    "content": "export * from './PrivateRoute';\n"
  },
  {
    "path": "src/_helpers/auth-header.js",
    "content": "import { authenticationService } from '@/_services';\n\nexport function authHeader() {\n    // return authorization header with jwt token\n    const currentUser = authenticationService.currentUserValue;\n    if (currentUser && currentUser.token) {\n        return { Authorization: `Bearer ${currentUser.token}` };\n    } else {\n        return {};\n    }\n}"
  },
  {
    "path": "src/_helpers/fake-backend.js",
    "content": "export function configureFakeBackend() {\n    let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];\n    let realFetch = window.fetch;\n    window.fetch = function (url, opts) {\n        const isLoggedIn = opts.headers['Authorization'] === 'Bearer fake-jwt-token';\n\n        return new Promise((resolve, reject) => {\n            // wrap in timeout to simulate server api call\n            setTimeout(() => {\n                // authenticate - public\n                if (url.endsWith('/users/authenticate') && opts.method === 'POST') {\n                    const params = JSON.parse(opts.body);\n                    const user = users.find(x => x.username === params.username && x.password === params.password);\n                    if (!user) return error('Username or password is incorrect');\n                    return ok({\n                        id: user.id,\n                        username: user.username,\n                        firstName: user.firstName,\n                        lastName: user.lastName,\n                        token: 'fake-jwt-token'\n                    });\n                }\n\n                // get users - secure\n                if (url.endsWith('/users') && opts.method === 'GET') {\n                    if (!isLoggedIn) return unauthorised();\n                    return ok(users);\n                }\n\n                // pass through any requests not handled above\n                realFetch(url, opts).then(response => resolve(response));\n\n                // private helper functions\n\n                function ok(body) {\n                    resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) })\n                }\n\n                function unauthorised() {\n                    resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorised' })) })\n                }\n\n                function error(message) {\n                    resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) })\n                }\n            }, 500);\n        });\n    }\n}"
  },
  {
    "path": "src/_helpers/handle-response.js",
    "content": "import { authenticationService } from '@/_services';\n\nexport function handleResponse(response) {\n    return response.text().then(text => {\n        const data = text && JSON.parse(text);\n        if (!response.ok) {\n            if ([401, 403].indexOf(response.status) !== -1) {\n                // auto logout if 401 Unauthorized or 403 Forbidden response returned from api\n                authenticationService.logout();\n                location.reload(true);\n            }\n\n            const error = (data && data.message) || response.statusText;\n            return Promise.reject(error);\n        }\n\n        return data;\n    });\n}"
  },
  {
    "path": "src/_helpers/history.js",
    "content": "import { createBrowserHistory } from 'history';\n\nexport const history = createBrowserHistory();"
  },
  {
    "path": "src/_helpers/index.js",
    "content": "export * from './auth-header';\nexport * from './fake-backend';\nexport * from './handle-response';\nexport * from './history';"
  },
  {
    "path": "src/_services/authentication.service.js",
    "content": "import { BehaviorSubject } from 'rxjs';\n\nimport config from 'config';\nimport { handleResponse } from '@/_helpers';\n\nconst currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser')));\n\nexport const authenticationService = {\n    login,\n    logout,\n    currentUser: currentUserSubject.asObservable(),\n    get currentUserValue () { return currentUserSubject.value }\n};\n\nfunction login(username, password) {\n    const requestOptions = {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ username, password })\n    };\n\n    return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)\n        .then(handleResponse)\n        .then(user => {\n            // store user details and jwt token in local storage to keep user logged in between page refreshes\n            localStorage.setItem('currentUser', JSON.stringify(user));\n            currentUserSubject.next(user);\n\n            return user;\n        });\n}\n\nfunction logout() {\n    // remove user from local storage to log user out\n    localStorage.removeItem('currentUser');\n    currentUserSubject.next(null);\n}\n"
  },
  {
    "path": "src/_services/index.js",
    "content": "export * from './authentication.service';\nexport * from './user.service';\n"
  },
  {
    "path": "src/_services/user.service.js",
    "content": "import config from 'config';\nimport { authHeader, handleResponse } from '@/_helpers';\n\nexport const userService = {\n    getAll\n};\n\nfunction getAll() {\n    const requestOptions = { method: 'GET', headers: authHeader() };\n    return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);\n}"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>React - JWT Authentication Tutorial & Example</title>\n\n    <!-- bootstrap css -->\n    <link href=\"//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css\" rel=\"stylesheet\" />\n\n    <style>\n        a { cursor: pointer; }\n    </style>\n</head>\n<body>\n    <div id=\"app\"></div>\n\n    <!-- credits -->\n    <div class=\"text-center\">\n        <p>\n            <a href=\"http://jasonwatmore.com/post/2019/04/06/react-jwt-authentication-tutorial-example\" target=\"_top\">React - JWT Authentication Tutorial with Example</a>\n        </p>       \n        <p>\n            <a href=\"http://jasonwatmore.com\" target=\"_top\">JasonWatmore.com</a>\n        </p>\n    </div>\n</body>\n</html>"
  },
  {
    "path": "src/index.jsx",
    "content": "import React from 'react';\nimport { render } from 'react-dom';\n\nimport { App } from './App';\n\n// setup fake backend\nimport { configureFakeBackend } from './_helpers';\nconfigureFakeBackend();\n\nrender(\n    <App />,\n    document.getElementById('app')\n);"
  },
  {
    "path": "webpack.config.js",
    "content": "var HtmlWebpackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = {\n    mode: 'development',\n    resolve: {\n        extensions: ['.js', '.jsx']\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.jsx?$/,\n                loader: 'babel-loader'\n            }\n        ]\n    },\n    resolve: {\n        extensions: ['.js', '.jsx'],\n        alias: {\n            '@': path.resolve(__dirname, 'src/'),\n        }\n    },\n    plugins: [new HtmlWebpackPlugin({\n        template: './src/index.html'\n    })],\n    devServer: {\n        historyApiFallback: true\n    },\n    externals: {\n        // global app config object\n        config: JSON.stringify({\n            apiUrl: 'http://localhost:4000'\n        })\n    }\n}"
  }
]