Repository: cornflourblue/react-signup-verification-boilerplate Branch: master Commit: 9e95d5127a17 Files: 37 Total size: 73.7 KB Directory structure: gitextract_o8cqunop/ ├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── _components/ │ │ ├── Alert.jsx │ │ ├── Nav.jsx │ │ ├── PrivateRoute.jsx │ │ └── index.js │ ├── _helpers/ │ │ ├── fake-backend.js │ │ ├── fetch-wrapper.js │ │ ├── history.js │ │ ├── index.js │ │ └── role.js │ ├── _services/ │ │ ├── account.service.js │ │ ├── alert.service.js │ │ └── index.js │ ├── account/ │ │ ├── ForgotPassword.jsx │ │ ├── Index.jsx │ │ ├── Login.jsx │ │ ├── Register.jsx │ │ ├── ResetPassword.jsx │ │ └── VerifyEmail.jsx │ ├── admin/ │ │ ├── Index.jsx │ │ ├── Overview.jsx │ │ └── users/ │ │ ├── AddEdit.jsx │ │ ├── Index.jsx │ │ └── List.jsx │ ├── app/ │ │ └── Index.jsx │ ├── home/ │ │ └── Index.jsx │ ├── index.html │ ├── index.jsx │ ├── profile/ │ │ ├── Details.jsx │ │ ├── Index.jsx │ │ └── Update.jsx │ └── styles.less └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "@babel/preset-react", "@babel/preset-env" ] } ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules jspm_packages typings # Optional npm cache directory .npm # Optional REPL history .node_repl_history # dist folder dist ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Jason Watmore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # react-signup-verification-boilerplate React - Email Sign Up with Verification, Authentication & Forgot Password For documentation and a live demo see https://jasonwatmore.com/post/2020/04/22/react-email-sign-up-with-verification-authentication-forgot-password ================================================ FILE: package.json ================================================ { "name": "react-signup-verification-boilerplate", "version": "1.0.0", "repository": { "type": "git", "url": "https://github.com/cornflourblue/react-signup-verification-boilerplate.git" }, "license": "MIT", "scripts": { "build": "webpack --mode production", "start": "webpack-dev-server --open" }, "dependencies": { "formik": "^2.1.4", "history": "^4.10.1", "prop-types": "^15.7.2", "query-string": "^6.11.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-router-dom": "^5.0.0", "rxjs": "^6.3.3", "yup": "^0.28.1" }, "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", "@babel/preset-react": "^7.0.0", "babel-loader": "^8.0.5", "css-loader": "^3.4.2", "html-webpack-plugin": "^3.2.0", "less": "^3.11.0", "less-loader": "^5.0.0", "path": "^0.12.7", "style-loader": "^1.1.3", "webpack": "^4.29.6", "webpack-cli": "^3.3.0", "webpack-dev-server": "^3.2.1" } } ================================================ FILE: src/_components/Alert.jsx ================================================ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { alertService, AlertType } from '@/_services'; import { history } from '@/_helpers'; const propTypes = { id: PropTypes.string, fade: PropTypes.bool }; const defaultProps = { id: 'default-alert', fade: true }; function Alert({ id, fade }) { const [alerts, setAlerts] = useState([]); useEffect(() => { // subscribe to new alert notifications const subscription = alertService.onAlert(id) .subscribe(alert => { // clear alerts when an empty alert is received if (!alert.message) { setAlerts(alerts => { // filter out alerts without 'keepAfterRouteChange' flag const filteredAlerts = alerts.filter(x => x.keepAfterRouteChange); // remove 'keepAfterRouteChange' flag on the rest filteredAlerts.forEach(x => delete x.keepAfterRouteChange); return filteredAlerts; }); } else { // add alert to array setAlerts(alerts => ([...alerts, alert])); // auto close alert if required if (alert.autoClose) { setTimeout(() => removeAlert(alert), 3000); } } }); // clear alerts on location change const historyUnlisten = history.listen(({ pathname }) => { // don't clear if pathname has trailing slash because this will be auto redirected again if (pathname.endsWith('/')) return; alertService.clear(id); }); // clean up function that runs when the component unmounts return () => { // unsubscribe & unlisten to avoid memory leaks subscription.unsubscribe(); historyUnlisten(); }; }, []); function removeAlert(alert) { if (fade) { // fade out alert const alertWithFade = { ...alert, fade: true }; setAlerts(alerts => alerts.map(x => x === alert ? alertWithFade : x)); // remove alert after faded out setTimeout(() => { setAlerts(alerts => alerts.filter(x => x !== alertWithFade)); }, 250); } else { // remove alert setAlerts(alerts => alerts.filter(x => x !== alert)); } } function cssClasses(alert) { if (!alert) return; const classes = ['alert', 'alert-dismissable']; const alertTypeClass = { [AlertType.Success]: 'alert alert-success', [AlertType.Error]: 'alert alert-danger', [AlertType.Info]: 'alert alert-info', [AlertType.Warning]: 'alert alert-warning' } classes.push(alertTypeClass[alert.type]); if (alert.fade) { classes.push('fade'); } return classes.join(' '); } if (!alerts.length) return null; return (
{alerts.map((alert, index) =>
removeAlert(alert)}>×
)}
); } Alert.propTypes = propTypes; Alert.defaultProps = defaultProps; export { Alert }; ================================================ FILE: src/_components/Nav.jsx ================================================ import React, { useState, useEffect } from 'react'; import { NavLink, Route } from 'react-router-dom'; import { Role } from '@/_helpers'; import { accountService } from '@/_services'; function Nav() { const [user, setUser] = useState({}); useEffect(() => { const subscription = accountService.user.subscribe(x => setUser(x)); return subscription.unsubscribe; }, []); // only show nav when logged in if (!user) return null; return (
); } function AdminNav({ match }) { const { path } = match; return ( ); } export { Nav }; ================================================ FILE: src/_components/PrivateRoute.jsx ================================================ import React from 'react'; import { Route, Redirect } from 'react-router-dom'; import { accountService } from '@/_services'; function PrivateRoute({ component: Component, roles, ...rest }) { return ( { const user = accountService.userValue; if (!user) { // not logged in so redirect to login page with the return url return } // check if route is restricted by role if (roles && roles.indexOf(user.role) === -1) { // role not authorized so redirect to home page return } // authorized so return component return }} /> ); } export { PrivateRoute }; ================================================ FILE: src/_components/index.js ================================================ export * from './Alert'; export * from './Nav'; export * from './PrivateRoute'; ================================================ FILE: src/_helpers/fake-backend.js ================================================ import { Role } from './' import { alertService } from '@/_services'; // array in local storage for registered users const usersKey = 'react-signup-verification-boilerplate-users'; let users = JSON.parse(localStorage.getItem(usersKey)) || []; export function configureFakeBackend() { let realFetch = window.fetch; window.fetch = function (url, opts) { return new Promise((resolve, reject) => { // wrap in timeout to simulate server api call setTimeout(handleRoute, 500); function handleRoute() { const { method } = opts; switch (true) { case url.endsWith('/accounts/authenticate') && method === 'POST': return authenticate(); case url.endsWith('/accounts/refresh-token') && method === 'POST': return refreshToken(); case url.endsWith('/accounts/revoke-token') && method === 'POST': return revokeToken(); case url.endsWith('/accounts/register') && method === 'POST': return register(); case url.endsWith('/accounts/verify-email') && method === 'POST': return verifyEmail(); case url.endsWith('/accounts/forgot-password') && method === 'POST': return forgotPassword(); case url.endsWith('/accounts/validate-reset-token') && method === 'POST': return validateResetToken(); case url.endsWith('/accounts/reset-password') && method === 'POST': return resetPassword(); case url.endsWith('/accounts') && method === 'GET': return getUsers(); case url.match(/\/accounts\/\d+$/) && method === 'GET': return getUserById(); case url.endsWith('/accounts') && method === 'POST': return createUser(); case url.match(/\/accounts\/\d+$/) && method === 'PUT': return updateUser(); case url.match(/\/accounts\/\d+$/) && method === 'DELETE': return deleteUser(); default: // pass through any requests not handled above return realFetch(url, opts) .then(response => resolve(response)) .catch(error => reject(error)); } } // route functions function authenticate() { const { email, password } = body(); const user = users.find(x => x.email === email && x.password === password && x.isVerified); if (!user) return error('Email or password is incorrect'); // add refresh token to user user.refreshTokens.push(generateRefreshToken()); localStorage.setItem(usersKey, JSON.stringify(users)); return ok({ id: user.id, email: user.email, title: user.title, firstName: user.firstName, lastName: user.lastName, role: user.role, jwtToken: generateJwtToken(user) }); } function refreshToken() { const refreshToken = getRefreshToken(); if (!refreshToken) return unauthorized(); const user = users.find(x => x.refreshTokens.includes(refreshToken)); if (!user) return unauthorized(); // replace old refresh token with a new one and save user.refreshTokens = user.refreshTokens.filter(x => x !== refreshToken); user.refreshTokens.push(generateRefreshToken()); localStorage.setItem(usersKey, JSON.stringify(users)); return ok({ id: user.id, email: user.email, title: user.title, firstName: user.firstName, lastName: user.lastName, role: user.role, jwtToken: generateJwtToken(user) }) } function revokeToken() { if (!isAuthenticated()) return unauthorized(); const refreshToken = getRefreshToken(); const user = users.find(x => x.refreshTokens.includes(refreshToken)); // revoke token and save user.refreshTokens = user.refreshTokens.filter(x => x !== refreshToken); localStorage.setItem(usersKey, JSON.stringify(users)); return ok(); } function register() { const user = body(); if (users.find(x => x.email === user.email)) { // display email already registered "email" in alert setTimeout(() => { alertService.info(`

Email Already Registered

Your email ${user.email} is already registered.

If you don't know your password please visit the forgot password page.

NOTE: The fake backend displayed this "email" so you can test without an api. A real backend would send a real email.
`, { autoClose: false }); }, 1000); // always return ok() response to prevent email enumeration return ok(); } // assign user id and a few other properties then save user.id = newUserId(); if (user.id === 1) { // first registered user is an admin user.role = Role.Admin; } else { user.role = Role.User; } user.dateCreated = new Date().toISOString(); user.verificationToken = new Date().getTime().toString(); user.isVerified = false; user.refreshTokens = []; delete user.confirmPassword; users.push(user); localStorage.setItem(usersKey, JSON.stringify(users)); // display verification email in alert setTimeout(() => { const verifyUrl = `${location.origin}/account/verify-email?token=${user.verificationToken}`; alertService.info(`

Verification Email

Thanks for registering!

Please click the below link to verify your email address:

${verifyUrl}

NOTE: The fake backend displayed this "email" so you can test without an api. A real backend would send a real email.
`, { autoClose: false }); }, 1000); return ok(); } function verifyEmail() { const { token } = body(); const user = users.find(x => !!x.verificationToken && x.verificationToken === token); if (!user) return error('Verification failed'); // set is verified flag to true if token is valid user.isVerified = true; localStorage.setItem(usersKey, JSON.stringify(users)); return ok(); } function forgotPassword() { const { email } = body(); const user = users.find(x => x.email === email); // always return ok() response to prevent email enumeration if (!user) return ok(); // create reset token that expires after 24 hours user.resetToken = new Date().getTime().toString(); user.resetTokenExpires = new Date(Date.now() + 24*60*60*1000).toISOString(); localStorage.setItem(usersKey, JSON.stringify(users)); // display password reset email in alert setTimeout(() => { const resetUrl = `${location.origin}/account/reset-password?token=${user.resetToken}`; alertService.info(`

Reset Password Email

Please click the below link to reset your password, the link will be valid for 1 day:

${resetUrl}

NOTE: The fake backend displayed this "email" so you can test without an api. A real backend would send a real email.
`, { autoClose: false }); }, 1000); return ok(); } function validateResetToken() { const { token } = body(); const user = users.find(x => !!x.resetToken && x.resetToken === token && new Date() < new Date(x.resetTokenExpires) ); if (!user) return error('Invalid token'); return ok(); } function resetPassword() { const { token, password } = body(); const user = users.find(x => !!x.resetToken && x.resetToken === token && new Date() < new Date(x.resetTokenExpires) ); if (!user) return error('Invalid token'); // update password and remove reset token user.password = password; user.isVerified = true; delete user.resetToken; delete user.resetTokenExpires; localStorage.setItem(usersKey, JSON.stringify(users)); return ok(); } function getUsers() { if (!isAuthorized(Role.Admin)) return unauthorized(); return ok(users); } function getUserById() { if (!isAuthenticated()) return unauthorized(); let user = users.find(x => x.id === idFromUrl()); // users can get own profile and admins can get all profiles if (user.id !== currentUser().id && !isAuthorized(Role.Admin)) { return unauthorized(); } return ok(user); } function createUser() { if (!isAuthorized(Role.Admin)) return unauthorized(); const user = body(); if (users.find(x => x.email === user.email)) { return error(`Email ${user.email} is already registered`); } // assign user id and a few other properties then save user.id = newUserId(); user.dateCreated = new Date().toISOString(); user.isVerified = true; user.refreshTokens = []; delete user.confirmPassword; users.push(user); localStorage.setItem(usersKey, JSON.stringify(users)); return ok(); } function updateUser() { if (!isAuthenticated()) return unauthorized(); let params = body(); let user = users.find(x => x.id === idFromUrl()); // users can update own profile and admins can update all profiles if (user.id !== currentUser().id && !isAuthorized(Role.Admin)) { return unauthorized(); } // only update password if included if (!params.password) { delete params.password; } // don't save confirm password delete params.confirmPassword; // update and save user Object.assign(user, params); localStorage.setItem(usersKey, JSON.stringify(users)); return ok({ id: user.id, email: user.email, title: user.title, firstName: user.firstName, lastName: user.lastName, role: user.role }); } function deleteUser() { if (!isAuthenticated()) return unauthorized(); let user = users.find(x => x.id === idFromUrl()); // users can delete own account and admins can delete any account if (user.id !== currentUser().id && !isAuthorized(Role.Admin)) { return unauthorized(); } // delete user then save users = users.filter(x => x.id !== idFromUrl()); localStorage.setItem(usersKey, JSON.stringify(users)); return ok(); } // helper functions function ok(body) { resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) }); } function unauthorized() { resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorized' })) }); } function error(message) { resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) }); } function isAuthenticated() { return !!currentUser(); } function isAuthorized(role) { const user = currentUser(); if (!user) return false; return user.role === role; } function idFromUrl() { const urlParts = url.split('/'); return parseInt(urlParts[urlParts.length - 1]); } function body() { return opts.body && JSON.parse(opts.body); } function newUserId() { return users.length ? Math.max(...users.map(x => x.id)) + 1 : 1; } function generateJwtToken(user) { // create token that expires in 15 minutes const tokenPayload = { exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000), id: user.id } return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`; } function currentUser() { // check if jwt token is in auth header const authHeader = opts.headers['Authorization'] || ''; if (!authHeader.startsWith('Bearer fake-jwt-token')) return; // check if token is expired const jwtToken = JSON.parse(atob(authHeader.split('.')[1])); const tokenExpired = Date.now() > (jwtToken.exp * 1000); if (tokenExpired) return; const user = users.find(x => x.id === jwtToken.id); return user; } function generateRefreshToken() { const token = new Date().getTime().toString(); // add token cookie that expires in 7 days const expires = new Date(Date.now() + 7*24*60*60*1000).toUTCString(); document.cookie = `fakeRefreshToken=${token}; expires=${expires}; path=/`; return token; } function getRefreshToken() { // get refresh token from cookie return (document.cookie.split(';').find(x => x.includes('fakeRefreshToken')) || '=').split('=')[1]; } }); } } ================================================ FILE: src/_helpers/fetch-wrapper.js ================================================ import config from 'config'; import { accountService } from '@/_services'; export const fetchWrapper = { get, post, put, delete: _delete } function get(url) { const requestOptions = { method: 'GET', headers: authHeader(url) }; return fetch(url, requestOptions).then(handleResponse); } function post(url, body) { const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader(url) }, credentials: 'include', body: JSON.stringify(body) }; return fetch(url, requestOptions).then(handleResponse); } function put(url, body) { const requestOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', ...authHeader(url) }, body: JSON.stringify(body) }; return fetch(url, requestOptions).then(handleResponse); } // prefixed with underscored because delete is a reserved word in javascript function _delete(url) { const requestOptions = { method: 'DELETE', headers: authHeader(url) }; return fetch(url, requestOptions).then(handleResponse); } // helper functions function authHeader(url) { // return auth header with jwt if user is logged in and request is to the api url const user = accountService.userValue; const isLoggedIn = user && user.jwtToken; const isApiUrl = url.startsWith(config.apiUrl); if (isLoggedIn && isApiUrl) { return { Authorization: `Bearer ${user.jwtToken}` }; } else { return {}; } } function handleResponse(response) { return response.text().then(text => { const data = text && JSON.parse(text); if (!response.ok) { if ([401, 403].includes(response.status) && accountService.userValue) { // auto logout if 401 Unauthorized or 403 Forbidden response returned from api accountService.logout(); } const error = (data && data.message) || response.statusText; return Promise.reject(error); } return data; }); } ================================================ FILE: src/_helpers/history.js ================================================ import { createBrowserHistory } from 'history'; export const history = createBrowserHistory(); ================================================ FILE: src/_helpers/index.js ================================================ export * from './fake-backend'; export * from './fetch-wrapper'; export * from './history'; export * from './role'; ================================================ FILE: src/_helpers/role.js ================================================ export const Role = { Admin: 'Admin', User: 'User' } ================================================ FILE: src/_services/account.service.js ================================================ import { BehaviorSubject } from 'rxjs'; import config from 'config'; import { fetchWrapper, history } from '@/_helpers'; const userSubject = new BehaviorSubject(null); const baseUrl = `${config.apiUrl}/accounts`; export const accountService = { login, logout, refreshToken, register, verifyEmail, forgotPassword, validateResetToken, resetPassword, getAll, getById, create, update, delete: _delete, user: userSubject.asObservable(), get userValue () { return userSubject.value } }; function login(email, password) { return fetchWrapper.post(`${baseUrl}/authenticate`, { email, password }) .then(user => { // publish user to subscribers and start timer to refresh token userSubject.next(user); startRefreshTokenTimer(); return user; }); } function logout() { // revoke token, stop refresh timer, publish null to user subscribers and redirect to login page fetchWrapper.post(`${baseUrl}/revoke-token`, {}); stopRefreshTokenTimer(); userSubject.next(null); history.push('/account/login'); } function refreshToken() { return fetchWrapper.post(`${baseUrl}/refresh-token`, {}) .then(user => { // publish user to subscribers and start timer to refresh token userSubject.next(user); startRefreshTokenTimer(); return user; }); } function register(params) { return fetchWrapper.post(`${baseUrl}/register`, params); } function verifyEmail(token) { return fetchWrapper.post(`${baseUrl}/verify-email`, { token }); } function forgotPassword(email) { return fetchWrapper.post(`${baseUrl}/forgot-password`, { email }); } function validateResetToken(token) { return fetchWrapper.post(`${baseUrl}/validate-reset-token`, { token }); } function resetPassword({ token, password, confirmPassword }) { return fetchWrapper.post(`${baseUrl}/reset-password`, { token, password, confirmPassword }); } function getAll() { return fetchWrapper.get(baseUrl); } function getById(id) { return fetchWrapper.get(`${baseUrl}/${id}`); } function create(params) { return fetchWrapper.post(baseUrl, params); } function update(id, params) { return fetchWrapper.put(`${baseUrl}/${id}`, params) .then(user => { // update stored user if the logged in user updated their own record if (user.id === userSubject.value.id) { // publish updated user to subscribers user = { ...userSubject.value, ...user }; userSubject.next(user); } return user; }); } // prefixed with underscore because 'delete' is a reserved word in javascript function _delete(id) { return fetchWrapper.delete(`${baseUrl}/${id}`) .then(x => { // auto logout if the logged in user deleted their own record if (id === userSubject.value.id) { logout(); } return x; }); } // helper functions let refreshTokenTimeout; function startRefreshTokenTimer() { // parse json object from base64 encoded jwt token const jwtToken = JSON.parse(atob(userSubject.value.jwtToken.split('.')[1])); // set a timeout to refresh the token a minute before it expires const expires = new Date(jwtToken.exp * 1000); const timeout = expires.getTime() - Date.now() - (60 * 1000); refreshTokenTimeout = setTimeout(refreshToken, timeout); } function stopRefreshTokenTimer() { clearTimeout(refreshTokenTimeout); } ================================================ FILE: src/_services/alert.service.js ================================================ import { Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; const alertSubject = new Subject(); const defaultId = 'default-alert'; export const alertService = { onAlert, success, error, info, warn, alert, clear }; export const AlertType = { Success: 'Success', Error: 'Error', Info: 'Info', Warning: 'Warning' } // enable subscribing to alerts observable function onAlert(id = defaultId) { return alertSubject.asObservable().pipe(filter(x => x && x.id === id)); } // convenience methods function success(message, options) { alert({ ...options, type: AlertType.Success, message }); } function error(message, options) { alert({ ...options, type: AlertType.Error, message }); } function info(message, options) { alert({ ...options, type: AlertType.Info, message }); } function warn(message, options) { alert({ ...options, type: AlertType.Warning, message }); } // core alert method function alert(alert) { alert.id = alert.id || defaultId; alert.autoClose = (alert.autoClose === undefined ? true : alert.autoClose); alertSubject.next(alert); } // clear alerts function clear(id = defaultId) { alertSubject.next({ id }); } ================================================ FILE: src/_services/index.js ================================================ export * from './account.service'; export * from './alert.service'; ================================================ FILE: src/account/ForgotPassword.jsx ================================================ import React from 'react'; import { Link } from 'react-router-dom'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; import { accountService, alertService } from '@/_services'; function ForgotPassword() { const initialValues = { email: '' }; const validationSchema = Yup.object().shape({ email: Yup.string() .email('Email is invalid') .required('Email is required') }); function onSubmit({ email }, { setSubmitting }) { alertService.clear(); accountService.forgotPassword(email) .then(() => alertService.success('Please check your email for password reset instructions')) .catch(error => alertService.error(error)) .finally(() => setSubmitting(false)); } return ( {({ errors, touched, isSubmitting }) => (

Forgot Password

Cancel
)}
) } export { ForgotPassword }; ================================================ FILE: src/account/Index.jsx ================================================ import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; import { accountService } from '@/_services'; import { Login } from './Login'; import { Register } from './Register'; import { VerifyEmail } from './VerifyEmail'; import { ForgotPassword } from './ForgotPassword'; import { ResetPassword } from './ResetPassword'; function Account({ history, match }) { const { path } = match; useEffect(() => { // redirect to home if already logged in if (accountService.userValue) { history.push('/'); } }, []); return (
); } export { Account }; ================================================ FILE: src/account/Login.jsx ================================================ import React from 'react'; import { Link } from 'react-router-dom'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; import { accountService, alertService } from '@/_services'; function Login({ history, location }) { const initialValues = { email: '', password: '' }; const validationSchema = Yup.object().shape({ email: Yup.string() .email('Email is invalid') .required('Email is required'), password: Yup.string().required('Password is required') }); function onSubmit({ email, password }, { setSubmitting }) { alertService.clear(); accountService.login(email, password) .then(() => { const { from } = location.state || { from: { pathname: "/" } }; history.push(from); }) .catch(error => { setSubmitting(false); alertService.error(error); }); } return ( {({ errors, touched, isSubmitting }) => (

Login

Register
Forgot Password?
)}
) } export { Login }; ================================================ FILE: src/account/Register.jsx ================================================ import React from 'react'; import { Link } from 'react-router-dom'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; import { accountService, alertService } from '@/_services'; function Register({ history }) { const initialValues = { title: '', firstName: '', lastName: '', email: '', password: '', confirmPassword: '', acceptTerms: false }; const validationSchema = Yup.object().shape({ title: Yup.string() .required('Title is required'), firstName: Yup.string() .required('First Name is required'), lastName: Yup.string() .required('Last Name is required'), email: Yup.string() .email('Email is invalid') .required('Email is required'), password: Yup.string() .min(6, 'Password must be at least 6 characters') .required('Password is required'), confirmPassword: Yup.string() .oneOf([Yup.ref('password'), null], 'Passwords must match') .required('Confirm Password is required'), acceptTerms: Yup.bool() .oneOf([true], 'Accept Terms & Conditions is required') }); function onSubmit(fields, { setStatus, setSubmitting }) { setStatus(); accountService.register(fields) .then(() => { alertService.success('Registration successful, please check your email for verification instructions', { keepAfterRouteChange: true }); history.push('login'); }) .catch(error => { setSubmitting(false); alertService.error(error); }); } return ( {({ errors, touched, isSubmitting }) => (

Register

Cancel
)}
) } export { Register }; ================================================ FILE: src/account/ResetPassword.jsx ================================================ import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import queryString from 'query-string'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; import { accountService, alertService } from '@/_services'; function ResetPassword({ history }) { const TokenStatus = { Validating: 'Validating', Valid: 'Valid', Invalid: 'Invalid' } const [token, setToken] = useState(null); const [tokenStatus, setTokenStatus] = useState(TokenStatus.Validating); useEffect(() => { const { token } = queryString.parse(location.search); // remove token from url to prevent http referer leakage history.replace(location.pathname); accountService.validateResetToken(token) .then(() => { setToken(token); setTokenStatus(TokenStatus.Valid); }) .catch(() => { setTokenStatus(TokenStatus.Invalid); }); }, []); function getForm() { const initialValues = { password: '', confirmPassword: '' }; const validationSchema = Yup.object().shape({ password: Yup.string() .min(6, 'Password must be at least 6 characters') .required('Password is required'), confirmPassword: Yup.string() .oneOf([Yup.ref('password'), null], 'Passwords must match') .required('Confirm Password is required'), }); function onSubmit({ password, confirmPassword }, { setSubmitting }) { alertService.clear(); accountService.resetPassword({ token, password, confirmPassword }) .then(() => { alertService.success('Password reset successful, you can now login', { keepAfterRouteChange: true }); history.push('login'); }) .catch(error => { setSubmitting(false); alertService.error(error); }); } return ( {({ errors, touched, isSubmitting }) => (
Cancel
)}
); } function getBody() { switch (tokenStatus) { case TokenStatus.Valid: return getForm(); case TokenStatus.Invalid: return
Token validation failed, if the token has expired you can get a new one at the forgot password page.
; case TokenStatus.Validating: return
Validating token...
; } } return (

Reset Password

{getBody()}
) } export { ResetPassword }; ================================================ FILE: src/account/VerifyEmail.jsx ================================================ import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import queryString from 'query-string'; import { accountService, alertService } from '@/_services'; function VerifyEmail({ history }) { const EmailStatus = { Verifying: 'Verifying', Failed: 'Failed' } const [emailStatus, setEmailStatus] = useState(EmailStatus.Verifying); useEffect(() => { const { token } = queryString.parse(location.search); // remove token from url to prevent http referer leakage history.replace(location.pathname); accountService.verifyEmail(token) .then(() => { alertService.success('Verification successful, you can now login', { keepAfterRouteChange: true }); history.push('login'); }) .catch(() => { setEmailStatus(EmailStatus.Failed); }); }, []); function getBody() { switch (emailStatus) { case EmailStatus.Verifying: return
Verifying...
; case EmailStatus.Failed: return
Verification failed, you can also verify your account using the forgot password page.
; } } return (

Verify Email

{getBody()}
) } export { VerifyEmail }; ================================================ FILE: src/admin/Index.jsx ================================================ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { Overview } from './Overview'; import { Users } from './users'; function Admin({ match }) { const { path } = match; return (
); } export { Admin }; ================================================ FILE: src/admin/Overview.jsx ================================================ import React from 'react'; import { Link } from 'react-router-dom'; function Overview({ match }) { const { path } = match; return (

Admin

This section can only be accessed by administrators.

Manage Users

); } export { Overview }; ================================================ FILE: src/admin/users/AddEdit.jsx ================================================ import React, { useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; import { accountService, alertService } from '@/_services'; function AddEdit({ history, match }) { const { id } = match.params; const isAddMode = !id; const initialValues = { title: '', firstName: '', lastName: '', email: '', role: '', password: '', confirmPassword: '' }; const validationSchema = Yup.object().shape({ title: Yup.string() .required('Title is required'), firstName: Yup.string() .required('First Name is required'), lastName: Yup.string() .required('Last Name is required'), email: Yup.string() .email('Email is invalid') .required('Email is required'), role: Yup.string() .required('Role is required'), password: Yup.string() .concat(isAddMode ? Yup.string().required('Password is required') : null) .min(6, 'Password must be at least 6 characters'), confirmPassword: Yup.string() .when('password', (password, schema) => { if (password) return schema.required('Confirm Password is required'); }) .oneOf([Yup.ref('password')], 'Passwords must match') }); function onSubmit(fields, { setStatus, setSubmitting }) { setStatus(); if (isAddMode) { createUser(fields, setSubmitting); } else { updateUser(id, fields, setSubmitting); } } function createUser(fields, setSubmitting) { accountService.create(fields) .then(() => { alertService.success('User added successfully', { keepAfterRouteChange: true }); history.push('.'); }) .catch(error => { setSubmitting(false); alertService.error(error); }); } function updateUser(id, fields, setSubmitting) { accountService.update(id, fields) .then(() => { alertService.success('Update successful', { keepAfterRouteChange: true }); history.push('..'); }) .catch(error => { setSubmitting(false); alertService.error(error); }); } return ( {({ errors, touched, isSubmitting, setFieldValue }) => { useEffect(() => { if (!isAddMode) { // get user and set form fields accountService.getById(id).then(user => { const fields = ['title', 'firstName', 'lastName', 'email', 'role']; fields.forEach(field => setFieldValue(field, user[field], false)); }); } }, []); return (

{isAddMode ? 'Add User' : 'Edit User'}

{!isAddMode &&

Change Password

Leave blank to keep the same password

}
Cancel
); }}
); } export { AddEdit }; ================================================ FILE: src/admin/users/Index.jsx ================================================ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { List } from './List'; import { AddEdit } from './AddEdit'; function Users({ match }) { const { path } = match; return ( ); } export { Users }; ================================================ FILE: src/admin/users/List.jsx ================================================ import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { accountService } from '@/_services'; function List({ match }) { const { path } = match; const [users, setUsers] = useState(null); useEffect(() => { accountService.getAll().then(x => setUsers(x)); }, []); function deleteUser(id) { setUsers(users.map(x => { if (x.id === id) { x.isDeleting = true; } return x; })); accountService.delete(id).then(() => { setUsers(users => users.filter(x => x.id !== id)); }); } return (

Users

All users from secure (admin only) api end point:

Add User {users && users.map(user => )} {!users && }
Name Email Role
{user.title} {user.firstName} {user.lastName} {user.email} {user.role} Edit
); } export { List }; ================================================ FILE: src/app/Index.jsx ================================================ import React, { useState, useEffect } from 'react'; import { Route, Switch, Redirect, useLocation } from 'react-router-dom'; import { Role } from '@/_helpers'; import { accountService } from '@/_services'; import { Nav, PrivateRoute, Alert } from '@/_components'; import { Home } from '@/home'; import { Profile } from '@/profile'; import { Admin } from '@/admin'; import { Account } from '@/account'; function App() { const { pathname } = useLocation(); const [user, setUser] = useState({}); useEffect(() => { const subscription = accountService.user.subscribe(x => setUser(x)); return subscription.unsubscribe; }, []); return (
); } export { App }; ================================================ FILE: src/home/Index.jsx ================================================ import React from 'react'; import { accountService } from '@/_services'; function Home() { const user = accountService.userValue; return (

Hi {user.firstName}!

You're logged in with React & JWT!!

); } export { Home }; ================================================ FILE: src/index.html ================================================ React - Email Sign Up with Verification, Authentication & Forgot Password
================================================ FILE: src/index.jsx ================================================ import React from 'react'; import { Router } from 'react-router-dom'; import { render } from 'react-dom'; import { history } from './_helpers'; import { accountService } from './_services'; import { App } from './app'; import './styles.less'; // setup fake backend import { configureFakeBackend } from './_helpers'; configureFakeBackend(); // attempt silent token refresh before startup accountService.refreshToken().finally(startApp); function startApp() { render( , document.getElementById('app') ); } ================================================ FILE: src/profile/Details.jsx ================================================ import React from 'react'; import { Link } from 'react-router-dom'; import { accountService } from '@/_services'; function Details({ match }) { const { path } = match; const user = accountService.userValue; return (

My Profile

Name: {user.title} {user.firstName} {user.lastName}
Email: {user.email}

Update Profile

); } export { Details }; ================================================ FILE: src/profile/Index.jsx ================================================ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { Details } from './Details'; import { Update } from './Update'; function Profile({ match }) { const { path } = match; return (
); } export { Profile }; ================================================ FILE: src/profile/Update.jsx ================================================ import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; import { accountService, alertService } from '@/_services'; function Update({ history }) { const user = accountService.userValue; const initialValues = { title: user.title, firstName: user.firstName, lastName: user.lastName, email: user.email, password: '', confirmPassword: '' }; const validationSchema = Yup.object().shape({ title: Yup.string() .required('Title is required'), firstName: Yup.string() .required('First Name is required'), lastName: Yup.string() .required('Last Name is required'), email: Yup.string() .email('Email is invalid') .required('Email is required'), password: Yup.string() .min(6, 'Password must be at least 6 characters'), confirmPassword: Yup.string() .when('password', (password, schema) => { if (password) return schema.required('Confirm Password is required'); }) .oneOf([Yup.ref('password')], 'Passwords must match') }); function onSubmit(fields, { setStatus, setSubmitting }) { setStatus(); accountService.update(user.id, fields) .then(() => { alertService.success('Update successful', { keepAfterRouteChange: true }); history.push('.'); }) .catch(error => { setSubmitting(false); alertService.error(error); }); } const [isDeleting, setIsDeleting] = useState(false); function onDelete() { if (confirm('Are you sure?')) { setIsDeleting(true); accountService.delete(user.id) .then(() => alertService.success('Account deleted successfully')); } } return ( {({ errors, touched, isSubmitting }) => (

Update Profile

Change Password

Leave blank to keep the same password

Cancel
)}
) } export { Update }; ================================================ FILE: src/styles.less ================================================ // global styles a { cursor: pointer; } .app-container { min-height: 320px; } .admin-nav { padding-top: 0; padding-bottom: 0; background-color: #e8e9ea; border-bottom: 1px solid #ccc; } ================================================ FILE: webpack.config.js ================================================ var HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); module.exports = { mode: 'development', module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader' }, { test: /\.less$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'less-loader' } ] } ] }, resolve: { mainFiles: ['index', 'Index'], extensions: ['.js', '.jsx'], alias: { '@': path.resolve(__dirname, 'src/'), } }, plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })], devServer: { historyApiFallback: true }, externals: { // global app config object config: JSON.stringify({ apiUrl: 'http://localhost:4000' }) } }