Let’s set up our directories

mkdir email_auth_tutorial && cd email_auth_tutorial
npx create-react-app email_auth_tutorial_frontend --template typescript
mkdir email_auth_tutorial_backend

Frontend

cd email_auth_tutorial_frontend && npm start
npm i --save react-router-dom
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import React from 'react';
import './App.css';
import { Link } from 'react-router-dom';
function App() {
return (
<div>
<div>
<div>
<Link to="/login">Log in</Link>
</div>
<div>
<Link to="/signup">Sign up</Link>
</div>
<div>
You are not logged in.
</div>
</div>
</div>
);
}
export default App;
import React from 'react';function Login() {
return (
<div>
log in
</div>
);
}
export default Login;
import React from 'react';function Signup() {
return (
<div>
sign up
</div>
);
}
export default Signup;
import React from 'react';function CheckYourEmail() {
return (
<div>
Please check your email for a verification link.
</div>
);
}
export default CheckYourEmail;
import React from 'react';function VerifyEmail() {
return (
<div>
Verifying...
</div>
);
}
export default VerifyEmail;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Login from './Login';
import Signup from './Signup';
import CheckYourEmail from './CheckYourEmail';
import VerifyEmail from './VerifyEmail';
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/check-your-email" element={<CheckYourEmail />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Sign up interface:

import React from "react";
import { Link, useNavigate } from "react-router-dom";
function SignUp() {const navigate = useNavigate();const [formState, setFormState] = React.useState({
username: "",
email: "",
password: ""
})
const onChange = (e: React.ChangeEvent<HTMLInputElement>, type: string) => {
e.preventDefault();
setFormState({
...formState,
[type]: e.target.value
})
}
const getErrors = (): string[] => {
const errors = [];
if (!formState.username) errors.push("Name required");
if (!formState.email) {
errors.push("Email required");
} else if (!/^\S+@\S+\.\S+$/.test(formState.email)) {
errors.push("Invalid email");
}
if (!formState.password) errors.push("Password required");
return errors;
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errors = getErrors();
for (let error of errors) {
alert(error);
}
if (errors.length) return;
const response = await fetch('http://localhost:8000/api/v1/signup', {
method: "post",
headers: {
// needed so express parser says the body is OK to read
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: formState.username,
email: formState.email,
password: formState.password
})
})
if (response.status !== 200) {
// TODO: Add more detailed error handling.
return alert("Something went wrong.");
}
navigate("/check-your-email");
}
return (
<div>
<h1>Sign up</h1>
<form onSubmit={onSubmit}>
<div>
<label>Name</label>
<input onChange={(e) => onChange(e, "username")} type="text" value={formState.username} />
</div>
<div>
<label>Email</label>
<input onChange={(e) => onChange(e, "email")} type="email" value={formState.email} />
</div>
<div>
<label>Password</label>
<input onChange={(e) => onChange(e, "password")} type="password" value={formState.password} />
</div>
<button type="submit">Submit</button>
</form>
<div>
<Link to="/login">Already have an account? Log in</Link>
</div>
<div>
<Link to="/">Back to home</Link>
</div>
</div>
);
}
export default SignUp;
import React from "react";
import { Link, useNavigate } from "react-router-dom";
function Login() {const navigate = useNavigate();const [formState, setFormState] = React.useState({
email: "",
password: ""
})

const onChange = (e: React.ChangeEvent<HTMLInputElement>, type: string) => {
e.preventDefault();
setFormState({
...formState,
[type]: e.target.value
})
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const response = await fetch('http://localhost:8000/api/v1/login', {
method: "post",
credentials: "include",
headers: {
// needed so express parser says OK to read
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: formState.email,
email: formState.email,
password: formState.password
})
})
if (response.status !== 200) {
return alert("Something went wrong");
}
navigate("/");
}
return (
<div>
<h1>Log in</h1>
<form onSubmit={onSubmit}>
<div>
<label>Email</label>
<input onChange={(e) => onChange(e, "email")} type="email" value={formState.email} />
</div>
<div>
<label>Password</label>
<input onChange={(e) => onChange(e, "password")} type="password" value={formState.password} />
</div>
<button>Submit</button>
</form>
<div>
<Link to="/signup">Don't have an account? Sign up</Link>
</div>
<div>
<Link to="/">Back to home</Link>
</div>
</div>
);
}
export default Login;
import React from "react";
import { Link } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
enum VerificationState {
pending = "pending",
invalid = "invalid",
already_verified = "already_verified",
success = "success"
}
function VerifyEmail() {const [searchParams] = useSearchParams();const [verificationState, setVerificationState] = React.useState(VerificationState.pending);const attemptToVerify = async () => {
const code = searchParams.get("code");
const email = searchParams.get("email");
if (!code || !email) {
return setVerificationState(VerificationState.invalid)
}
const response = await fetch('http://localhost:8000/api/v1/verify-email', {
method: "post",
headers: {
// needed so express parser says OK to read
'Content-Type': 'application/json'
},
body: JSON.stringify({
code,
email
})
})
const result = await response.json();
if (result.data === "already verified") {
return setVerificationState(VerificationState.already_verified)
}
setVerificationState(VerificationState.success);
}

React.useEffect(() => {
attemptToVerify()
}, []);
switch (verificationState) {
case VerificationState.success: {
return (
<div>
Email verified. Please <Link to="/login">sign in</Link>.
</div>
)
}
case VerificationState.already_verified: {
return (
<div>
This link is expired. Please <Link to="/login">log in</Link>.
</div>
)
}
case VerificationState.invalid: {
return (
<div>
This link is invalid link.
</div>
)
}
default: {
return (
<div>
Verifying...
</div>
);
}
}
}export default VerifyEmail;
import React from "react";
import { Link } from "react-router-dom";
import './App.css';
function App() {const [user, setUser] = React.useState(null);React.useEffect(() => {
fetch('http://localhost:8000/api/v1/user', {
method: "get",
credentials: "include",
headers: {
// needed so express parser says OK to read
'Content-Type': 'application/json'
},
})
.then(response => response.json())
.then(result => {
console.log(result, 'result')
if (result.data) {
setUser(result.data)
}
});
}, []);
const logout = async () => {
const response = await fetch('http://localhost:8000/api/v1/logout', {
method: "get",
credentials: "include",
headers: {
// needed so express parser says OK to read
'Content-Type': 'application/json'
},
})
const result = await response.json();
if (result.data) {
setUser(null)
}
}
return (
<div className="App">
{
!user && (
<div>
<div>
<Link to="/login">Log in</Link>
</div>
<div>
<Link to="/signup">Sign up</Link>
</div>
<p>Not logged in</p>
</div>
)
}
{
user && <div>{user}<button onClick={logout}>logout</button></div>
}
</div>
);
}
export default App;

Back end

npm init -y
touch index.js
npm i --save express
const express = require('express');
const app = express();
const port = 8000;app.listen(port, () => console.log('App listening on port ' + port));
node index.js

Routes

var express = require('express');
var router = express.Router();
router.route('/ping')
.get((req, res) => {
res.status(200).send({
data: "pong"
})
});
module.exports = router;
const express = require('express');
const routes = require("./routes");
const app = express();app.use('/api/v1/', routes);const port = 8000;app.listen(port, () => console.log('App listening on port ' + port));

Database

brew services start mongodb-community@5.0
"C:\Program Files\MongoDB\Server\5.0\bin\mongod.exe" --dbpath="c:\data\db"
mongo
use SampleDatabase;
npm i --save body-parser cors express-session mongoose nodemailer passport passport-local passport-local-mongoose passport-mongoose
/*  EXPRESS SETUP  */const express = require('express');
const app = express();
app.use(express.static(__dirname));const session = require('express-session');
const mongoose = require('mongoose');
const passport = require('passport');
// helps parse the body so the server can understand JSON.
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// sets up session management
const expressSession = session({
secret: 'this_is_a_secret_that_should_not_be_disclosed',
resave: false,
saveUninitialized: false,
});
app.use(expressSession);
// Lots of behind-the-scenes configurations
// happen here to reduce the amount of code
// you have to write in order for express and passport to communicate with each other.
app.use(passport.initialize());
app.use(passport.session());
// cross-origin resource sharing set up
var cors = require('cors');
var corsOption = {
origin: true,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
exposedHeaders: ['x-auth-token', "Access-Control-Allow-Credentials"]
};
app.use(cors(corsOption));
const port = 8000;
app.listen(port, () => console.log('App listening on port ' + port));
const routes = require("./routes");// database setup
mongoose.connect("mongodb://localhost/SampleDatabase",
{ useNewUrlParser: true, useUnifiedTopology: true });
app.use('/api/v1/', routes);

Bind MongoDB with Passport

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
const passport = require('passport');
const PassportLocalStrategy = require('passport-local');
const UserDetail = new mongoose.Schema({
username: String,
email: String,
password: String,
emailVerified: Boolean,
emailVerificationHash: String
});
UserDetail.plugin(passportLocalMongoose);const UserDetails = mongoose.model('user', UserDetail, 'user');passport.use(new PassportLocalStrategy(
function(username, password, done) {
UserDetails.findOne({ email: username }, function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
user.authenticate(password, function(err,model,passwordError){
if(passwordError){
console.log(err)
done(null, false);
} else if(model) {
done(null, user);
}
})
});
}
));
passport.serializeUser(function(user, done) {
console.log('serializing user: ');
console.log(user);
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
UserDetails.findById(id, function(err, user) {
done(err, user);
});
});
module.exports = { UserDetails };

You’ve got mail

mkdir services && touch services/mail.service.js
const nodemailer = require("nodemailer");const sendMail = async ({
subject,
to,
html,
text
}) => {
let transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: '<YOUR_GMAIL_USERNAME<@gmail.com',
pass: '<YOUR_PASSWORD_DONT_SAVE_THIS_TO_GITHUB>' // naturally, replace both with your real credentials or an application-specific password
}
});
return await transporter.sendMail({
from: '"The Demo App Team" <foo@example.com>', // sender address
to, // list of receivers
subject, // Subject line
text, // plain text body
html, // html body
});
}
module.exports = {
sendMail
}

Authentication endpoints

var express = require('express');
var router = express.Router();
const { UserDetails } = require("./user");
const { sendMail } = require('./services/mail.service');
const passport = require('passport');
const generateHash = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
const sendVerificationEmail = async ({
to,
verificationHash,
email
}) => {
const linkAddress = `https://localhost:3000/verify-email?code=${verificationHash}&email=${email}`
return await sendMail({
to,
subject: "Please verify your email",
text: `Hi John,
\nPlease click on the following link to verify your email:
\n${linkAddress}
\n
\nSincerely,
The Demo App Team`,
html: `Hi John,
<br>Please click on the following link to verify your email:
<br><a href="${linkAddress}">${linkAddress}</a>
<br><br>Sincerely,
<br>The Demo App Team`
});
}
router.route('/ping')
.get((req, res) => {
res.send({
data: "pong"
})
});
router.route('/verify-email')
.post(async (req, res) => {
UserDetails.findOne({
email: req.body.email,
emailVerificationHash: req.body.code
}, (err, account) => {
if (err) {
console.log(err);
return res.status(400).send({ error: err.message });
}
if (account.emailVerified) {
return res.status(200).send({data: "already verified"});
}
UserDetails.updateOne({_id: account.id}, {
$set: {
emailVerified: true
}
}, (_err) => {
if (_err) {
console.log(_err);
return res.status(400).send({ error: _err.message });
}
return res.status(200).send({data: "done"})
})

})
});
function checkAuthentication(req,res,next){
if(req.isAuthenticated()) {
next();
} else{
res.status(403).send({reason: "unauthenticated"});
}
}
router.get('/user', checkAuthentication, (req, res) => {
console.log(req.session, 'sesion', req.user)
res.status(200).send({data: req.user.email})
})
router.route('/logout')
.get((req, res) => {
req.logout()
res.status(200).send({data: "OK"})
})
router.route('/login')
.post((req, res, next) => {
passport.authenticate('local',
(err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(400).send({data: "no user"})
}
req.logIn(user, function(err) {
if (err) {
return res.status(400).send({data: "error"})
}
return res.status(200).send({data: "ok"})
});
})(req, res, next);
})
router.route('/signup')
.post((req, res) => {
const emailVerificationHash = generateHash();
UserDetails.register({
username: req.body.username,
email: req.body.email,
emailVerified: false,
emailVerificationHash
}, req.body.password, async (err, account) => {
if (err) {
console.log(err);
return res.status(400).send({ error: err.message });
}
await sendVerificationEmail({
to: req.body.email,
verificationHash: emailVerificationHash,
email: req.body.email
})
res.status(200).send({ data: "ok" });
})
});
module.exports = router;

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store