Building A User Login System With Flask and Fauna
Tags: python, flask, serverless, fauna
Published: 2021-03-16 23:24:43

# Introduction

In this article, I will be showing how to build a user login system with Python, Flask, and [Fauna](https://fauna.com/). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o6j3m91ly9nrfk8b0snq.png)

# Installing Dependencies

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

```bash
pip install flask faunadb python-dotenv
pip freeze > requirements.txt
```

# Creating Our Flask App

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

```python

from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
 app.run()
```

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

```bash
export FLASK_DEBUG=true # if you are using windows change export to set
flask run
```

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

![dev server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ah0sc7bqvn8e4xxbhny5.png)

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

![simple server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhf7y5riemhe8refbjpp.png)

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](https://dashboard.fauna.com/), then click on `NEW DATABASE` and enter the database name in the prompt

![Create a database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whd61it3fmoaikus0tuu.png)
![Enter database name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62z2ry2257ma64vkwpmc.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fm64saeb19gr8g7cclo.png)

![Enter secret key details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7dikwurici0b3phki0uw.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5655p9e1xi8ng1mzs1j7.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7hu6nm0xvvpq9ah5mfop.png)

# 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'
```

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:

```python
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')
```

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

```html
<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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7ogsrslubddy6ik502q.png)

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

```python
@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')
```

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 

```python
session[‘name’]=value
```

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

```python
@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'))
```

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:

```python
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')
```

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:

```html
<!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>
```

In your `signup.html` file type the following

```html
<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>
```

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:

```html
<!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>
```

# Testing The Application

Finally, we will be testing the application. First, go to the [home route](http://127.0.0.1:5000/), you should see the following:

![Home Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4sqk5mbtb12nhg1ptvf.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6bv3gh164uy86ibkkd3y.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5uhvcz4uwuwyrlpxsyp.png)

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

![Dashboard page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6kq875o8zobm6tqenc9s.png)

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

![Sign out](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0c7ug34arrfsngohjj4c.png)

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

![Sign in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nuchrg2rmrxizstljcwa.png)

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

![Dashboard page 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rgueivqun0555hbio8hg.png)

# Database Check

Let’s view our user collection dashboard in [Fauna](https://dashboard.fauna.com/collections/user/@db/fauna-flask-login)

![DB Check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w6j2342qvm491i8soyxg.png)

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](https://github.com/Babatunde13/Fauna-Login) If you find this article interesting please do share it with your friends and colleagues. You can reach out to me via [Twitter](https://twitter.com/bkoiki950) if you have any questions.