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 (
<div className="container">
<div className="m-3">
{alerts.map((alert, index) =>
<div key={index} className={cssClasses(alert)}>
<a className="close" onClick={() => removeAlert(alert)}>×</a>
<span dangerouslySetInnerHTML={{__html: alert.message}}></span>
</div>
)}
</div>
</div>
);
}
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 (
<div>
<nav className="navbar navbar-expand navbar-dark bg-dark">
<div className="navbar-nav">
<NavLink exact to="/" className="nav-item nav-link">Home</NavLink>
<NavLink to="/profile" className="nav-item nav-link">Profile</NavLink>
{user.role === Role.Admin &&
<NavLink to="/admin" className="nav-item nav-link">Admin</NavLink>
}
<a onClick={accountService.logout} className="nav-item nav-link">Logout</a>
</div>
</nav>
<Route path="/admin" component={AdminNav} />
</div>
);
}
function AdminNav({ match }) {
const { path } = match;
return (
<nav className="admin-nav navbar navbar-expand navbar-light">
<div className="navbar-nav">
<NavLink to={`${path}/users`} className="nav-item nav-link">Users</NavLink>
</div>
</nav>
);
}
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 (
<Route {...rest} render={props => {
const user = accountService.userValue;
if (!user) {
// not logged in so redirect to login page with the return url
return <Redirect to={{ pathname: '/account/login', state: { from: props.location } }} />
}
// check if route is restricted by role
if (roles && roles.indexOf(user.role) === -1) {
// role not authorized so redirect to home page
return <Redirect to={{ pathname: '/'}} />
}
// authorized so return component
return <Component {...props} />
}} />
);
}
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(`
<h4>Email Already Registered</h4>
<p>Your email ${user.email} is already registered.</p>
<p>If you don't know your password please visit the <a href="${location.origin}/account/forgot-password">forgot password</a> page.</p>
<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>
`, { 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(`
<h4>Verification Email</h4>
<p>Thanks for registering!</p>
<p>Please click the below link to verify your email address:</p>
<p><a href="${verifyUrl}">${verifyUrl}</a></p>
<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>
`, { 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(`
<h4>Reset Password Email</h4>
<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
<p><a href="${resetUrl}">${resetUrl}</a></p>
<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>
`, { 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 (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ errors, touched, isSubmitting }) => (
<Form>
<h3 className="card-header">Forgot Password</h3>
<div className="card-body">
<div className="form-group">
<label>Email</label>
<Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
<ErrorMessage name="email" component="div" className="invalid-feedback" />
</div>
<div className="form-row">
<div className="form-group col">
<button type="submit" disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Submit
</button>
<Link to="login" className="btn btn-link">Cancel</Link>
</div>
</div>
</div>
</Form>
)}
</Formik>
)
}
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 (
<div className="container">
<div className="row">
<div className="col-sm-8 offset-sm-2 mt-5">
<div className="card m-3">
<Switch>
<Route path={`${path}/login`} component={Login} />
<Route path={`${path}/register`} component={Register} />
<Route path={`${path}/verify-email`} component={VerifyEmail} />
<Route path={`${path}/forgot-password`} component={ForgotPassword} />
<Route path={`${path}/reset-password`} component={ResetPassword} />
</Switch>
</div>
</div>
</div>
</div>
);
}
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 (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ errors, touched, isSubmitting }) => (
<Form>
<h3 className="card-header">Login</h3>
<div className="card-body">
<div className="form-group">
<label>Email</label>
<Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
<ErrorMessage name="email" component="div" className="invalid-feedback" />
</div>
<div className="form-group">
<label>Password</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-row">
<div className="form-group col">
<button type="submit" disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Login
</button>
<Link to="register" className="btn btn-link">Register</Link>
</div>
<div className="form-group col text-right">
<Link to="forgot-password" className="btn btn-link pr-0">Forgot Password?</Link>
</div>
</div>
</div>
</Form>
)}
</Formik>
)
}
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 (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ errors, touched, isSubmitting }) => (
<Form>
<h3 className="card-header">Register</h3>
<div className="card-body">
<div className="form-row">
<div className="form-group col">
<label>Title</label>
<Field name="title" as="select" className={'form-control' + (errors.title && touched.title ? ' is-invalid' : '')}>
<option value=""></option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Miss">Miss</option>
<option value="Ms">Ms</option>
</Field>
<ErrorMessage name="title" component="div" className="invalid-feedback" />
</div>
<div className="form-group col-5">
<label>First Name</label>
<Field name="firstName" type="text" className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />
<ErrorMessage name="firstName" component="div" className="invalid-feedback" />
</div>
<div className="form-group col-5">
<label>Last Name</label>
<Field name="lastName" type="text" className={'form-control' + (errors.lastName && touched.lastName ? ' is-invalid' : '')} />
<ErrorMessage name="lastName" component="div" className="invalid-feedback" />
</div>
</div>
<div className="form-group">
<label>Email</label>
<Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
<ErrorMessage name="email" component="div" className="invalid-feedback" />
</div>
<div className="form-row">
<div className="form-group col">
<label>Password</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-group col">
<label>Confirm Password</label>
<Field name="confirmPassword" type="password" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />
<ErrorMessage name="confirmPassword" component="div" className="invalid-feedback" />
</div>
</div>
<div className="form-group form-check">
<Field type="checkbox" name="acceptTerms" id="acceptTerms" className={'form-check-input ' + (errors.acceptTerms && touched.acceptTerms ? ' is-invalid' : '')} />
<label htmlFor="acceptTerms" className="form-check-label">Accept Terms & Conditions</label>
<ErrorMessage name="acceptTerms" component="div" className="invalid-feedback" />
</div>
<div className="form-group">
<button type="submit" disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Register
</button>
<Link to="login" className="btn btn-link">Cancel</Link>
</div>
</div>
</Form>
)}
</Formik>
)
}
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 (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ errors, touched, isSubmitting }) => (
<Form>
<div className="form-group">
<label>Password</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-group">
<label>Confirm Password</label>
<Field name="confirmPassword" type="password" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />
<ErrorMessage name="confirmPassword" component="div" className="invalid-feedback" />
</div>
<div className="form-row">
<div className="form-group col">
<button type="submit" disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Reset Password
</button>
<Link to="login" className="btn btn-link">Cancel</Link>
</div>
</div>
</Form>
)}
</Formik>
);
}
function getBody() {
switch (tokenStatus) {
case TokenStatus.Valid:
return getForm();
case TokenStatus.Invalid:
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>;
case TokenStatus.Validating:
return <div>Validating token...</div>;
}
}
return (
<div>
<h3 className="card-header">Reset Password</h3>
<div className="card-body">{getBody()}</div>
</div>
)
}
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 <div>Verifying...</div>;
case EmailStatus.Failed:
return <div>Verification failed, you can also verify your account using the <Link to="forgot-password">forgot password</Link> page.</div>;
}
}
return (
<div>
<h3 className="card-header">Verify Email</h3>
<div className="card-body">{getBody()}</div>
</div>
)
}
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 (
<div className="p-4">
<div className="container">
<Switch>
<Route exact path={path} component={Overview} />
<Route path={`${path}/users`} component={Users} />
</Switch>
</div>
</div>
);
}
export { Admin };
================================================
FILE: src/admin/Overview.jsx
================================================
import React from 'react';
import { Link } from 'react-router-dom';
function Overview({ match }) {
const { path } = match;
return (
<div>
<h1>Admin</h1>
<p>This section can only be accessed by administrators.</p>
<p><Link to={`${path}/users`}>Manage Users</Link></p>
</div>
);
}
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 (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ 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 (
<Form>
<h1>{isAddMode ? 'Add User' : 'Edit User'}</h1>
<div className="form-row">
<div className="form-group col">
<label>Title</label>
<Field name="title" as="select" className={'form-control' + (errors.title && touched.title ? ' is-invalid' : '')}>
<option value=""></option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Miss">Miss</option>
<option value="Ms">Ms</option>
</Field>
<ErrorMessage name="title" component="div" className="invalid-feedback" />
</div>
<div className="form-group col-5">
<label>First Name</label>
<Field name="firstName" type="text" className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />
<ErrorMessage name="firstName" component="div" className="invalid-feedback" />
</div>
<div className="form-group col-5">
<label>Last Name</label>
<Field name="lastName" type="text" className={'form-control' + (errors.lastName && touched.lastName ? ' is-invalid' : '')} />
<ErrorMessage name="lastName" component="div" className="invalid-feedback" />
</div>
</div>
<div className="form-row">
<div className="form-group col-7">
<label>Email</label>
<Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
<ErrorMessage name="email" component="div" className="invalid-feedback" />
</div>
<div className="form-group col">
<label>Role</label>
<Field name="role" as="select" className={'form-control' + (errors.role && touched.role ? ' is-invalid' : '')}>
<option value=""></option>
<option value="User">User</option>
<option value="Admin">Admin</option>
</Field>
<ErrorMessage name="role" component="div" className="invalid-feedback" />
</div>
</div>
{!isAddMode &&
<div>
<h3 className="pt-3">Change Password</h3>
<p>Leave blank to keep the same password</p>
</div>
}
<div className="form-row">
<div className="form-group col">
<label>Password</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-group col">
<label>Confirm Password</label>
<Field name="confirmPassword" type="password" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />
<ErrorMessage name="confirmPassword" component="div" className="invalid-feedback" />
</div>
</div>
<div className="form-group">
<button type="submit" disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Save
</button>
<Link to={isAddMode ? '.' : '..'} className="btn btn-link">Cancel</Link>
</div>
</Form>
);
}}
</Formik>
);
}
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 (
<Switch>
<Route exact path={path} component={List} />
<Route path={`${path}/add`} component={AddEdit} />
<Route path={`${path}/edit/:id`} component={AddEdit} />
</Switch>
);
}
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 (
<div>
<h1>Users</h1>
<p>All users from secure (admin only) api end point:</p>
<Link to={`${path}/add`} className="btn btn-sm btn-success mb-2">Add User</Link>
<table className="table table-striped">
<thead>
<tr>
<th style={{ width: '30%' }}>Name</th>
<th style={{ width: '30%' }}>Email</th>
<th style={{ width: '30%' }}>Role</th>
<th style={{ width: '10%' }}></th>
</tr>
</thead>
<tbody>
{users && users.map(user =>
<tr key={user.id}>
<td>{user.title} {user.firstName} {user.lastName}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td style={{ whiteSpace: 'nowrap' }}>
<Link to={`${path}/edit/${user.id}`} className="btn btn-sm btn-primary mr-1">Edit</Link>
<button onClick={() => deleteUser(user.id)} className="btn btn-sm btn-danger" style={{ width: '60px' }} disabled={user.isDeleting}>
{user.isDeleting
? <span className="spinner-border spinner-border-sm"></span>
: <span>Delete</span>
}
</button>
</td>
</tr>
)}
{!users &&
<tr>
<td colSpan="4" className="text-center">
<span className="spinner-border spinner-border-lg align-center"></span>
</td>
</tr>
}
</tbody>
</table>
</div>
);
}
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 (
<div className={'app-container' + (user && ' bg-light')}>
<Nav />
<Alert />
<Switch>
<Redirect from="/:url*(/+)" to={pathname.slice(0, -1)} />
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute path="/profile" component={Profile} />
<PrivateRoute path="/admin" roles={[Role.Admin]} component={Admin} />
<Route path="/account" component={Account} />
<Redirect from="*" to="/" />
</Switch>
</div>
);
}
export { App };
================================================
FILE: src/home/Index.jsx
================================================
import React from 'react';
import { accountService } from '@/_services';
function Home() {
const user = accountService.userValue;
return (
<div className="p-4">
<div className="container">
<h1>Hi {user.firstName}!</h1>
<p>You're logged in with React & JWT!!</p>
</div>
</div>
);
}
export { Home };
================================================
FILE: src/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<base href="/" />
<title>React - Email Sign Up with Verification, Authentication & Forgot Password</title>
<!-- bootstrap css -->
<link href="//netdna.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<!-- credits -->
<div class="text-center mt-4">
<p>
<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>
</p>
<p>
<a href="https://jasonwatmore.com" target="_top">JasonWatmore.com</a>
</p>
</div>
</body>
</html>
================================================
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(
<Router history={history}>
<App />
</Router>,
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 (
<div>
<h1>My Profile</h1>
<p>
<strong>Name: </strong> {user.title} {user.firstName} {user.lastName}<br />
<strong>Email: </strong> {user.email}
</p>
<p><Link to={`${path}/update`}>Update Profile</Link></p>
</div>
);
}
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 (
<div className="p-4">
<div className="container">
<Switch>
<Route exact path={path} component={Details} />
<Route path={`${path}/update`} component={Update} />
</Switch>
</div>
</div>
);
}
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 (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ errors, touched, isSubmitting }) => (
<Form>
<h1>Update Profile</h1>
<div className="form-row">
<div className="form-group col">
<label>Title</label>
<Field name="title" as="select" className={'form-control' + (errors.title && touched.title ? ' is-invalid' : '')}>
<option value=""></option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Miss">Miss</option>
<option value="Ms">Ms</option>
</Field>
<ErrorMessage name="title" component="div" className="invalid-feedback" />
</div>
<div className="form-group col-5">
<label>First Name</label>
<Field name="firstName" type="text" className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />
<ErrorMessage name="firstName" component="div" className="invalid-feedback" />
</div>
<div className="form-group col-5">
<label>Last Name</label>
<Field name="lastName" type="text" className={'form-control' + (errors.lastName && touched.lastName ? ' is-invalid' : '')} />
<ErrorMessage name="lastName" component="div" className="invalid-feedback" />
</div>
</div>
<div className="form-group">
<label>Email</label>
<Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
<ErrorMessage name="email" component="div" className="invalid-feedback" />
</div>
<h3 className="pt-3">Change Password</h3>
<p>Leave blank to keep the same password</p>
<div className="form-row">
<div className="form-group col">
<label>Password</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-group col">
<label>Confirm Password</label>
<Field name="confirmPassword" type="password" className={'form-control' + (errors.confirmPassword && touched.confirmPassword ? ' is-invalid' : '')} />
<ErrorMessage name="confirmPassword" component="div" className="invalid-feedback" />
</div>
</div>
<div className="form-group">
<button type="submit" disabled={isSubmitting} className="btn btn-primary mr-2">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Update
</button>
<button type="button" onClick={() => onDelete()} className="btn btn-danger" style={{ width: '75px' }} disabled={isDeleting}>
{isDeleting
? <span className="spinner-border spinner-border-sm"></span>
: <span>Delete</span>
}
</button>
<Link to="." className="btn btn-link">Cancel</Link>
</div>
</Form>
)}
</Formik>
)
}
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'
})
}
}
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
SYMBOL INDEX (51 symbols across 24 files)
FILE: src/_components/Alert.jsx
function Alert (line 17) | function Alert({ id, fade }) {
FILE: src/_components/Nav.jsx
function Nav (line 7) | function Nav() {
function AdminNav (line 35) | function AdminNav({ match }) {
FILE: src/_components/PrivateRoute.jsx
function PrivateRoute (line 6) | function PrivateRoute({ component: Component, roles, ...rest }) {
FILE: src/_helpers/fake-backend.js
function configureFakeBackend (line 8) | function configureFakeBackend() {
FILE: src/_helpers/fetch-wrapper.js
function get (line 11) | function get(url) {
function post (line 19) | function post(url, body) {
function put (line 29) | function put(url, body) {
function _delete (line 39) | function _delete(url) {
function authHeader (line 49) | function authHeader(url) {
function handleResponse (line 61) | function handleResponse(response) {
FILE: src/_services/account.service.js
method userValue (line 24) | get userValue () { return userSubject.value }
function login (line 27) | function login(email, password) {
function logout (line 37) | function logout() {
function refreshToken (line 45) | function refreshToken() {
function register (line 55) | function register(params) {
function verifyEmail (line 59) | function verifyEmail(token) {
function forgotPassword (line 63) | function forgotPassword(email) {
function validateResetToken (line 67) | function validateResetToken(token) {
function resetPassword (line 71) | function resetPassword({ token, password, confirmPassword }) {
function getAll (line 75) | function getAll() {
function getById (line 79) | function getById(id) {
function create (line 83) | function create(params) {
function update (line 87) | function update(id, params) {
function _delete (line 101) | function _delete(id) {
function startRefreshTokenTimer (line 116) | function startRefreshTokenTimer() {
function stopRefreshTokenTimer (line 126) | function stopRefreshTokenTimer() {
FILE: src/_services/alert.service.js
function onAlert (line 25) | function onAlert(id = defaultId) {
function success (line 30) | function success(message, options) {
function error (line 34) | function error(message, options) {
function info (line 38) | function info(message, options) {
function warn (line 42) | function warn(message, options) {
function alert (line 47) | function alert(alert) {
function clear (line 54) | function clear(id = defaultId) {
FILE: src/account/ForgotPassword.jsx
function ForgotPassword (line 8) | function ForgotPassword() {
FILE: src/account/Index.jsx
function Account (line 12) | function Account({ history, match }) {
FILE: src/account/Login.jsx
function Login (line 8) | function Login({ history, location }) {
FILE: src/account/Register.jsx
function Register (line 8) | function Register({ history }) {
FILE: src/account/ResetPassword.jsx
function ResetPassword (line 9) | function ResetPassword({ history }) {
FILE: src/account/VerifyEmail.jsx
function VerifyEmail (line 7) | function VerifyEmail({ history }) {
FILE: src/admin/Index.jsx
function Admin (line 7) | function Admin({ match }) {
FILE: src/admin/Overview.jsx
function Overview (line 4) | function Overview({ match }) {
FILE: src/admin/users/AddEdit.jsx
function AddEdit (line 8) | function AddEdit({ history, match }) {
FILE: src/admin/users/Index.jsx
function Users (line 7) | function Users({ match }) {
FILE: src/admin/users/List.jsx
function List (line 6) | function List({ match }) {
FILE: src/app/Index.jsx
function App (line 12) | function App() {
FILE: src/home/Index.jsx
function Home (line 5) | function Home() {
FILE: src/index.jsx
function startApp (line 18) | function startApp() {
FILE: src/profile/Details.jsx
function Details (line 6) | function Details({ match }) {
FILE: src/profile/Index.jsx
function Profile (line 7) | function Profile({ match }) {
FILE: src/profile/Update.jsx
function Update (line 8) | function Update({ history }) {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
{
"path": ".babelrc",
"chars": 85,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\",\n \"@babel/preset-env\"\n ]\n}"
},
{
"path": ".gitignore",
"chars": 605,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2019 Jason Watmore\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 263,
"preview": "# react-signup-verification-boilerplate\n\nReact - Email Sign Up with Verification, Authentication & Forgot Password\n\nFor "
},
{
"path": "package.json",
"chars": 1138,
"preview": "{\n \"name\": \"react-signup-verification-boilerplate\",\n \"version\": \"1.0.0\",\n \"repository\": {\n \"type\": \"git\""
},
{
"path": "src/_components/Alert.jsx",
"chars": 3799,
"preview": "import React, { useState, useEffect } from 'react';\r\nimport PropTypes from 'prop-types';\r\n\r\nimport { alertService, Alert"
},
{
"path": "src/_components/Nav.jsx",
"chars": 1549,
"preview": "import React, { useState, useEffect } from 'react';\r\nimport { NavLink, Route } from 'react-router-dom';\r\n\r\nimport { Role"
},
{
"path": "src/_components/PrivateRoute.jsx",
"chars": 938,
"preview": "import React from 'react';\r\nimport { Route, Redirect } from 'react-router-dom';\r\n\r\nimport { accountService } from '@/_se"
},
{
"path": "src/_components/index.js",
"chars": 80,
"preview": "export * from './Alert';\nexport * from './Nav';\nexport * from './PrivateRoute';\n"
},
{
"path": "src/_helpers/fake-backend.js",
"chars": 16351,
"preview": "import { Role } from './'\nimport { alertService } from '@/_services';\n\n// array in local storage for registered users\nco"
},
{
"path": "src/_helpers/fetch-wrapper.js",
"chars": 2113,
"preview": "import config from 'config';\nimport { accountService } from '@/_services';\n\nexport const fetchWrapper = {\n get,\n p"
},
{
"path": "src/_helpers/history.js",
"chars": 95,
"preview": "import { createBrowserHistory } from 'history';\n\nexport const history = createBrowserHistory();"
},
{
"path": "src/_helpers/index.js",
"chars": 116,
"preview": "export * from './fake-backend';\nexport * from './fetch-wrapper';\nexport * from './history';\nexport * from './role';\n"
},
{
"path": "src/_helpers/role.js",
"chars": 64,
"preview": "export const Role = {\n Admin: 'Admin',\n User: 'User' \n}"
},
{
"path": "src/_services/account.service.js",
"chars": 3600,
"preview": "import { BehaviorSubject } from 'rxjs';\n\nimport config from 'config';\nimport { fetchWrapper, history } from '@/_helpers'"
},
{
"path": "src/_services/alert.service.js",
"chars": 1221,
"preview": "import { Subject } from 'rxjs';\nimport { filter } from 'rxjs/operators';\n\nconst alertSubject = new Subject();\nconst defa"
},
{
"path": "src/_services/index.js",
"chars": 68,
"preview": "export * from './account.service';\nexport * from './alert.service';\n"
},
{
"path": "src/account/ForgotPassword.jsx",
"chars": 2210,
"preview": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'f"
},
{
"path": "src/account/Index.jsx",
"chars": 1411,
"preview": "import React, { useEffect } from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { accountService } f"
},
{
"path": "src/account/Login.jsx",
"chars": 2991,
"preview": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'f"
},
{
"path": "src/account/Register.jsx",
"chars": 6199,
"preview": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMessage } from 'f"
},
{
"path": "src/account/ResetPassword.jsx",
"chars": 4483,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport queryString from 'qu"
},
{
"path": "src/account/VerifyEmail.jsx",
"chars": 1481,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport queryString from 'qu"
},
{
"path": "src/admin/Index.jsx",
"chars": 543,
"preview": "import React from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { Overview } from './Overview';\nimp"
},
{
"path": "src/admin/Overview.jsx",
"chars": 366,
"preview": "import React from 'react';\nimport { Link } from 'react-router-dom';\n\nfunction Overview({ match }) {\n const { path } ="
},
{
"path": "src/admin/users/AddEdit.jsx",
"chars": 7921,
"preview": "import React, { useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMe"
},
{
"path": "src/admin/users/Index.jsx",
"chars": 471,
"preview": "import React from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { List } from './List';\nimport { Ad"
},
{
"path": "src/admin/users/List.jsx",
"chars": 2671,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { accountService } "
},
{
"path": "src/app/Index.jsx",
"chars": 1265,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Route, Switch, Redirect, useLocation } from 'react-router-d"
},
{
"path": "src/home/Index.jsx",
"chars": 389,
"preview": "import React from 'react';\n\nimport { accountService } from '@/_services';\n\nfunction Home() {\n const user = accountSer"
},
{
"path": "src/index.html",
"chars": 797,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <base href=\"/\" />\n <title>React - Email Sign U"
},
{
"path": "src/index.jsx",
"chars": 597,
"preview": "import React from 'react';\nimport { Router } from 'react-router-dom';\nimport { render } from 'react-dom';\n\nimport { hist"
},
{
"path": "src/profile/Details.jsx",
"chars": 569,
"preview": "import React from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { accountService } from '@/_services';\n\nfunc"
},
{
"path": "src/profile/Index.jsx",
"chars": 552,
"preview": "import React from 'react';\nimport { Route, Switch } from 'react-router-dom';\n\nimport { Details } from './Details';\nimpor"
},
{
"path": "src/profile/Update.jsx",
"chars": 6183,
"preview": "import React, { useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { Formik, Field, Form, ErrorMes"
},
{
"path": "src/styles.less",
"chars": 207,
"preview": "// global styles\na { cursor: pointer; }\n\n.app-container {\n min-height: 320px;\n}\n\n.admin-nav {\n padding-top: 0;\n "
},
{
"path": "webpack.config.js",
"chars": 986,
"preview": "var HtmlWebpackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = {\n mode: 'dev"
}
]
About this extraction
This page contains the full source code of the cornflourblue/react-signup-verification-boilerplate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (73.7 KB), approximately 15.9k tokens, and a symbol index with 51 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.