DEV Community

Sodiq_dev
Sodiq_dev

Posted on

Building a User Login System with Express js and Fauna

In this article, I’ll show you how to build a user login system with the node framework express.js and Fauna.

What is Fauna?

Fauna is a global cloud database created to integrate with the Jamstack and modern serverless architecture. Fauna is a flexible, developer-friendly, transactional database delivered as a secure and scalable cloud API with native GraphQL.

Fauna is a NoSQL serverless database, so you don’t have to worry about database provisioning, scaling, sharding, replication, or correctness.

Let’s dive right into building our user login system!

Prerequisites

To take full advantage of this article, you need to have the following installed on your laptop.

  1. Node.js
  2. Have access to one package manager such as npm or yarn
  3. Access to Fauna dashboard
  4. Have a basic knowledge of Node.js, Express, and Handlebars.js or a view engine.

About the App

In this app, we will have four routes:

  • Signup Route: In this route, a new user is created using necessary credentials, e.g. email, username, and password, and then the user is logged into their account and shown their dashboard page.
  • Sign In Route: In this route, the user logs in by providing sign-up details. If successful, the user is shown their dashboard page, not if not. The user is shown the providing with necessary error message depending on what caused the sign-in to be unsuccessful.
  • Dashboard Route: In this route, after a successful sign-up or sign-in, the user is shown a customised dashboard page welcoming the user to their page.
  • Sign Out Route: This is the route to sign a user out of their account.
  • Delete Account Route: In our app, a user is allowed to delete an account created. If successful, the user’s account is deleted from our Fauna database.
  • Confirm Token Route: This route allows users to confirm their email address before successfully redirecting to the dashboard page.

Before we create our routes, we need to create our Fauna database that we’ll use for the app following the steps below.

Step 1: Set Up Our Fauna Database

To get started with our app, we need to create our database for the app in the Fauna dashboard.

You can create a Fauna account here.

In your dashboard, click on the ”Create Database” button, provide a name for your database, and click create.

create database

Step 2: Generating your Fauna API key

The Fauna secret key connects fauna to an application or script, unique to a database.

We need to create a Fauna API key to connect the Fauna database to our app. To do this, go to the security settings on the left side of the screen.

fauna API key

Generate key

When you click on save in the last image, it will generate a new API key for you. Copy the API key and keep the key somewhere safe as you can’t have access to that key in the dashboard again

Step 3: Creating a Fauna collection

We need to create a Fauna collection that we will use within our code.

A collection is simply a grouping of documents(rows) with the same or a similar purpose. A collection acts similarly to a table in a traditional SQL database.

In our app, we will have only a collection for users. The user collection is where we will store our user data.

To create the collection, click on the database you created, click on “New Collection”, enter your chosen collection name then click save.

You can create as many collection names as you wish to use in your app.

create collection

Step 4: Creating a Fauna Index

Indexes are used to quickly find data without searching every document in a database collection every time a database collection is accessed. Indexes can be created using one or more fields of a database collection. To create a Fauna index, click on the indexes section in the left part of your dashboard.

create fauna index

In our app, we will only create one index, which is the user_by_email index.

The user_by_email index is what we’ll use to get a user’s data with a given email. This index needs to be unique, so the collection doesn’t have duplicate emails.

Creating the project and installing dependencies

First, we need to initialise your project in npm; type the following in your terminal to do so:

npm init
Enter fullscreen mode Exit fullscreen mode

This will prompt some questions asked, you can answer them appropriately, and when this is done, a package.json file is created for you.

Next, we need to install the required dependencies. Type the following in your terminal:

npm install express faunadb dotenv express-handlebars
Enter fullscreen mode Exit fullscreen mode

Structuring the App

App structure

  • The routes folder is where we have our routes.js file for defining our routes.
  • The views folder is where our pages will be created and, in this case, handlebars.
  • The app.js file is where we will set up our server.
  • The configure.js file is where we will set up our app’s middleware.
  • The fauna.js file is where we will connect our Fauna database to our app and define functions used to create-user, login-user, and some other functions we will use in our routes.
  • The sendMail.js file is where we will use nodemailer to send confirmation emails to verify a user after a user creates an account.

Building Our Application

  1. Configuring and running the server: In your app.js, write the following code to set up your server.
var express = require('express'),
    config = require('./configure'),
    path = require("path"),
    app = express();
app = config(app);
app.set("port", process.env.PORT || 5000);
app.set("views", path.join(__dirname, "views"));
var server = app.listen(app.get("port"), function () {
  console.log("Server up: http://localhost:" + app.get("port"));
});
Enter fullscreen mode Exit fullscreen mode
  1. In your config file, which is configure.js, write the following code to configure your middleware functions.
var createError = require('http-errors');
  routes = require('./routes/routes')
  express = require('express'),
  session = require('express-session'),
  path = require('path'),
  cookieParser = require('cookie-parser'),
  logger = require('morgan'),
  dotenv = require('dotenv').config(), 
  flash = require('connect-flash'),
  exphbs = require('express-handlebars'),
  relativeTime = require('dayjs/plugin/relativeTime'),
  dayjs = require('dayjs');
module.exports = function (app) {
  dayjs.extend(relativeTime);
  app.engine('.hbs', exphbs.create({
    defaultlayout: 'main',
    layoutsDir: path.join(__dirname, './views/layouts'),
    partialsDir: path.join(__dirname, './views/partials'),
    helpers: { timeago: () => dayjs(new Date().toString()).fromNow()},
    extname: '.hbs',
  }).engine);
  app.set('view engine', 'hbs');
  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(flash());
  app.use(session({
    secret: process.env.SECRET,
    resave: true,
    saveUninitialized: true,
    maxAge: 600
  }))
  app.use(function(req,res,next){
    app.locals.isLoggedIn = req.session.user ? true : false
    next();
})
  app.use(routes)
  app.use('/public/', express.static(path.join(__dirname, './public')));
  // catch 404 and forward to error handler
  app.use(function(req, res, next) {
    next(createError(404));
  });
  // error handler
  app.use(function(err, req, res) {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};
    // render the error page
    res.status(err.status || 500);
    res.render('error');
    });
    return app;
};
Enter fullscreen mode Exit fullscreen mode
  1. Create a .env file in your route folder and fill it with the following:
NODE_LOGIN_FAUNA_KEY=’your generated fauna API key’
SECRET=’your app secret key’
EMAIL=’your email’
PASSWORD=’your email password’
Enter fullscreen mode Exit fullscreen mode

The email you input here is what you will use to send confirmation emails to new users, so ensure it’s a valid and functional one.

Creating our Fauna helper functions

To create a user, log in a user, update a user verification status that we will use to know if a user is verified or not, and delete a user in Fauna. Fauna has provided helper functions to help with that. Paste the following in your code to  help with that:

var dotenv = require('dotenv').config(),
    faunadb = require('faunadb'),
    bcrypt = require('bcrypt'),
    q = faunadb.query;
 
let Client = new faunadb.Client({ secret: process.env.NODE_LOGIN_FAUNA_KEY });
exports.createUser = async (email, username, password) => {
  password = bcrypt.hashSync(password, bcrypt.genSaltSync(10)) // generates a hash for the password
  let data
  try {
    data= await Client.query(   
      q.Create(
        q.Collection('Users'),
        {
          data: {email, username, password, isVerified: false}
        }
      )
    )
    if (data.username === 'BadRequest') return // if there's an error in the data creation it should return null
  } catch (error) {
    console.log(error)
    return 
  }
  const user = data.data
  user.id = data.ref.value.id // attaches the ref id as the user id in the client, it will be easy to fetch and you can guarantee that it's unique
  return user
}
exports.getUserByEmail = async (email) => {
  try {
    const user = await Client.query(
      q.Get(
        q.Match(
          q.Index('user_by_email'),
          email
        )
      )
    )
    return user.data
  } catch {
    return // return null if there is any error.
  }
}
exports.loginUser = async (email, password) => {
 try {
  let userData = await Client.query(
    q.Get(  
      q.Match(q.Index('user_by_email'), email.trim())
    )
  )
  userData.data.id = userData.ref.value.id
  if (bcrypt.compareSync(password, userData.data.password)) return userData.data
  else return
 } catch (error) {
   return
 }
}
exports.updateUser = (userId) => {
  const user = Client.query(
    q.Update(
      q.Ref(q.Collection('Users'), userId),
      {
        data: {
          isVerified: true
        }
      }
    )
  )
  .then((result) => result.data)
  .catch((err) => console.log(err.message))
}
exports.deleteUser = (userId) => {
  const user = Client.query(
    q.Delete(
      q.Ref(q.Collection('Users'), userId)
    )
  )
  .then((result) => console.log(result))
  .catch((err) => console.log(err.message))
}
Enter fullscreen mode Exit fullscreen mode

Above, we created five Fauna helper functions which are:

  1. createUser: It takes in an email, username and password, generates a hash for the password using bcrypt, saves the user’s information to false and set isVerified to false until the user confirms the account, then the isVerified will be set to true
  2. getUserByEmail: It retrieves a user by email using the index we created earlier.
  3. loginUser: It logs a user in using the email and password.
  4. updateUser: It updates a user’s information which in this case, updates a user’s verified status.
  5. deleteUser: Deletes a user from the Fauna database.

Defining Routes

To define all possible routes we discussed earlier for the app, create a routes.js file in the routes folder type the following:

var express = require('express'),
    hbs = require('express-handlebars'),
    router = express.Router(),
    auth = require('../fauna'),
    {sendMail} = require('../sendMail'),
    dotenv = require('dotenv').config(),
    jwt = require('jsonwebtoken');
router.get('/', (req, res) => {
  return res.render('index');
});
// Sign Up Routes 
router.get('/signup/', (req, res) => {
  return res.render('auth/signup')
})
router.post('/signup/', async (req, res) => {
  try {
    const {username, email, password, confirm_password} = req.body
    if (password !== confirm_password) {
      return res.render('auth/signup', {
        error: 'Passwords do not match'
      })
    }
    const user = await auth.createUser(email, username, password)
    let token = jwt.sign(user, process.env.SECRET, {expiresIn: 600})
    if (user) {
      req.session.user = user

      // Send verification mail for confirmation of account using Nodemailer
      sendMail(email, `Hi ${username}!,\nTo verify your account, please click on the link below and signin again. \nhttp://${req.headers.host}/confirm/${token}`, 'Verify your account')
      req.session.save((err) => {console.log(err)})
      return res.redirect('/dashboard/')
    }
  }
  catch (error){
    return res.render('auth/signup', {
      error: error.message
    })
  }
  return res.render('auth/signup', {
    error: 'Username or Email is chosen'
  })
})
// Sign In Routes
router.get('/signin/', function(req, res) {
  return res.render('auth/signin');
});
router.post('/signin/', async (req, res) => {
  try {
    const {email, password} = req.body
    const user = await auth.loginUser(email, password)
    if (user)  {
      req.session.user = user
      req.session.save((err) => console.log(err))
      return res.redirect('/dashboard/')
    }
  }
  catch (error){
    return res.render('auth/signin', {
      error: 'Invalid Email or Password'
    })
  }
  return res.render('auth/signin', {
    error: 'Invalid Email or Password'
  })
});
// Dashboard Routes
router.get('/dashboard/', async (req, res) => {
  try {
    if (req.session.user) {
      const user = req.session.user
      return res.render('dashboard', {user})
    }
  }
  catch (error){
    return res.render('dashboard', {
      error: error.message
    })
  }
  return res.redirect('/')
});
// Sign Out Routes
router.get('/signout/', (req, res) => {
  req.session.destroy((err) => console.log(err))
  return res.redirect('/signin/')
})
// Delete Account Route
router.delete('/delete-account/', async (req, res) => {
  if (req.session.user) {
    auth.deleteUser(req.session.user.id)
    req.session.destroy();
    return res.status(200).json({success: 'Data Deleted Successfully' })
  } else {
    return res.status(400).json({error: 'Not Successfully Deleted'})
  }
})
// confirm token and update user verification status
router.get('/confirm/:token', (req, res) => {
  const token = req.params.token
  jwt.verify(token, process.env.SECRET, (err, decoded) => {
    try {
      if (err) {
        return res.render('auth/signup', {
          error: 'Invalid Token'
        })
      }
      user = auth.updateUser(decoded.id, {isVerified: true})
      if (user) {
        req.session.user = user
        return res.redirect('/dashboard')
      }
    } catch (error) {
      return res.render('auth/signup', {
        error: 'Invalid Token'
      })
    }
  })
})
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

In the dashboard route, we added user session after sign-in for easy logging in for a period of time provided the user hasn’t signed out yet.

In the signout route, the user session is deleted, and the user is redirected back to the home page.

In the delete route, the user is deleted from our Fauna database with the deleteUser function we created in our fauna.js file.

In the confirm route, we generated a unique token using jsonwebtoken, sending an email using nodemailer with a redirect link containing the unique token with the link redirecting to the dashboard page and confirming the user email address. Then the user’s isVerified status will be set to true.

Sending Mails

I’ve been mentioning sending mails, but for the mail to actually be sent, we need a helper function to send a mail after a user has created an account. We would create a sendMail.js file. Type the following below:

var config = require('./configure'),
    express = require('express'),
    router = express.Router(),
    nodemailer = require('nodemailer');

exports.sendMail = async (to, html, subject) => {
    var transporter = nodemailer.createTransport({
    service: 'gmail',
    port:465,
    auth: {
        user: process.env.EMAIL,
        pass: process.env.PASSWORD
    }
});

var mailOptions = {
  from: process.env.EMAIL,
  to: to,
  subject: subject || 'Confirmation of Account',
  html: html
};

transporter.sendMail(mailOptions, function(error, info){
    if (error) {
        console.log(error);
        return {error: error.message}
    } else {
        console.log('Email sent: ' + info.response);
        return {success: info.response}
        }
    });
    transporter.close()
}
Enter fullscreen mode Exit fullscreen mode

Testing Our App

Like I said earlier, our front end is built with handlebars. You can choose any view engine you want to use. Let’s test the routes we’ve built:

  • SignUp Route

We signup with our credentials (email, username and password), It redirects to the dashboard page with a welcome message but saying the user should check his/her email for verification instructions.

Signup

Let’s confirm if the user has been created in the database

confirm user

We’ll then confirm if our email has been sent.

confirm email

Ps: For you to enable nodemailer to send mails using your provided email, you have to configure your Gmail settings to “allow less secure apps” and enable Recaptcha.

  • Signin Route

We’ll click the link sent to the mail and check if it redirects to the sign-in page.

confirm sent mail

We’ll sign in again and see the new welcome message of a verified user.

welcome message

  • Signout Route

We’ll click the sign out button and sign out of the account.

  • Delete Route

We sign in again and test the delete account feature. The user will be completely deleted from the Fauna database.

Lastly, We’ll now confirm from our database if the user has been deleted.

check database

As we can see above, the only user we created has been deleted.

Conclusion

This article has built a web application that logs users in and logs users out using two exciting technologies, Fauna and Expressjs. The source code for this project is available on Github. If you enjoyed this article, please share it with your friends who will need it. You can reach me on Twitter if you have any questions.

Written in connection with the Write with Fauna Program.

Discussion (3)

Collapse
tonitammi profile image
tonitammi • Edited

if(data.username === 'BadRequest') return // ... should return null

If that and few other lines should return null, it's maybe a good idea to return null instead of undefined.

Collapse
rabiudev profile image
rabiu-dev

Great article Sodiq! But here's a suggestion, I think it'd be more secure and advisable to use Oauth2 for the Gmail service for nodemailer..

Collapse
sodiq123 profile image
Sodiq_dev Author

Alright, thank you