Introduction
This article is the seventh part of a 10 part series that I posted as a tutorial for the readers. I will go through step-by-step going over how I created a link shortener app from scratch.
In this post, we will finally allow our app to allow for user level activity. This includes using passport.js to support user session, and we will also fill the dashboard with the data of our user, and allow users to modify their account.
Installing dependencies
In order to allow for user sessions, we will use a wellknown npm package called passport.js. The library makes managing user sessions easy and it also have support for social media log in.
For our purpose, we will only store user’s email and password and we will not use any user’s social media.
Moreover we wil also install a library called bcrypt which allow us to encrypt user password before saving it in our database. That way, we ensure our user’s data is secure even if our server is comprimised.
We can install our dependencies by running the following:
npm i bcrypt passport-local passport express-sessionWrite passport-config.js
As mentioned, passport has multiple authentication strategy that it can support, however, for us we will use the local strategy which is where we store the user email and password in our local database, additionally we can also add social media strategy like linkedin, facebook, and google login. See all strategies here
We can add the authentication method by initializing the passport object which is why we will create passport-config.js in the packages folder , which include the following code:
require('dotenv').config();
const localStrategy = require('passport-local').Strategy // using email and password
const bcrypt = require('bcrypt')
/**
* initialization for passport library for login, and logout
*/
function initialize(passport, getUserByEmail, getUserById) {
const authenticateUser = async (email, password, done) => {
await getUserByEmail(email, async (err, user) => {
try {
if (user.length === 0) {
return done(null, null, { message: 'user_not_found' })
} else {
let possibleUsers = []
for (let i = 0; i < user.length; i++) {
if (await bcrypt.compare(password, user[i].hashed_password)) {
possibleUsers.push(user[i])
}
}
if (possibleUsers.length) {
return done(null, possibleUsers)
} else {
return done(null, null, { message: 'incorrect_password' })
}
}
} catch (e) {
return done(e, null, { message: 'internal_error' })
}
})
}
passport.use(new localStrategy({ usernameField: 'email' }, authenticateUser))
passport.serializeUser((user, done) => done(null, user.id))
passport.deserializeUser((id, done) => getUserById(id, (err, result) => {
done(err, result)
}))
}
module.exports = initializeadd getUserById method
In the passport strategy, we will also need a method where we can find the user by the ID, this is called a serializer, and hence we added getUserById in the db.js object
/**
* getUserById - get user by id
* @param {id} id
* @param {function} cb
*/
getUserById(id, cb) {
let query = `SELECT user.*, pw.hashed_password
FROM user
LEFT JOIN pw
ON user.id = pw.user_id
WHERE user.id='${id}'`
this.con.query(query, cb);
}Write router auth.js
Now that we have set up our passport configurations, we will now add a new router to our app that allow us to better organize our code.
we will createy auth.js in the router folder which will include all our interaction with the user authentication such as their login, register, resetting password, and modifying their account.
The base structure of the file will be as the following:
require('dotenv').config();
var bcrypt = require('bcrypt');
const {
getCurrentTime
} = require('../packages/util')
let db = null
const databaseHandler = require('../packages/db/db');
db = new databaseHandler();
const sqlConf = {
"host": process.env.MY_SQL_HOST || "CHANGE YOUR HOST IN .env",
"user": process.env.MY_SQL_USER || "CHANGE YOUR USER IN .env",
"password": process.env.MY_SQL_PWD || "CHANGE YOUR PW IN .env",
connectTimeout: 30000,
charset: 'utf8mb4'
}
db.start(sqlConf, () => {
console.log("SUCCESS CONNECT TO DB")
})
const passport = require('passport');
const initializePassport = require("../packages/passport-config")
initializePassport(
passport,
(email, cb) => db.getUserByEmailWithPw(email, cb),
(id, cb) => db.getUserById(id, cb)
)
var express = require('express');
var router = express.Router()
// NEED TO ADD ROUTES HERE
module.exports = routerLogin
For the login routes, we will call passport.authenticate and passing local as a parameter to specify our strategy to be used.
router.post('/login', function (req, res, next) {
passport.authenticate('local', function (err, users, info) {
if (err) {
return next(err);
} else if (!users) {
return res.redirect(`/login/?error=${info.message}`)
} else {
const user = users[0]
return req.login(user, loginErr => {
if (loginErr) {
return next(loginErr);
} else {
db.modify("user", {
last_seen: getCurrentTime()
}, "id", user.id, () => {
res.redirect(`/`)
})
}
});
}
})(req, res, next);
});Register
In this part, we will just need to do a quick check to make sure the user is not yet exist in the database. The code is the following:
router.post('/register', function (req, res, next) {
db.getXbyY("user", "email", req.body.email, (err, result) => {
if (result.length != 0) {
res.redirect(`/login`)
} else {
let newUser = {}
newUser.first_name = req.body.first_name
newUser.last_name = req.body.last_name
newUser.email = req.body.email
newUser.created_at = getCurrentTime()
db.add("user", newUser, async (err, result) => {
if (err) {
console.log(err)
res.redirect("/error")
} else {
db.getXbyY("user", "email", newUser.email, async (err, result) => {
if (err) {
console.log(err)
} else {
let hashedPassword = await bcrypt.hash(req.body.password, 10)
let userPw = {}
userPw.user_id = result[0].id
userPw.hashed_password = hashedPassword
db.add("pw", userPw, (err, result) => {
if (err) {
res.redirect("/error")
} else {
res.redirect(`/login`)
}
})
}
})
}
})
}
})
})Reset password
In this part, we will need to make sure the current password given by the user, matches the currently saved password, before overwriting their existing password.
router.post('/reset-password', async function (req, res, next) {
if (!req.user) {
res.redirect('/error')
} else {
let userId = req.user[0].id
db.getXbyY("pw", "user_id", userId, async (err, result) => {
if (await bcrypt.compare(req.body.cur_pw, result[0].hashed_password)) {
let hashedPassword = await bcrypt.hash(req.body.new_pw, 10)
db.modify("pw", {
hashed_password: hashedPassword
}, "user_id", userId, async (err, result) => {
if (err) {
console.log(err)
res.redirect("/error")
} else {
res.redirect(`/setting`)
}
})
} else {
res.redirect("/error")
}
})
}
})Modify account
router.post('/modify', async function (req, res, next) {
if (!req.user) {
res.redirect('/error')
} else {
let userId = req.user[0].id
let newUserData = { ...req.body, last_seen: getCurrentTime() }
db.modify("user", newUserData, "id", userId, async (err, result) => {
if (err) {
res.redirect("/error")
} else {
res.redirect(`/setting`)
}
})
}
})Modify app.js
Now that we created a router, we need to ofcourse import it in our main app, and we need to add several things in our main app to be able to support user sessions.
First, lets add the router by adding the following:
app.use('/auth', require("./router/auth"))Setting up user session
To set up user session, we need to add firstly a SESSION_SECRET in our .env. Here is to know more about what session secret do.
We can do so by adding the following line in the .env, (This can be any arbitrary string)
SESSION_SECRET=SESSION_SECRET
Next, we can finally set up our session by adding this in our app.js
const session = require('express-session');
const passport = require('passport');
// FOR SESSION
app.use(
session({
secret: process.env.SESSION_SECRET
? process.env.SESSION_SECRET
: "secret",
resave: false,
saveUninitialized: false,
cookie: { expires: new Date(253402300000000) }
})
)
app.use(passport.initialize())
app.use(passport.session())
app.use('/auth', require("./router/auth"))Set permission
While we are on app.js, now would be a good time for us to set up permission on some of the pages to make sure only logged in user will be able to see it. and we can pass the user id to the front end. So our root path, will become the following:
app.get('/', function (req, res) {
if (!req.user) {
res.redirect("/login")
} else {
let userId = req.user[0].id
res.render(__dirname + '/public/pages/home.html', {
userId
})
}
})
app.get('/login', function (req, res) {
if (req.user) {
res.redirect("/")
} else {
res.render(__dirname + '/public/pages/auth/login.html')
}
})
app.get('/forgot-password', function (req, res) {
res.render(__dirname + '/public/pages/auth/forgot-password.html')
})
app.get('/register', function (req, res) {
if (req.user) {
res.redirect("/")
} else {
res.render(__dirname + '/public/pages/auth/register.html')
}
})
app.get('/setting', function (req, res) {
if (!req.user) {
res.redirect("/login")
} else {
res.render(__dirname + '/public/pages/setting.html', {
...req.user[0]
})
}
})
app.get('/error', function (req, res) {
res.render(__dirname + '/public/pages/error.html')
})
app.get('/logout', function (req, res) {
req.logout(function (err) {
if (err) { return next(err); }
res.redirect('/login');
});
})Modify front-end
Now that our back end is set up and ready, we will change our HTML pages to actually view the user’s data and let user login and register.
Login form
We need to change the login form
<form class="pt-3" method="POST" action="/auth/login">
<div class="form-group">
<input name="email" type="email" class="form-control form-control-lg" id="email"
placeholder="Email">
</div>
<div class="form-group">
<input name="password" type="password" class="form-control form-control-lg"
name="password" placeholder="Password">
</div>
<div class="mt-3">
<button type="submit"
class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn">SIGN
IN</button>
</div>
<div class="text-center mt-4 font-weight-light">
Don't have an account? <a href="/register" class="text-primary">Create</a>
</div>
<!--<div class="text-center mt-4 font-weight-light">
<a href="/forgot-password" class="auth-link text-black">Forgot password?</a>
</div> -->
</form>Register page
And ofcourse we will need to change the register page as well.
<form class="pt-3" method="POST" action="/auth/register">
<div class="form-group">
<input type="text" name="first_name" class="form-control form-control-lg" placeholder="First Name">
</div>
<div class="form-group">
<input type="text" name="last_name" class="form-control form-control-lg" placeholder="Last Name">
</div>
<div class="form-group">
<input type="email" name="email" class="form-control form-control-lg" placeholder="Email">
</div>
<div class="form-group">
<input type="password" name="password" class="form-control form-control-lg" placeholder="Password">
</div>
<div class="mb-4">
<div class="form-check">
<label class="form-check-label text-muted">
<input type="checkbox" class="form-check-input" required>
I agree to all Terms & Conditions
</label>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn">SIGN
UP</button>
</div>
<div class="text-center mt-4 font-weight-light">
Already have an account? <a href="/login" class="text-primary">Login</a>
</div>
</form>Home page
First of all, we will remove the tbody that we added initially for our sample data. and the idea is we can populate it with the users links by calling our backend API.
We can do so by adding these scripts
<script>
const userId = "<%=userId%>"
fetch("/api/user/links").then((res) => {
return res.json()
}).then((res) => {
res.result.forEach(element => {
document.querySelector("#all_links").innerHTML += `
<tr>
<td>${element.id}</td>
<td>
<a target="__blank" href="/${element.source_id}">
link.kecil.com/${element.source_id}
</a>
</td>
<td>
<a target="__blank" href="${element.target_url}">
${element.target_url.length > 40 ? element.target_url.slice(0, 35) + "..." : element.target_url}
</a>
</td>
<td>
<label onclick="deleteLink('${element.id}')" class="badge badge-danger">Delete</label>
</td>
<form id="delete-link-${element.id}" action="/api/link/delete/${element.id}" method="POST" style="display:none;">
</form>
</tr>
`
});
})
</script>Lastly, we will create a deleteLink function which will be called when the delete button for a link is pressed.
<script>
function deleteLink(linkId) {
document.querySelector(`#delete-link-${linkId}`).submit()
}
</script>Setting page
Lastly, we will simply modify the setting form with adding action="/auth/modify" method="POST"
<form class="forms-sample" action="/auth/modify" method="POST">
<div class="form-group">
<label for="exampleInputName1">First Name</label>
<input type="text" value="<%=first_name%>" class="form-control" id="first_name"
name="first_name" placeholder="First Name" required>
</div>
<div class="form-group">
<label for="exampleInputName1">Last Name</label>
<input type="text" value="<%=last_name%>" class="form-control" id="last_name"
name="last_name" placeholder="Last Name" required>
</div>
<div class="form-group">
<label for="exampleInputEmail3">Email address</label>
<input type="email" value="<%=email%>" class="form-control" id="email"
name="email" placeholder="Email" disabled>
</div>
<button type="submit" class="btn btn-primary mr-2">Submit</button>
</form>and similarly for the reset password form:
<form class="forms-sample" action="/auth/reset-password" method="POST">
<div class="form-group">
<label for="exampleInputName1">Current Password</label>
<input type="password" class="form-control" id="cur_pw" name="cur_pw"
placeholder="Current Password">
</div>
<div class="form-group">
<label for="exampleInputName1">New Password</label>
<input type="password" class="form-control" id="new_pw" name="new_pw"
placeholder="New Password">
</div>
<button type="submit" class="btn btn-primary mr-2">Save</button>
</form>Conclusion
As we finish this part, your app is already functional and ready to made live as is!😊 If you are stuck with anything, feel free to visit the code in here. In the next part we will really make our app our own by modifying the design and adding the logo for our own, we will take Let us now go to the next part 😊