DEV Community

Babatunde Koiki
Babatunde Koiki

Posted on

Building A User Login System With Flask and Fauna

Introduction

In this article, I will be showing how to build a user login system with Python, Flask, and Fauna. Flask is a Python microframework that is used to build web applications while Fauna is an easy-to-use NoSQL serverless database.

Fauna provides a lot of features to make its database integration process seamless. I have come to love its native support for GraphQL and cloud functions. You can create an account on Fauna here.

App Structure

In this article, we will be building a web application with 4 pages:

  • Home/Landing Page
  • Dashboard Page
  • Sign-in Page
  • Signup Page

To begin, create a folder and create the following files app.py, templates/dashboard.html, templates/home.html, templates/signin.html, templates/signup.html, .env. Your project structure should look like the following:

app structure

Installing Dependencies

Next, you need to install the required dependencies, type the following in your terminal:

pip install flask faunadb python-dotenv
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Creating Our Flask App

To create a simple Flask application for the tutorial, type the code below in the app.py


from flask import Flask

app = Flask(__name__)

@app.route(/)
def home():
 return {
 'Message': 'Welcome to our app'
 }

if __name__ == '__main__':
 app.run()
Enter fullscreen mode Exit fullscreen mode

To run the code above, first set the FLASK_DEBUG variable to true then run the app using the command below:

export FLASK_DEBUG=true # if you are using windows change export to set
flask run
Enter fullscreen mode Exit fullscreen mode

You should get a response similar to the image below in your terminal:

dev server

Click on the link and you will see the following in your browser

simple server

Yaay! you have built a simple flask app with a few lines of code.

Setting Up Fauna Database

Step 1: Creating Fauna Database

To create a database, go to Fauna's Dashboard, then click on NEW DATABASE and enter the database name in the prompt

Create a database
Enter database name

Step 2: Creating Fauna Secret Key

We will be using Fauna’s secret key to communicate with our database. To generate a secret key, click on the security button and click on NEW KEY button in the security tab.

Create a secret key

Enter secret key details

Copy your secret key and keep it safe, you won’t be able to see it again.

Step 3: Creating Our Fauna Collection

Create a collection

Click on NEW COLLECTION and enter the collection name in the prompt, in this case, my collection name is user.

Step 4: Creating An Index For Your Collection

Go to the INDEXES tab in the left part of the screen and click on the NEW INDEX button, and enter your details in the prompt, it should look like the one in the image below

Create an index

Building Our Application

Let’s get started with the real application for this article, our app will demonstrate how to authenticate users in a Flask application.

In your .env file, type the following:

Step 1: Adding Environment Keys

FAUNA_SECRET='your fauna secret'
SECRET_KEY='your application secret key'
Enter fullscreen mode Exit fullscreen mode

Step 2: Importing Dependencies and Defining Home Route

Let’s write the view for the home route, update your app.py file with the code below:

import re
from flask import Flask, flash, redirect, url_for, render_template, request, session
from werkzeug.security import generate_password_hash, check_password_hash
from faunadb import query as q
from faunadb.client import FaunaClient
from faunadb.objects import Ref
from faunadb.errors import BadRequest, NotFound
from dotenv import load_dotenv
import os, secrets

app = Flask(__name__)
app.config['SECRET_KEY']=os.getenv('SECRET_KEY')

client = FaunaClient(secret=os.getenv('FAUNA_SECRET'))

@app.route('/')
def home():
 return render_template('home.html')
Enter fullscreen mode Exit fullscreen mode

Next, we create our home.html file and add the following:

<head>
 <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Fauna-Login</title>
 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
 <link rel="stylesheet" href="style.css">
</head>
<body>

 <!-- Navigation -->
 <nav class="navbar navbar-inverse">
     <div class="container-fluid">
         <div class="navbar-header left">
             <a class="navbar-brand active" href="/">Fauna-Login</a>
         </div>
         <ul class="nav navbar-nav right">
             <li><a href="/signin">SignIn</a></li>
             <li><a href="/signup">Signup</a></li>
         </ul>
     </div>
 </nav>

 <!-- Page Content -->

 <div>
     {% with messages = get_flashed_messages(with_categories=True) %}
         {% if messages %}
             {% for category, message in messages %}
                 <div class="alert alert-{{ category }}"> 
                     {{ message }}
                 </div>
             {% endfor %}
         {% endif %}
     {% endwith %}
 </div>
 <h1>Hello there!</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The with block is a feature of jinja which is the Flask template engine. The with block here is used for flashing messages to the user like signup success message, error message by the user, etc. In the server, we will use the flash function in Flask, and if that code block is seen it will display it in the browser because we have added it to our HTML file.

If you refresh the page you should see the image below. Note that the login and register buttons aren’t working yet as we have not defined routes for them yet.

Home page

Step 3: Defining Signin and Signup routes

Let’s define our SignIn and SignUp endpoint, add the following code below the home function in the app.py file

@app.route('/signin/', methods=['POST', 'GET'])
def signin():
    if session.get('user_id'):
            flash('You are logged in!', 'warning')
            return redirect(url_for('dashboard'))
    if request.method =='POST':
        # get the user details
        email = request.form['email']
        password = request.form['password']
        # verify if the user details exist
        try:
            user = client.query(
                    q.get(q.match(q.index('user_by_email'), email))
            )
        except NotFound:
            flash('Invalid email or password', category='warning')
        else:
            if check_password_hash(user['data']['password'], password):
                session['user_id'] = user['ref'].id()
                flash('Signed in successfully', 'success')
                return redirect(url_for('dashboard'))
            else:
                flash('Invalid email or password', 'warning')
    return render_template('signin.html')

@app.route('/signup/', methods=['POST', 'GET'])
def signup():
    if session.get('user_id'):
            flash('You are logged in!', 'warning')
            return redirect(url_for('dashboard'))
    if request.method =='POST':
        name = request.form['name']
        email = request.form['email']
        password = request.form['password']
        email_regex = '^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'
        if not re.search(email_regex, email) or not 6 < len(password) < 20:
            flash('Invalid email or password!, password needs to be between 6 and 20 characters', 'warning')
            return render_template('signup.html')
        if password != request.form['confirm_password']:
            flash('password fields not equal', 'warning')
            return render_template('signup.html')
        password = generate_password_hash(password)
        user = {'name': name, 'email': email, 'password': password}
        try:
            # store user data to db
            new_user = client.query(q.create(
                q.collection('user'),
                {'data': user}
            ))
        except BadRequest:
            flash('Email already exists')
        else:
            session['user_id'] = new_user['ref'].id()
            flash('Account created successfully', 'success')
            return redirect(url_for('dashboard'))
    return render_template('signup.html')
Enter fullscreen mode Exit fullscreen mode

A lot is going on here, the signin route takes form data from the signin.html file and processes it. First, I add a methods argument to the route and add POST AND GET, it’s GET by default. Then I checked if a user session is set, I will be explaining this in detail later. Then I also check if the request is sent via a POST request, if it is then I retrieve the user data and use Fauna’s client object to check if a user exists with that email, if no user exists I flash an error message to the user and redirect them back to the signin endpoint using url_for and redirect method in Flask. If the user exists I used the session object in Flask to log the user in. To store a session object all I need to do is

session[name]=value
Enter fullscreen mode Exit fullscreen mode

After storing the session I redirected them to the dashboard page. Also, the signup page is similar except for the fact that the data we retrieved from the browser is used in creating new data. I also confirmed that the email is unique, if it is not unique it will raise an error that I handled. Before dealing with the front-end codes it will be nice to work with all the server-side code, we will define two more endpoints and we will also update our home route.

Step 4: Defining dashboard and signout routes

@app.route('/dashboard/')
def dashboard():
    if not session.get('user_id'):
            flash('You need to be logged in to view this page!', 'warning')
            return redirect(url_for('signin'))
    user = client.query(
        q.get(q.ref(q.collection("user"), session['user_id']))
    )['data']
    return render_template('dashboard.html', current_user=user)

@app.route("/signout/")
def signout():
    if not session.get('user_id'):
        flash('You need to be logged in to do this!', 'warning')
    else:
        session.pop('user_id', None)
        flash('Signed out successfully', 'success')
    return redirect(url_for('home'))
Enter fullscreen mode Exit fullscreen mode

In the dashboard route, I first check if a user_id session is not set which means the request is unauthorized, if this condition is true I redirected back to the signin route. If the session is set, I used the user_id session variable to get the user data and I passed it to the HTML file that’s being rendered.

In the signup route, I first check if the user_id session isn’t set then I flash a message to the user but if the session is set I remove it and in either case, I redirect them back to the home route. I used the session.pop function which takes the name we of the variable I want to remove from the session and I also pass in None

Edit the home route to be like the one below:

app.route('/')
def home():
    if session.get('user_id'):
            flash('You are logged in!', 'warning')
            return redirect(url_for('dashboard'))
    return render_template('home.html')
Enter fullscreen mode Exit fullscreen mode

Before rendering the home.html file I first check if the user_id session is set, if it is then the user is signed in and I redirect back to the dashboard page.

Step 5: Creating the html files

In your signin.html file type the following:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SignIn - Fauna-Login</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

</head>
<body>
    <!-- Navigation -->
    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
          <div class="navbar-header">
            <a class="navbar-brand" href="{{url_for('home')}}">Fauna-Login</a>
          </div>
          <ul class="nav navbar-nav">
            <li><a href="{{url_for('signin')}}">SignIn</a></li>
            <li><a href="{{url_for('signup')}}">Signup</a></li>
          </ul>
        </div>
      </nav>
  <!-- Page Content -->
  <div class="container">
    {% with messages = get_flashed_messages(with_categories=True) %}
    {% if messages %}
        {% for category, message in messages %}
            <div class="alert alert-{{ category }}"> 
                {{ message }}
            </div>
        {% endfor %}
    {% endif %}
    {% endwith %}
  </div>
  <div class="col-md-4"></div>
  <div class="container content-section col-md-4">
    <h1 class="mt-4">Sign In</h1>
    <form action="{{url_for('signin')}}" method="post">
        <div class="form-group">
            <label for="name" class="form-control-label">Email:</label>
            <input class='form-control-lg form-control' type="email" name="email" id="" required>
        </div>

        <div>
            <label width="100" for="name" class="form-control-label">Password:</label>
            <input class='form-control-lg form-control' type="password" name="password" id="" required>
        </div>
        <div class="form-group">
            <input type="submit" class="btn btn-outline-info" value="Sign In">
        </div>
        <div>Don't have an account? <a href="/signup">Signup</a></div>
    </form>
</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

In your signup.html file type the following

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SignUp - Fauna-Login</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

</head>
<body>
    <!-- Navigation -->
    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
          <div class="navbar-header">
            <a class="navbar-brand" href="{{url_for('home')}}">Fauna-Login</a>
          </div>
          <ul class="nav navbar-nav">
            <li><a href="{{url_for('signin')}}">SignIn</a></li>
            <li><a href="{{url_for('signup')}}" class="active">Signup</a></li>
          </ul>
        </div>
      </nav>
  <!-- Page Content -->
  <div class="container">
    {% with messages = get_flashed_messages(with_categories=True) %}
    {% if messages %}
        {% for category, message in messages %}
            <div class="alert alert-{{ category }}"> 
                {{ message }}
            </div>
        {% endfor %}
    {% endif %}
    {% endwith %}
  </div>
  <div class="col-md-4"></div>
  <div class="container content-section col-md-4">
    <h1 class="mt-4">Sign In</h1>
    <form action="{{url_for('signup')}}" method="post">
        <div class="form-group">
            <label for="name" class="form-control-label">Name:</label>
            <input class='form-control-lg form-control' type="text" name="name" id="" required>
        </div>

        <div>
            <label for="email" class="form-control-label">Email:</label>
            <input class='form-control-lg form-control' type="email" name="email" id="" required>
        </div>

        <div>
            <label for="name" class="form-control-label">Password:</label>
            <input class='form-control-lg form-control' type="password" name="password" id="" required>
        </div>

        <div>
            <label for="name" class="form-control-label">Confirm Password:</label>
            <input class='form-control-lg form-control' type="password" name="confirm_password" id="" required>
        </div>
        <div class="form-group">
            <input type="submit" class='btn btn-outline-info' value="Sign Up">
        </div> 
        <div>Have an account? <a href="/signin">Login</a></div>
    </form>
</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

In the action of the form, I used url_for which is a flask function that takes in a view function and returns the endpoint that the view function responds to. Alternatively, I can just pass the endpoint as I did at the end of the form.

In your dashboard.html file type the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{current_user.name}} - Fauna-Login</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

</head>
<body>
    <!-- Navigation -->
    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
          <div class="navbar-header">
            <a class="navbar-brand active" href="#">Fauna-Login</a>
          </div>
          <ul class="nav navbar-nav">
            <li><a href="{{url_for('signout')}}">Signout</a></li>
          </ul>
        </div>
      </nav>
  <!-- Page Content -->
  <div>
    {% with messages = get_flashed_messages(with_categories=True) %}
    {% if messages %}
        {% for category, message in messages %}
            <div class="alert alert-{{ category }}"> 
                {{ message }}
            </div>
        {% endfor %}
    {% endif %}
    {% endwith %}
  </div>
    <h1>Welcome {{current_user.name}}</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Testing The Application

Finally, we will be testing the application. First, go to the home route, you should see the following:

Home Page

Click the signup button and try to sign up with an email and password whose length is not between 6 and 20 characters.

Invalid sign up

Alt Text

So we can see that our data is being validated before data creation. Next, create user data.

Dashboard page

Click the signout button, you get a cool success message and you can signin again.

Sign out

Click the signin button and try to sign in with the data you created while signing up.

Sign in

If your data is correct you should be able to sign in successfully.

Dashboard page 2

Database Check

Let’s view our user collection dashboard in Fauna

DB Check

As you can see, the data is stored in the dashboard correctly. You can also query your data directly in your Fauna dashboard using FQL.

Conclusion

In this article, you have been able to build a web application that logs users in and also logs users out using two interesting technologies, Flask and Fauna. The source code for this project is available on GitHub If you find this article interesting please do share it with your friends and colleagues. You can reach out to me via Twitter if you have any questions.

Top comments (1)

Collapse
 
abzresponsible profile image
Abdi Abyan

With python login can be much faster as your app user base grows and people log-in and sign up all at the same time.