[
  {
    "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\n\n# dist folder\ndist"
  },
  {
    "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-signup-verification-boilerplate\n\nReact - Email Sign Up with Verification, Authentication & Forgot Password\n\nFor documentation and a live demo see https://jasonwatmore.com/post/2020/04/22/react-email-sign-up-with-verification-authentication-forgot-password"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"react-signup-verification-boilerplate\",\n    \"version\": \"1.0.0\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/cornflourblue/react-signup-verification-boilerplate.git\"\n    },\n    \"license\": \"MIT\",\n    \"scripts\": {\n        \"build\": \"webpack --mode production\",\n        \"start\": \"webpack-dev-server --open\"\n    },\n    \"dependencies\": {\n        \"formik\": \"^2.1.4\",\n        \"history\": \"^4.10.1\",\n        \"prop-types\": \"^15.7.2\",\n        \"query-string\": \"^6.11.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.28.1\"\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        \"css-loader\": \"^3.4.2\",\n        \"html-webpack-plugin\": \"^3.2.0\",\n        \"less\": \"^3.11.0\",\n        \"less-loader\": \"^5.0.0\",\n        \"path\": \"^0.12.7\",\n        \"style-loader\": \"^1.1.3\",\n        \"webpack\": \"^4.29.6\",\n        \"webpack-cli\": \"^3.3.0\",\n        \"webpack-dev-server\": \"^3.2.1\"\n    }\n}\n"
  },
  {
    "path": "src/_components/Alert.jsx",
    "content": "import React, { useState, useEffect } from 'react';\r\nimport PropTypes from 'prop-types';\r\n\r\nimport { alertService, AlertType } from '@/_services';\r\nimport { history } from '@/_helpers';\r\n\r\nconst propTypes = {\r\n    id: PropTypes.string,\r\n    fade: PropTypes.bool\r\n};\r\n\r\nconst defaultProps = {\r\n    id: 'default-alert',\r\n    fade: true\r\n};\r\n\r\nfunction Alert({ id, fade }) {\r\n    const [alerts, setAlerts] = useState([]);\r\n\r\n    useEffect(() => {\r\n        // subscribe to new alert notifications\r\n        const subscription = alertService.onAlert(id)\r\n            .subscribe(alert => {\r\n                // clear alerts when an empty alert is received\r\n                if (!alert.message) {\r\n                    setAlerts(alerts => {\r\n                        // filter out alerts without 'keepAfterRouteChange' flag\r\n                        const filteredAlerts = alerts.filter(x => x.keepAfterRouteChange);\r\n\r\n                        // remove 'keepAfterRouteChange' flag on the rest\r\n                        filteredAlerts.forEach(x => delete x.keepAfterRouteChange);\r\n                        return filteredAlerts;\r\n                    });\r\n                } else {\r\n                    // add alert to array\r\n                    setAlerts(alerts => ([...alerts, alert]));\r\n\r\n                    // auto close alert if required\r\n                    if (alert.autoClose) {\r\n                        setTimeout(() => removeAlert(alert), 3000);\r\n                    }\r\n                }\r\n            });\r\n\r\n        // clear alerts on location change\r\n        const historyUnlisten = history.listen(({ pathname }) => {\r\n            // don't clear if pathname has trailing slash because this will be auto redirected again\r\n            if (pathname.endsWith('/')) return;\r\n\r\n            alertService.clear(id);\r\n        });\r\n\r\n        // clean up function that runs when the component unmounts\r\n        return () => {\r\n            // unsubscribe & unlisten to avoid memory leaks\r\n            subscription.unsubscribe();\r\n            historyUnlisten();\r\n        };\r\n    }, []);\r\n\r\n    function removeAlert(alert) {\r\n        if (fade) {\r\n            // fade out alert\r\n            const alertWithFade = { ...alert, fade: true };\r\n            setAlerts(alerts => alerts.map(x => x === alert ? alertWithFade : x));\r\n\r\n            // remove alert after faded out\r\n            setTimeout(() => {\r\n                setAlerts(alerts => alerts.filter(x => x !== alertWithFade));\r\n            }, 250);\r\n        } else {\r\n            // remove alert\r\n            setAlerts(alerts => alerts.filter(x => x !== alert));\r\n        }\r\n    }\r\n\r\n    function cssClasses(alert) {\r\n        if (!alert) return;\r\n\r\n        const classes = ['alert', 'alert-dismissable'];\r\n                \r\n        const alertTypeClass = {\r\n            [AlertType.Success]: 'alert alert-success',\r\n            [AlertType.Error]: 'alert alert-danger',\r\n            [AlertType.Info]: 'alert alert-info',\r\n            [AlertType.Warning]: 'alert alert-warning'\r\n        }\r\n\r\n        classes.push(alertTypeClass[alert.type]);\r\n\r\n        if (alert.fade) {\r\n            classes.push('fade');\r\n        }\r\n\r\n        return classes.join(' ');\r\n    }\r\n\r\n    if (!alerts.length) return null;\r\n\r\n    return (\r\n        <div className=\"container\">\r\n            <div className=\"m-3\">\r\n                {alerts.map((alert, index) =>\r\n                    <div key={index} className={cssClasses(alert)}>\r\n                        <a className=\"close\" onClick={() => removeAlert(alert)}>&times;</a>\r\n                        <span dangerouslySetInnerHTML={{__html: alert.message}}></span>\r\n                    </div>\r\n                )}\r\n            </div>\r\n        </div>\r\n    );\r\n}\r\n\r\nAlert.propTypes = propTypes;\r\nAlert.defaultProps = defaultProps;\r\nexport { Alert };"
  },
  {
    "path": "src/_components/Nav.jsx",
    "content": "import React, { useState, useEffect } from 'react';\r\nimport { NavLink, Route } from 'react-router-dom';\r\n\r\nimport { Role } from '@/_helpers';\r\nimport { accountService } from '@/_services';\r\n\r\nfunction Nav() {\r\n    const [user, setUser] = useState({});\r\n\r\n    useEffect(() => {\r\n        const subscription = accountService.user.subscribe(x => setUser(x));\r\n        return subscription.unsubscribe;\r\n    }, []);\r\n\r\n    // only show nav when logged in\r\n    if (!user) return null;\r\n\r\n    return (\r\n        <div>\r\n            <nav className=\"navbar navbar-expand navbar-dark bg-dark\">\r\n                <div className=\"navbar-nav\">\r\n                    <NavLink exact to=\"/\" className=\"nav-item nav-link\">Home</NavLink>\r\n                    <NavLink to=\"/profile\" className=\"nav-item nav-link\">Profile</NavLink>\r\n                    {user.role === Role.Admin &&\r\n                        <NavLink to=\"/admin\" className=\"nav-item nav-link\">Admin</NavLink>\r\n                    }\r\n                    <a onClick={accountService.logout} className=\"nav-item nav-link\">Logout</a>\r\n                </div>\r\n            </nav>\r\n            <Route path=\"/admin\" component={AdminNav} />\r\n        </div>\r\n    );\r\n}\r\n\r\nfunction AdminNav({ match }) {\r\n    const { path } = match;\r\n\r\n    return (\r\n        <nav className=\"admin-nav navbar navbar-expand navbar-light\">\r\n            <div className=\"navbar-nav\">\r\n                <NavLink to={`${path}/users`} className=\"nav-item nav-link\">Users</NavLink>\r\n            </div>\r\n        </nav>\r\n    );\r\n}\r\n\r\nexport { Nav }; "
  },
  {
    "path": "src/_components/PrivateRoute.jsx",
    "content": "import React from 'react';\r\nimport { Route, Redirect } from 'react-router-dom';\r\n\r\nimport { accountService } from '@/_services';\r\n\r\nfunction PrivateRoute({ component: Component, roles, ...rest }) {\r\n    return (\r\n        <Route {...rest} render={props => {\r\n            const user = accountService.userValue;\r\n            if (!user) {\r\n                // not logged in so redirect to login page with the return url\r\n                return <Redirect to={{ pathname: '/account/login', state: { from: props.location } }} />\r\n            }\r\n\r\n            // check if route is restricted by role\r\n            if (roles && roles.indexOf(user.role) === -1) {\r\n                // role not authorized so redirect to home page\r\n                return <Redirect to={{ pathname: '/'}} />\r\n            }\r\n\r\n            // authorized so return component\r\n            return <Component {...props} />\r\n        }} />\r\n    );\r\n}\r\n\r\nexport { PrivateRoute };"
  },
  {
    "path": "src/_components/index.js",
    "content": "export * from './Alert';\nexport * from './Nav';\nexport * from './PrivateRoute';\n"
  },
  {
    "path": "src/_helpers/fake-backend.js",
    "content": "import { Role } from './'\nimport { alertService } from '@/_services';\n\n// array in local storage for registered users\nconst usersKey = 'react-signup-verification-boilerplate-users';\nlet users = JSON.parse(localStorage.getItem(usersKey)) || [];\n\nexport function configureFakeBackend() {\n    let realFetch = window.fetch;\n    window.fetch = function (url, opts) {\n        return new Promise((resolve, reject) => {\n            // wrap in timeout to simulate server api call\n            setTimeout(handleRoute, 500);\n\n            function handleRoute() {\n                const { method } = opts;\n                switch (true) {\n                    case url.endsWith('/accounts/authenticate') && method === 'POST':\n                        return authenticate();\n                    case url.endsWith('/accounts/refresh-token') && method === 'POST':\n                        return refreshToken();\n                    case url.endsWith('/accounts/revoke-token') && method === 'POST':\n                        return revokeToken();\n                    case url.endsWith('/accounts/register') && method === 'POST':\n                        return register();\n                    case url.endsWith('/accounts/verify-email') && method === 'POST':\n                        return verifyEmail();\n                    case url.endsWith('/accounts/forgot-password') && method === 'POST':\n                        return forgotPassword();\n                    case url.endsWith('/accounts/validate-reset-token') && method === 'POST':\n                        return validateResetToken();\n                    case url.endsWith('/accounts/reset-password') && method === 'POST':\n                        return resetPassword();\n                    case url.endsWith('/accounts') && method === 'GET':\n                        return getUsers();\n                    case url.match(/\\/accounts\\/\\d+$/) && method === 'GET':\n                        return getUserById();\n                    case url.endsWith('/accounts') && method === 'POST':\n                        return createUser();\n                    case url.match(/\\/accounts\\/\\d+$/) && method === 'PUT':\n                        return updateUser();\n                    case url.match(/\\/accounts\\/\\d+$/) && method === 'DELETE':\n                        return deleteUser();\n                    default:\n                        // pass through any requests not handled above\n                        return realFetch(url, opts)\n                            .then(response => resolve(response))\n                            .catch(error => reject(error));\n                }\n            }\n\n            // route functions\n\n            function authenticate() {\n                const { email, password } = body();\n                const user = users.find(x => x.email === email && x.password === password && x.isVerified);\n\n                if (!user) return error('Email or password is incorrect');\n\n                // add refresh token to user\n                user.refreshTokens.push(generateRefreshToken());\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok({\n                    id: user.id,\n                    email: user.email,\n                    title: user.title,\n                    firstName: user.firstName,\n                    lastName: user.lastName,\n                    role: user.role,\n                    jwtToken: generateJwtToken(user)\n                });\n            }\n\n            function refreshToken() {\n                const refreshToken = getRefreshToken();\n                \n                if (!refreshToken) return unauthorized();\n\n                const user = users.find(x => x.refreshTokens.includes(refreshToken));\n                \n                if (!user) return unauthorized();\n\n                // replace old refresh token with a new one and save\n                user.refreshTokens = user.refreshTokens.filter(x => x !== refreshToken);\n                user.refreshTokens.push(generateRefreshToken());\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok({\n                    id: user.id,\n                    email: user.email,\n                    title: user.title,\n                    firstName: user.firstName,\n                    lastName: user.lastName,\n                    role: user.role,\n                    jwtToken: generateJwtToken(user)\n                })\n            }\n\n            function revokeToken() {\n                if (!isAuthenticated()) return unauthorized();\n                \n                const refreshToken = getRefreshToken();\n                const user = users.find(x => x.refreshTokens.includes(refreshToken));\n                \n                // revoke token and save\n                user.refreshTokens = user.refreshTokens.filter(x => x !== refreshToken);\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok();\n            }\n\n            function register() {\n                const user = body();\n    \n                if (users.find(x => x.email === user.email)) {\n                    // display email already registered \"email\" in alert\n                    setTimeout(() => {\n                        alertService.info(`\n                            <h4>Email Already Registered</h4>\n                            <p>Your email ${user.email} is already registered.</p>\n                            <p>If you don't know your password please visit the <a href=\"${location.origin}/account/forgot-password\">forgot password</a> page.</p>\n                            <div><strong>NOTE:</strong> The fake backend displayed this \"email\" so you can test without an api. A real backend would send a real email.</div>\n                        `, { autoClose: false });\n                    }, 1000);\n\n                    // always return ok() response to prevent email enumeration\n                    return ok();\n                }\n    \n                // assign user id and a few other properties then save\n                user.id = newUserId();\n                if (user.id === 1) {\n                    // first registered user is an admin\n                    user.role = Role.Admin;\n                } else {\n                    user.role = Role.User;\n                }\n                user.dateCreated = new Date().toISOString();\n                user.verificationToken = new Date().getTime().toString();\n                user.isVerified = false;\n                user.refreshTokens = [];\n                delete user.confirmPassword;\n                users.push(user);\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                // display verification email in alert\n                setTimeout(() => {\n                    const verifyUrl = `${location.origin}/account/verify-email?token=${user.verificationToken}`;\n                    alertService.info(`\n                        <h4>Verification Email</h4>\n                        <p>Thanks for registering!</p>\n                        <p>Please click the below link to verify your email address:</p>\n                        <p><a href=\"${verifyUrl}\">${verifyUrl}</a></p>\n                        <div><strong>NOTE:</strong> The fake backend displayed this \"email\" so you can test without an api. A real backend would send a real email.</div>\n                    `, { autoClose: false });\n                }, 1000);\n\n                return ok();\n            }\n    \n            function verifyEmail() {\n                const { token } = body();\n                const user = users.find(x => !!x.verificationToken && x.verificationToken === token);\n                \n                if (!user) return error('Verification failed');\n                \n                // set is verified flag to true if token is valid\n                user.isVerified = true;\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok();\n            }\n\n            function forgotPassword() {\n                const { email } = body();\n                const user = users.find(x => x.email === email);\n                \n                // always return ok() response to prevent email enumeration\n                if (!user) return ok();\n                \n                // create reset token that expires after 24 hours\n                user.resetToken = new Date().getTime().toString();\n                user.resetTokenExpires = new Date(Date.now() + 24*60*60*1000).toISOString();\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                // display password reset email in alert\n                setTimeout(() => {\n                    const resetUrl = `${location.origin}/account/reset-password?token=${user.resetToken}`;\n                    alertService.info(`\n                        <h4>Reset Password Email</h4>\n                        <p>Please click the below link to reset your password, the link will be valid for 1 day:</p>\n                        <p><a href=\"${resetUrl}\">${resetUrl}</a></p>\n                        <div><strong>NOTE:</strong> The fake backend displayed this \"email\" so you can test without an api. A real backend would send a real email.</div>\n                    `, { autoClose: false });\n                }, 1000);\n\n                return ok();\n            }\n\n            function validateResetToken() {\n                const { token } = body();\n                const user = users.find(x =>\n                    !!x.resetToken && x.resetToken === token &&\n                    new Date() < new Date(x.resetTokenExpires)\n                );\n                \n                if (!user) return error('Invalid token');\n                \n                return ok();\n            }\n\n            function resetPassword() {\n                const { token, password } = body();\n                const user = users.find(x =>\n                    !!x.resetToken && x.resetToken === token &&\n                    new Date() < new Date(x.resetTokenExpires)\n                );\n                \n                if (!user) return error('Invalid token');\n                \n                // update password and remove reset token\n                user.password = password;\n                user.isVerified = true;\n                delete user.resetToken;\n                delete user.resetTokenExpires;\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok();\n            }\n\n            function getUsers() {\n                if (!isAuthorized(Role.Admin)) return unauthorized();\n\n                return ok(users);\n            }\n\n            function getUserById() {\n                if (!isAuthenticated()) return unauthorized();\n    \n                let user = users.find(x => x.id === idFromUrl());\n\n                // users can get own profile and admins can get all profiles\n                if (user.id !== currentUser().id && !isAuthorized(Role.Admin)) {\n                    return unauthorized();\n                }\n\n                return ok(user);\n            }\n    \n            function createUser() {\n                if (!isAuthorized(Role.Admin)) return unauthorized();\n    \n                const user = body();\n                if (users.find(x => x.email === user.email)) {\n                    return error(`Email ${user.email} is already registered`);\n                }\n\n                // assign user id and a few other properties then save\n                user.id = newUserId();\n                user.dateCreated = new Date().toISOString();\n                user.isVerified = true;\n                user.refreshTokens = [];\n                delete user.confirmPassword;\n                users.push(user);\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok();\n            }\n    \n            function updateUser() {\n                if (!isAuthenticated()) return unauthorized();\n    \n                let params = body();\n                let user = users.find(x => x.id === idFromUrl());\n\n                // users can update own profile and admins can update all profiles\n                if (user.id !== currentUser().id && !isAuthorized(Role.Admin)) {\n                    return unauthorized();\n                }\n\n                // only update password if included\n                if (!params.password) {\n                    delete params.password;\n                }\n                // don't save confirm password\n                delete params.confirmPassword;\n\n                // update and save user\n                Object.assign(user, params);\n                localStorage.setItem(usersKey, JSON.stringify(users));\n\n                return ok({\n                    id: user.id,\n                    email: user.email,\n                    title: user.title,\n                    firstName: user.firstName,\n                    lastName: user.lastName,\n                    role: user.role\n                });\n            }\n    \n            function deleteUser() {\n                if (!isAuthenticated()) return unauthorized();\n    \n                let user = users.find(x => x.id === idFromUrl());\n\n                // users can delete own account and admins can delete any account\n                if (user.id !== currentUser().id && !isAuthorized(Role.Admin)) {\n                    return unauthorized();\n                }\n\n                // delete user then save\n                users = users.filter(x => x.id !== idFromUrl());\n                localStorage.setItem(usersKey, JSON.stringify(users));\n                return ok();\n            }\n    \n            // helper functions\n\n            function ok(body) {\n                resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) });\n            }\n\n            function unauthorized() {\n                resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorized' })) });\n            }\n\n            function error(message) {\n                resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) });\n            }\n\n            function isAuthenticated() {\n                return !!currentUser();\n            }\n    \n            function isAuthorized(role) {\n                const user = currentUser();\n                if (!user) return false;\n                return user.role === role;\n            }\n    \n            function idFromUrl() {\n                const urlParts = url.split('/');\n                return parseInt(urlParts[urlParts.length - 1]);\n            }\n\n            function body() {\n                return opts.body && JSON.parse(opts.body);    \n            }\n\n            function newUserId() {\n                return users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;\n            }\n\n            function generateJwtToken(user) {\n                // create token that expires in 15 minutes\n                const tokenPayload = { \n                    exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000),\n                    id: user.id\n                }\n                return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;\n            }\n\n            function currentUser() {\n                // check if jwt token is in auth header\n                const authHeader = opts.headers['Authorization'] || '';\n                if (!authHeader.startsWith('Bearer fake-jwt-token')) return;\n\n                // check if token is expired\n                const jwtToken = JSON.parse(atob(authHeader.split('.')[1]));\n                const tokenExpired = Date.now() > (jwtToken.exp * 1000);\n                if (tokenExpired) return;\n\n                const user = users.find(x => x.id === jwtToken.id);\n                return user;\n            }\n\n            function generateRefreshToken() {\n                const token = new Date().getTime().toString();\n\n                // add token cookie that expires in 7 days\n                const expires = new Date(Date.now() + 7*24*60*60*1000).toUTCString();\n                document.cookie = `fakeRefreshToken=${token}; expires=${expires}; path=/`;\n\n                return token;\n            }\n\n            function getRefreshToken() {\n                // get refresh token from cookie\n                return (document.cookie.split(';').find(x => x.includes('fakeRefreshToken')) || '=').split('=')[1];\n            }\n        });\n    }\n}"
  },
  {
    "path": "src/_helpers/fetch-wrapper.js",
    "content": "import config from 'config';\nimport { accountService } from '@/_services';\n\nexport const fetchWrapper = {\n    get,\n    post,\n    put,\n    delete: _delete\n}\n\nfunction get(url) {\n    const requestOptions = {\n        method: 'GET',\n        headers: authHeader(url)\n    };\n    return fetch(url, requestOptions).then(handleResponse);\n}\n\nfunction post(url, body) {\n    const requestOptions = {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json', ...authHeader(url) },\n        credentials: 'include',\n        body: JSON.stringify(body)\n    };\n    return fetch(url, requestOptions).then(handleResponse);\n}\n\nfunction put(url, body) {\n    const requestOptions = {\n        method: 'PUT',\n        headers: { 'Content-Type': 'application/json', ...authHeader(url) },\n        body: JSON.stringify(body)\n    };\n    return fetch(url, requestOptions).then(handleResponse);    \n}\n\n// prefixed with underscored because delete is a reserved word in javascript\nfunction _delete(url) {\n    const requestOptions = {\n        method: 'DELETE',\n        headers: authHeader(url)\n    };\n    return fetch(url, requestOptions).then(handleResponse);\n}\n\n// helper functions\n\nfunction authHeader(url) {\n    // return auth header with jwt if user is logged in and request is to the api url\n    const user = accountService.userValue;\n    const isLoggedIn = user && user.jwtToken;\n    const isApiUrl = url.startsWith(config.apiUrl);\n    if (isLoggedIn && isApiUrl) {\n        return { Authorization: `Bearer ${user.jwtToken}` };\n    } else {\n        return {};\n    }\n}\n\nfunction handleResponse(response) {\n    return response.text().then(text => {\n        const data = text && JSON.parse(text);\n        \n        if (!response.ok) {\n            if ([401, 403].includes(response.status) && accountService.userValue) {\n                // auto logout if 401 Unauthorized or 403 Forbidden response returned from api\n                accountService.logout();\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 './fake-backend';\nexport * from './fetch-wrapper';\nexport * from './history';\nexport * from './role';\n"
  },
  {
    "path": "src/_helpers/role.js",
    "content": "export const Role = {\n    Admin: 'Admin',\n    User: 'User'    \n}"
  },
  {
    "path": "src/_services/account.service.js",
    "content": "import { BehaviorSubject } from 'rxjs';\n\nimport config from 'config';\nimport { fetchWrapper, history } from '@/_helpers';\n\nconst userSubject = new BehaviorSubject(null);\nconst baseUrl = `${config.apiUrl}/accounts`;\n\nexport const accountService = {\n    login,\n    logout,\n    refreshToken,\n    register,\n    verifyEmail,\n    forgotPassword,\n    validateResetToken,\n    resetPassword,\n    getAll,\n    getById,\n    create,\n    update,\n    delete: _delete,\n    user: userSubject.asObservable(),\n    get userValue () { return userSubject.value }\n};\n\nfunction login(email, password) {\n    return fetchWrapper.post(`${baseUrl}/authenticate`, { email, password })\n        .then(user => {\n            // publish user to subscribers and start timer to refresh token\n            userSubject.next(user);\n            startRefreshTokenTimer();\n            return user;\n        });\n}\n\nfunction logout() {\n    // revoke token, stop refresh timer, publish null to user subscribers and redirect to login page\n    fetchWrapper.post(`${baseUrl}/revoke-token`, {});\n    stopRefreshTokenTimer();\n    userSubject.next(null);\n    history.push('/account/login');\n}\n\nfunction refreshToken() {\n    return fetchWrapper.post(`${baseUrl}/refresh-token`, {})\n        .then(user => {\n            // publish user to subscribers and start timer to refresh token\n            userSubject.next(user);\n            startRefreshTokenTimer();\n            return user;\n        });\n}\n\nfunction register(params) {\n    return fetchWrapper.post(`${baseUrl}/register`, params);\n}\n\nfunction verifyEmail(token) {\n    return fetchWrapper.post(`${baseUrl}/verify-email`, { token });\n}\n\nfunction forgotPassword(email) {\n    return fetchWrapper.post(`${baseUrl}/forgot-password`, { email });\n}\n\nfunction validateResetToken(token) {\n    return fetchWrapper.post(`${baseUrl}/validate-reset-token`, { token });\n}\n\nfunction resetPassword({ token, password, confirmPassword }) {\n    return fetchWrapper.post(`${baseUrl}/reset-password`, { token, password, confirmPassword });\n}\n\nfunction getAll() {\n    return fetchWrapper.get(baseUrl);\n}\n\nfunction getById(id) {\n    return fetchWrapper.get(`${baseUrl}/${id}`);\n}\n\nfunction create(params) {\n    return fetchWrapper.post(baseUrl, params);\n}\n\nfunction update(id, params) {\n    return fetchWrapper.put(`${baseUrl}/${id}`, params)\n        .then(user => {\n            // update stored user if the logged in user updated their own record\n            if (user.id === userSubject.value.id) {\n                // publish updated user to subscribers\n                user = { ...userSubject.value, ...user };\n                userSubject.next(user);\n            }\n            return user;\n        });\n}\n\n// prefixed with underscore because 'delete' is a reserved word in javascript\nfunction _delete(id) {\n    return fetchWrapper.delete(`${baseUrl}/${id}`)\n        .then(x => {\n            // auto logout if the logged in user deleted their own record\n            if (id === userSubject.value.id) {\n                logout();\n            }\n            return x;\n        });\n}\n\n// helper functions\n\nlet refreshTokenTimeout;\n\nfunction startRefreshTokenTimer() {\n    // parse json object from base64 encoded jwt token\n    const jwtToken = JSON.parse(atob(userSubject.value.jwtToken.split('.')[1]));\n\n    // set a timeout to refresh the token a minute before it expires\n    const expires = new Date(jwtToken.exp * 1000);\n    const timeout = expires.getTime() - Date.now() - (60 * 1000);\n    refreshTokenTimeout = setTimeout(refreshToken, timeout);\n}\n\nfunction stopRefreshTokenTimer() {\n    clearTimeout(refreshTokenTimeout);\n}\n"
  },
  {
    "path": "src/_services/alert.service.js",
    "content": "import { Subject } from 'rxjs';\nimport { filter } from 'rxjs/operators';\n\nconst alertSubject = new Subject();\nconst defaultId = 'default-alert';\n\nexport const alertService = {\n    onAlert,\n    success,\n    error,\n    info,\n    warn,\n    alert,\n    clear\n};\n\nexport const AlertType = {\n    Success: 'Success',\n    Error: 'Error',\n    Info: 'Info',\n    Warning: 'Warning'\n}\n\n// enable subscribing to alerts observable\nfunction onAlert(id = defaultId) {\n    return alertSubject.asObservable().pipe(filter(x => x && x.id === id));\n}\n\n// convenience methods\nfunction success(message, options) {\n    alert({ ...options, type: AlertType.Success, message });\n}\n\nfunction error(message, options) {\n    alert({ ...options, type: AlertType.Error, message });\n}\n\nfunction info(message, options) {\n    alert({ ...options, type: AlertType.Info, message });\n}\n\nfunction warn(message, options) {\n    alert({ ...options, type: AlertType.Warning, message });\n}\n\n// core alert method\nfunction alert(alert) {\n    alert.id = alert.id || defaultId;\n    alert.autoClose = (alert.autoClose === undefined ? true : alert.autoClose);\n    alertSubject.next(alert);\n}\n\n// clear alerts\nfunction clear(id = defaultId) {\n    alertSubject.next({ id });\n}"
  },
  {
    "path": "src/_services/index.js",
    "content": "export * from './account.service';\nexport * from './alert.service';\n"
  },
  {
    "path": "src/account/ForgotPassword.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction ForgotPassword() {\n    const initialValues = {\n        email: ''\n    };\n\n    const validationSchema = Yup.object().shape({\n        email: Yup.string()\n            .email('Email is invalid')\n            .required('Email is required')\n    });\n\n    function onSubmit({ email }, { setSubmitting }) {\n        alertService.clear();\n        accountService.forgotPassword(email)\n            .then(() => alertService.success('Please check your email for password reset instructions'))\n            .catch(error => alertService.error(error))\n            .finally(() => setSubmitting(false));\n    }\n\n    return (\n        <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>\n            {({ errors, touched, isSubmitting }) => (\n                <Form>\n                    <h3 className=\"card-header\">Forgot Password</h3>\n                    <div className=\"card-body\">\n                        <div className=\"form-group\">\n                            <label>Email</label>\n                            <Field name=\"email\" type=\"text\" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"email\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-row\">\n                            <div className=\"form-group col\">\n                                <button type=\"submit\" disabled={isSubmitting} className=\"btn btn-primary\">\n                                    {isSubmitting && <span className=\"spinner-border spinner-border-sm mr-1\"></span>}\n                                    Submit\n                                </button>\n                                <Link to=\"login\" className=\"btn btn-link\">Cancel</Link>\n                            </div>\n                        </div>\n                    </div>\n                </Form>\n            )}\n        </Formik>        \n    )\n}\n\nexport { ForgotPassword }; "
  },
  {
    "path": "src/account/Index.jsx",
    "content": "import React, { useEffect } from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { accountService } from '@/_services';\n\nimport { Login } from './Login';\nimport { Register } from './Register';\nimport { VerifyEmail } from './VerifyEmail';\nimport { ForgotPassword } from './ForgotPassword';\nimport { ResetPassword } from './ResetPassword';\n\nfunction Account({ history, match }) {\n    const { path } = match;\n\n    useEffect(() => {\n        // redirect to home if already logged in\n        if (accountService.userValue) {\n            history.push('/');\n        }\n    }, []);\n\n    return (\n        <div className=\"container\">\n            <div className=\"row\">\n                <div className=\"col-sm-8 offset-sm-2 mt-5\">\n                    <div className=\"card m-3\">\n                        <Switch>\n                            <Route path={`${path}/login`} component={Login} />\n                            <Route path={`${path}/register`} component={Register} />\n                            <Route path={`${path}/verify-email`} component={VerifyEmail} />\n                            <Route path={`${path}/forgot-password`} component={ForgotPassword} />\n                            <Route path={`${path}/reset-password`} component={ResetPassword} />\n                        </Switch>\n                    </div>\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport { Account };"
  },
  {
    "path": "src/account/Login.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction Login({ history, location }) {\n    const initialValues = {\n        email: '',\n        password: ''\n    };\n\n    const validationSchema = Yup.object().shape({\n        email: Yup.string()\n            .email('Email is invalid')\n            .required('Email is required'),\n        password: Yup.string().required('Password is required')\n    });\n\n    function onSubmit({ email, password }, { setSubmitting }) {\n        alertService.clear();\n        accountService.login(email, password)\n            .then(() => {\n                const { from } = location.state || { from: { pathname: \"/\" } };\n                history.push(from);\n            })\n            .catch(error => {\n                setSubmitting(false);\n                alertService.error(error);\n            });\n    }\n\n    return (\n        <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>\n            {({ errors, touched, isSubmitting }) => (\n                <Form>\n                    <h3 className=\"card-header\">Login</h3>\n                    <div className=\"card-body\">\n                        <div className=\"form-group\">\n                            <label>Email</label>\n                            <Field name=\"email\" type=\"text\" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"email\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-group\">\n                            <label>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-row\">\n                            <div className=\"form-group col\">\n                                <button type=\"submit\" disabled={isSubmitting} className=\"btn btn-primary\">\n                                    {isSubmitting && <span className=\"spinner-border spinner-border-sm mr-1\"></span>}\n                                    Login\n                                </button>\n                                <Link to=\"register\" className=\"btn btn-link\">Register</Link>\n                            </div>\n                            <div className=\"form-group col text-right\">\n                                <Link to=\"forgot-password\" className=\"btn btn-link pr-0\">Forgot Password?</Link>\n                            </div>\n                        </div>\n                    </div>\n                </Form>\n            )}\n        </Formik>\n    )\n}\n\nexport { Login }; "
  },
  {
    "path": "src/account/Register.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction Register({ history }) {\n    const initialValues = {\n        title: '',\n        firstName: '',\n        lastName: '',\n        email: '',\n        password: '',\n        confirmPassword: '',\n        acceptTerms: false\n    };\n\n    const validationSchema = Yup.object().shape({\n        title: Yup.string()\n            .required('Title is required'),\n        firstName: Yup.string()\n            .required('First Name is required'),\n        lastName: Yup.string()\n            .required('Last Name is required'),\n        email: Yup.string()\n            .email('Email is invalid')\n            .required('Email is required'),\n        password: Yup.string()\n            .min(6, 'Password must be at least 6 characters')\n            .required('Password is required'),\n        confirmPassword: Yup.string()\n            .oneOf([Yup.ref('password'), null], 'Passwords must match')\n            .required('Confirm Password is required'),\n        acceptTerms: Yup.bool()\n            .oneOf([true], 'Accept Terms & Conditions is required')\n    });\n\n    function onSubmit(fields, { setStatus, setSubmitting }) {\n        setStatus();\n        accountService.register(fields)\n            .then(() => {\n                alertService.success('Registration successful, please check your email for verification instructions', { keepAfterRouteChange: true });\n                history.push('login');\n            })\n            .catch(error => {\n                setSubmitting(false);\n                alertService.error(error);\n            });\n    }\n\n    return (\n        <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>\n            {({ errors, touched, isSubmitting }) => (\n                <Form>\n                    <h3 className=\"card-header\">Register</h3>\n                    <div className=\"card-body\">\n                        <div className=\"form-row\">\n                            <div className=\"form-group col\">\n                                <label>Title</label>\n                                <Field name=\"title\" as=\"select\" className={'form-control' + (errors.title && touched.title ? ' is-invalid' : '')}>\n                                    <option value=\"\"></option>\n                                    <option value=\"Mr\">Mr</option>\n                                    <option value=\"Mrs\">Mrs</option>\n                                    <option value=\"Miss\">Miss</option>\n                                    <option value=\"Ms\">Ms</option>\n                                </Field>\n                                <ErrorMessage name=\"title\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group col-5\">\n                                <label>First Name</label>\n                                <Field name=\"firstName\" type=\"text\" className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"firstName\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group col-5\">\n                                <label>Last Name</label>\n                                <Field name=\"lastName\" type=\"text\" className={'form-control' + (errors.lastName && touched.lastName ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"lastName\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                        </div>\n                        <div className=\"form-group\">\n                            <label>Email</label>\n                            <Field name=\"email\" type=\"text\" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"email\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-row\">\n                            <div className=\"form-group col\">\n                                <label>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 col\">\n                                <label>Confirm Password</label>\n                                <Field name=\"confirmPassword\" type=\"password\" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"confirmPassword\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                        </div>\n                        <div className=\"form-group form-check\">\n                            <Field type=\"checkbox\" name=\"acceptTerms\" id=\"acceptTerms\" className={'form-check-input ' + (errors.acceptTerms && touched.acceptTerms ? ' is-invalid' : '')} />\n                            <label htmlFor=\"acceptTerms\" className=\"form-check-label\">Accept Terms & Conditions</label>\n                            <ErrorMessage name=\"acceptTerms\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-group\">\n                            <button type=\"submit\" disabled={isSubmitting} className=\"btn btn-primary\">\n                                {isSubmitting && <span className=\"spinner-border spinner-border-sm mr-1\"></span>}\n                                Register\n                            </button>\n                            <Link to=\"login\" className=\"btn btn-link\">Cancel</Link>\n                        </div>\n                    </div>\n                </Form>\n            )}\n        </Formik>\n    )\n}\n\nexport { Register }; "
  },
  {
    "path": "src/account/ResetPassword.jsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport queryString from 'query-string';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction ResetPassword({ history }) {\n    const TokenStatus = {\n        Validating: 'Validating',\n        Valid: 'Valid',\n        Invalid: 'Invalid'\n    }\n    \n    const [token, setToken] = useState(null);\n    const [tokenStatus, setTokenStatus] = useState(TokenStatus.Validating);\n\n    useEffect(() => {\n        const { token } = queryString.parse(location.search);\n\n        // remove token from url to prevent http referer leakage\n        history.replace(location.pathname);\n\n        accountService.validateResetToken(token)\n            .then(() => {\n                setToken(token);\n                setTokenStatus(TokenStatus.Valid);\n            })\n            .catch(() => {\n                setTokenStatus(TokenStatus.Invalid);\n            });\n    }, []);\n\n    function getForm() {\n        const initialValues = {\n            password: '',\n            confirmPassword: ''\n        };\n\n        const validationSchema = Yup.object().shape({\n            password: Yup.string()\n                .min(6, 'Password must be at least 6 characters')\n                .required('Password is required'),\n            confirmPassword: Yup.string()\n                .oneOf([Yup.ref('password'), null], 'Passwords must match')\n                .required('Confirm Password is required'),\n        });\n\n        function onSubmit({ password, confirmPassword }, { setSubmitting }) {\n            alertService.clear();\n            accountService.resetPassword({ token, password, confirmPassword })\n                .then(() => {\n                    alertService.success('Password reset successful, you can now login', { keepAfterRouteChange: true });\n                    history.push('login');\n                })\n                .catch(error => {\n                    setSubmitting(false);\n                    alertService.error(error);\n                });\n        }\n\n        return (\n            <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>\n                {({ errors, touched, isSubmitting }) => (\n                    <Form>\n                        <div className=\"form-group\">\n                            <label>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                            <label>Confirm Password</label>\n                            <Field name=\"confirmPassword\" type=\"password\" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"confirmPassword\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-row\">\n                            <div className=\"form-group col\">\n                                <button type=\"submit\" disabled={isSubmitting} className=\"btn btn-primary\">\n                                    {isSubmitting && <span className=\"spinner-border spinner-border-sm mr-1\"></span>}\n                                    Reset Password\n                                </button>\n                                <Link to=\"login\" className=\"btn btn-link\">Cancel</Link>\n                            </div>\n                        </div>\n                    </Form>\n                )}\n            </Formik>\n        );\n    }\n\n    function getBody() {\n        switch (tokenStatus) {\n            case TokenStatus.Valid:\n                return getForm();\n            case TokenStatus.Invalid:\n                return <div>Token validation failed, if the token has expired you can get a new one at the <Link to=\"forgot-password\">forgot password</Link> page.</div>;\n            case TokenStatus.Validating:\n                return <div>Validating token...</div>;\n        }\n    }\n\n    return (\n        <div>\n            <h3 className=\"card-header\">Reset Password</h3>\n            <div className=\"card-body\">{getBody()}</div>\n        </div>\n    )\n}\n\nexport { ResetPassword }; "
  },
  {
    "path": "src/account/VerifyEmail.jsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport queryString from 'query-string';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction VerifyEmail({ history }) {\n    const EmailStatus = {\n        Verifying: 'Verifying',\n        Failed: 'Failed'\n    }\n\n    const [emailStatus, setEmailStatus] = useState(EmailStatus.Verifying);\n\n    useEffect(() => {\n        const { token } = queryString.parse(location.search);\n\n        // remove token from url to prevent http referer leakage\n        history.replace(location.pathname);\n\n        accountService.verifyEmail(token)\n            .then(() => {\n                alertService.success('Verification successful, you can now login', { keepAfterRouteChange: true });\n                history.push('login');\n            })\n            .catch(() => {\n                setEmailStatus(EmailStatus.Failed);\n            });\n    }, []);\n\n    function getBody() {\n        switch (emailStatus) {\n            case EmailStatus.Verifying:\n                return <div>Verifying...</div>;\n            case EmailStatus.Failed:\n                return <div>Verification failed, you can also verify your account using the <Link to=\"forgot-password\">forgot password</Link> page.</div>;\n        }\n    }\n\n    return (\n        <div>\n            <h3 className=\"card-header\">Verify Email</h3>\n            <div className=\"card-body\">{getBody()}</div>\n        </div>\n    )\n}\n\nexport { VerifyEmail }; "
  },
  {
    "path": "src/admin/Index.jsx",
    "content": "import React from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { Overview } from './Overview';\nimport { Users } from './users';\n\nfunction Admin({ match }) {\n    const { path } = match;\n\n    return (\n        <div className=\"p-4\">\n            <div className=\"container\">\n                <Switch>\n                    <Route exact path={path} component={Overview} />\n                    <Route path={`${path}/users`} component={Users} />\n                </Switch>\n            </div>\n        </div>\n    );\n}\n\nexport { Admin };"
  },
  {
    "path": "src/admin/Overview.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\n\nfunction Overview({ match }) {\n    const { path } = match;\n\n    return (\n        <div>\n            <h1>Admin</h1>\n            <p>This section can only be accessed by administrators.</p>\n            <p><Link to={`${path}/users`}>Manage Users</Link></p>\n        </div>\n    );\n}\n\nexport { Overview };"
  },
  {
    "path": "src/admin/users/AddEdit.jsx",
    "content": "import React, { useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction AddEdit({ history, match }) {\n    const { id } = match.params;\n    const isAddMode = !id;\n    \n    const initialValues = {\n        title: '',\n        firstName: '',\n        lastName: '',\n        email: '',\n        role: '',\n        password: '',\n        confirmPassword: ''\n    };\n\n    const validationSchema = Yup.object().shape({\n        title: Yup.string()\n            .required('Title is required'),\n        firstName: Yup.string()\n            .required('First Name is required'),\n        lastName: Yup.string()\n            .required('Last Name is required'),\n        email: Yup.string()\n            .email('Email is invalid')\n            .required('Email is required'),\n        role: Yup.string()\n            .required('Role is required'),\n        password: Yup.string()\n            .concat(isAddMode ? Yup.string().required('Password is required') : null)\n            .min(6, 'Password must be at least 6 characters'),\n        confirmPassword: Yup.string()\n            .when('password', (password, schema) => {\n                if (password) return schema.required('Confirm Password is required');\n            })\n            .oneOf([Yup.ref('password')], 'Passwords must match')\n    });\n\n    function onSubmit(fields, { setStatus, setSubmitting }) {\n        setStatus();\n        if (isAddMode) {\n            createUser(fields, setSubmitting);\n        } else {\n            updateUser(id, fields, setSubmitting);\n        }\n    }\n\n    function createUser(fields, setSubmitting) {\n        accountService.create(fields)\n            .then(() => {\n                alertService.success('User added successfully', { keepAfterRouteChange: true });\n                history.push('.');\n            })\n            .catch(error => {\n                setSubmitting(false);\n                alertService.error(error);\n            });\n    }\n\n    function updateUser(id, fields, setSubmitting) {\n        accountService.update(id, fields)\n            .then(() => {\n                alertService.success('Update successful', { keepAfterRouteChange: true });\n                history.push('..');\n            })\n            .catch(error => {\n                setSubmitting(false);\n                alertService.error(error);\n            });\n    }\n\n    return (\n        <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>\n            {({ errors, touched, isSubmitting, setFieldValue }) => {\n                useEffect(() => {\n                    if (!isAddMode) {\n                        // get user and set form fields\n                        accountService.getById(id).then(user => {\n                            const fields = ['title', 'firstName', 'lastName', 'email', 'role'];\n                            fields.forEach(field => setFieldValue(field, user[field], false));\n                        });\n                    }\n                }, []);\n\n                return (\n                    <Form>\n                        <h1>{isAddMode ? 'Add User' : 'Edit User'}</h1>\n                        <div className=\"form-row\">\n                            <div className=\"form-group col\">\n                                <label>Title</label>\n                                <Field name=\"title\" as=\"select\" className={'form-control' + (errors.title && touched.title ? ' is-invalid' : '')}>\n                                    <option value=\"\"></option>\n                                    <option value=\"Mr\">Mr</option>\n                                    <option value=\"Mrs\">Mrs</option>\n                                    <option value=\"Miss\">Miss</option>\n                                    <option value=\"Ms\">Ms</option>\n                                </Field>\n                                <ErrorMessage name=\"title\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group col-5\">\n                                <label>First Name</label>\n                                <Field name=\"firstName\" type=\"text\" className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"firstName\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group col-5\">\n                                <label>Last Name</label>\n                                <Field name=\"lastName\" type=\"text\" className={'form-control' + (errors.lastName && touched.lastName ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"lastName\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                        </div>\n                        <div className=\"form-row\">\n                            <div className=\"form-group col-7\">\n                                <label>Email</label>\n                                <Field name=\"email\" type=\"text\" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"email\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                            <div className=\"form-group col\">\n                                <label>Role</label>\n                                <Field name=\"role\" as=\"select\" className={'form-control' + (errors.role && touched.role ? ' is-invalid' : '')}>\n                                    <option value=\"\"></option>\n                                    <option value=\"User\">User</option>\n                                    <option value=\"Admin\">Admin</option>\n                                </Field>\n                                <ErrorMessage name=\"role\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                        </div>\n                        {!isAddMode &&\n                            <div>\n                                <h3 className=\"pt-3\">Change Password</h3>\n                                <p>Leave blank to keep the same password</p>\n                            </div>\n                        }\n                        <div className=\"form-row\">\n                            <div className=\"form-group col\">\n                                <label>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 col\">\n                                <label>Confirm Password</label>\n                                <Field name=\"confirmPassword\" type=\"password\" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />\n                                <ErrorMessage name=\"confirmPassword\" component=\"div\" className=\"invalid-feedback\" />\n                            </div>\n                        </div>\n                        <div className=\"form-group\">\n                            <button type=\"submit\" disabled={isSubmitting} className=\"btn btn-primary\">\n                                {isSubmitting && <span className=\"spinner-border spinner-border-sm mr-1\"></span>}\n                                Save\n                            </button>\n                            <Link to={isAddMode ? '.' : '..'} className=\"btn btn-link\">Cancel</Link>\n                        </div>\n                    </Form>\n                );\n            }}\n        </Formik>\n    );\n}\n\nexport { AddEdit };"
  },
  {
    "path": "src/admin/users/Index.jsx",
    "content": "import React from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { List } from './List';\nimport { AddEdit } from './AddEdit';\n\nfunction Users({ match }) {\n    const { path } = match;\n    \n    return (\n        <Switch>\n            <Route exact path={path} component={List} />\n            <Route path={`${path}/add`} component={AddEdit} />\n            <Route path={`${path}/edit/:id`} component={AddEdit} />\n        </Switch>\n    );\n}\n\nexport { Users };"
  },
  {
    "path": "src/admin/users/List.jsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { accountService } from '@/_services';\n\nfunction List({ match }) {\n    const { path } = match;\n    const [users, setUsers] = useState(null);\n\n    useEffect(() => {\n        accountService.getAll().then(x => setUsers(x));\n    }, []);\n\n    function deleteUser(id) {\n        setUsers(users.map(x => {\n            if (x.id === id) { x.isDeleting = true; }\n            return x;\n        }));\n        accountService.delete(id).then(() => {\n            setUsers(users => users.filter(x => x.id !== id));\n        });\n    }\n\n    return (\n        <div>\n            <h1>Users</h1>\n            <p>All users from secure (admin only) api end point:</p>\n            <Link to={`${path}/add`} className=\"btn btn-sm btn-success mb-2\">Add User</Link>\n            <table className=\"table table-striped\">\n                <thead>\n                    <tr>\n                        <th style={{ width: '30%' }}>Name</th>\n                        <th style={{ width: '30%' }}>Email</th>\n                        <th style={{ width: '30%' }}>Role</th>\n                        <th style={{ width: '10%' }}></th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {users && users.map(user =>\n                        <tr key={user.id}>\n                            <td>{user.title} {user.firstName} {user.lastName}</td>\n                            <td>{user.email}</td>\n                            <td>{user.role}</td>\n                            <td style={{ whiteSpace: 'nowrap' }}>\n                                <Link to={`${path}/edit/${user.id}`} className=\"btn btn-sm btn-primary mr-1\">Edit</Link>\n                                <button onClick={() => deleteUser(user.id)} className=\"btn btn-sm btn-danger\" style={{ width: '60px' }} disabled={user.isDeleting}>\n                                    {user.isDeleting \n                                        ? <span className=\"spinner-border spinner-border-sm\"></span>\n                                        : <span>Delete</span>\n                                    }\n                                </button>\n                            </td>\n                        </tr>\n                    )}\n                    {!users &&\n                        <tr>\n                            <td colSpan=\"4\" className=\"text-center\">\n                                <span className=\"spinner-border spinner-border-lg align-center\"></span>\n                            </td>\n                        </tr>\n                    }\n                </tbody>\n            </table>\n        </div>\n    );\n}\n\nexport { List };"
  },
  {
    "path": "src/app/Index.jsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Route, Switch, Redirect, useLocation } from 'react-router-dom';\n\nimport { Role } from '@/_helpers';\nimport { accountService } from '@/_services';\nimport { Nav, PrivateRoute, Alert } from '@/_components';\nimport { Home } from '@/home';\nimport { Profile } from '@/profile';\nimport { Admin } from '@/admin';\nimport { Account } from '@/account';\n\nfunction App() {\n    const { pathname } = useLocation();  \n    const [user, setUser] = useState({});\n\n    useEffect(() => {\n        const subscription = accountService.user.subscribe(x => setUser(x));\n        return subscription.unsubscribe;\n    }, []);\n\n    return (\n        <div className={'app-container' + (user && ' bg-light')}>\n            <Nav />\n            <Alert />\n            <Switch>\n                <Redirect from=\"/:url*(/+)\" to={pathname.slice(0, -1)} />\n                <PrivateRoute exact path=\"/\" component={Home} />\n                <PrivateRoute path=\"/profile\" component={Profile} />\n                <PrivateRoute path=\"/admin\" roles={[Role.Admin]} component={Admin} />\n                <Route path=\"/account\" component={Account} />\n                <Redirect from=\"*\" to=\"/\" />\n            </Switch>\n        </div>\n    );\n}\n\nexport { App }; "
  },
  {
    "path": "src/home/Index.jsx",
    "content": "import React from 'react';\n\nimport { accountService } from '@/_services';\n\nfunction Home() {\n    const user = accountService.userValue;\n    \n    return (\n        <div className=\"p-4\">\n            <div className=\"container\">\n                <h1>Hi {user.firstName}!</h1>\n                <p>You're logged in with React & JWT!!</p>\n            </div>\n        </div>\n    );\n}\n\nexport { Home };"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <base href=\"/\" />\n    <title>React - Email Sign Up with Verification, Authentication & Forgot Password</title>\n\n    <!-- bootstrap css -->\n    <link href=\"//netdna.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\" rel=\"stylesheet\" />\n</head>\n<body>\n    <div id=\"app\"></div>\n\n    <!-- credits -->\n    <div class=\"text-center mt-4\">\n        <p>\n            <a href=\"https://jasonwatmore.com/post/2020/04/22/react-email-sign-up-with-verification-authentication-forgot-password\" target=\"_top\">React - Email Sign Up with Verification, Authentication & Forgot Password</a>\n        </p>       \n        <p>\n            <a href=\"https://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 { Router } from 'react-router-dom';\nimport { render } from 'react-dom';\n\nimport { history } from './_helpers';\nimport { accountService } from './_services';\nimport { App } from './app';\n\nimport './styles.less';\n\n// setup fake backend\nimport { configureFakeBackend } from './_helpers';\nconfigureFakeBackend();\n\n// attempt silent token refresh before startup\naccountService.refreshToken().finally(startApp);\n\nfunction startApp() { \n    render(\n        <Router history={history}>\n            <App />\n        </Router>,\n        document.getElementById('app')\n    );\n}"
  },
  {
    "path": "src/profile/Details.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { accountService } from '@/_services';\n\nfunction Details({ match }) {\n    const { path } = match;\n    const user = accountService.userValue;\n\n    return (\n        <div>\n            <h1>My Profile</h1>\n            <p>\n                <strong>Name: </strong> {user.title} {user.firstName} {user.lastName}<br />\n                <strong>Email: </strong> {user.email}\n            </p>\n            <p><Link to={`${path}/update`}>Update Profile</Link></p>\n        </div>\n    );\n}\n\nexport { Details };"
  },
  {
    "path": "src/profile/Index.jsx",
    "content": "import React from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { Details } from './Details';\nimport { Update } from './Update';\n\nfunction Profile({ match }) {\n    const { path } = match;\n    \n    return (\n        <div className=\"p-4\">\n            <div className=\"container\">\n                <Switch>\n                    <Route exact path={path} component={Details} />\n                    <Route path={`${path}/update`} component={Update} />\n                </Switch>\n            </div>\n        </div>\n    );\n}\n\nexport { Profile };"
  },
  {
    "path": "src/profile/Update.jsx",
    "content": "import React, { useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'formik';\nimport * as Yup from 'yup';\n\nimport { accountService, alertService } from '@/_services';\n\nfunction Update({ history }) {\n    const user = accountService.userValue;\n    const initialValues = {\n        title: user.title,\n        firstName: user.firstName,\n        lastName: user.lastName,\n        email: user.email,\n        password: '',\n        confirmPassword: ''\n    };\n\n    const validationSchema = Yup.object().shape({\n        title: Yup.string()\n            .required('Title is required'),\n        firstName: Yup.string()\n            .required('First Name is required'),\n        lastName: Yup.string()\n            .required('Last Name is required'),\n        email: Yup.string()\n            .email('Email is invalid')\n            .required('Email is required'),\n        password: Yup.string()\n            .min(6, 'Password must be at least 6 characters'),\n        confirmPassword: Yup.string()\n            .when('password', (password, schema) => {\n                if (password) return schema.required('Confirm Password is required');\n            })\n            .oneOf([Yup.ref('password')], 'Passwords must match')\n    });\n\n    function onSubmit(fields, { setStatus, setSubmitting }) {\n        setStatus();\n        accountService.update(user.id, fields)\n            .then(() => {\n                alertService.success('Update successful', { keepAfterRouteChange: true });\n                history.push('.');\n            })\n            .catch(error => {\n                setSubmitting(false);\n                alertService.error(error);\n            });\n    }\n\n    const [isDeleting, setIsDeleting] = useState(false);\n    function onDelete() {\n        if (confirm('Are you sure?')) {\n            setIsDeleting(true);\n            accountService.delete(user.id)\n                .then(() => alertService.success('Account deleted successfully'));\n        }\n    }\n\n    return (\n        <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>\n            {({ errors, touched, isSubmitting }) => (\n                <Form>\n                    <h1>Update Profile</h1>\n                    <div className=\"form-row\">\n                        <div className=\"form-group col\">\n                            <label>Title</label>\n                            <Field name=\"title\" as=\"select\" className={'form-control' + (errors.title && touched.title ? ' is-invalid' : '')}>\n                                <option value=\"\"></option>\n                                <option value=\"Mr\">Mr</option>\n                                <option value=\"Mrs\">Mrs</option>\n                                <option value=\"Miss\">Miss</option>\n                                <option value=\"Ms\">Ms</option>\n                            </Field>\n                            <ErrorMessage name=\"title\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-group col-5\">\n                            <label>First Name</label>\n                            <Field name=\"firstName\" type=\"text\" className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"firstName\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                        <div className=\"form-group col-5\">\n                            <label>Last Name</label>\n                            <Field name=\"lastName\" type=\"text\" className={'form-control' + (errors.lastName && touched.lastName ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"lastName\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                    </div>\n                    <div className=\"form-group\">\n                        <label>Email</label>\n                        <Field name=\"email\" type=\"text\" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />\n                        <ErrorMessage name=\"email\" component=\"div\" className=\"invalid-feedback\" />\n                    </div>\n                    <h3 className=\"pt-3\">Change Password</h3>\n                    <p>Leave blank to keep the same password</p>\n                    <div className=\"form-row\">\n                        <div className=\"form-group col\">\n                            <label>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 col\">\n                            <label>Confirm Password</label>\n                            <Field name=\"confirmPassword\" type=\"password\" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />\n                            <ErrorMessage name=\"confirmPassword\" component=\"div\" className=\"invalid-feedback\" />\n                        </div>\n                    </div>\n                    <div className=\"form-group\">\n                        <button type=\"submit\" disabled={isSubmitting} className=\"btn btn-primary mr-2\">\n                            {isSubmitting && <span className=\"spinner-border spinner-border-sm mr-1\"></span>}\n                            Update\n                        </button>\n                        <button type=\"button\" onClick={() => onDelete()} className=\"btn btn-danger\" style={{ width: '75px' }} disabled={isDeleting}>\n                            {isDeleting\n                                ? <span className=\"spinner-border spinner-border-sm\"></span>\n                                : <span>Delete</span>\n                            }\n                        </button>\n                        <Link to=\".\" className=\"btn btn-link\">Cancel</Link>\n                    </div>\n                </Form>\n            )}\n        </Formik>\n    )\n}\n\nexport { Update };"
  },
  {
    "path": "src/styles.less",
    "content": "// global styles\na { cursor: pointer; }\n\n.app-container {\n    min-height: 320px;\n}\n\n.admin-nav {\n    padding-top: 0;\n    padding-bottom: 0;\n    background-color: #e8e9ea;\n    border-bottom: 1px solid #ccc;\n}"
  },
  {
    "path": "webpack.config.js",
    "content": "var HtmlWebpackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = {\n    mode: 'development',\n    module: {\n        rules: [\n            {\n                test: /\\.jsx?$/,\n                loader: 'babel-loader'\n            },\n            {\n                test: /\\.less$/,\n                use: [\n                    { loader: 'style-loader' },\n                    { loader: 'css-loader' },\n                    { loader: 'less-loader' }\n                ]\n            }\n        ]\n    },\n    resolve: {\n        mainFiles: ['index', 'Index'],\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}"
  }
]