Banking Web App Stories — Part 3

Nat Retsel
7 min readMay 19, 2023

--

Continuing from part 2 of the banking web app series, this part aims to get user registration and login to work. To do so we will need:

  • To verify the email address used for registration doesn’t already exist in the database.
  • Ensure that the details of the newly created account are pushed to the database.
  • By default, users who created their account on our website are assigned the “users” role id.
  • Users who are already logged in are not required to login again if they visit the /login and /register route. The anchor to /login should also not appear when users are logged in and perhaps should be replaced with a /logout route. To do so, we will need to keep track of their logged in states.
  • Ensuring user is logged in when the correct email and password is submitted.

Verifying unique email addresses during account creation

To validate if the user has entered a unique email during account creation, we can make a query to our database to search for what the user inputs in the email field when the user hits submit. Better yet, Flask allows us to add validation methods in our forms and it gets ran as part of validate_on_submit(). To do so, we follow the naming convention of the method to validate_fieldname().

If there exists a user with the email address, then we raise a ValidationError telling the user to use a different email address:

from typing import Optional
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, BooleanField, EmailField
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
from app.models import User

class RegistrationForm(FlaskForm):
"""User registration form
- User account registration. Login ID will be the email address
- Inherits FlaskForm object
- Fields:
- first_name (str): User first name
- last_name (str): User last name
- email (str): User email
- password (str): User password
- password (str): User password repeat
- submit ('POST')

Raises:
ValidationError: when an email is already present in the database
"""
first_name = StringField('First Name', validators=[DataRequired()])
last_name = StringField('Last Name', validators=[DataRequired()])
email = EmailField('Email',validators=[DataRequired(), Email()] )
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Submit')

def validate_email(self, email: str) -> None:
user: Optional[str] = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')

Pushing new account details into the database

We will push details of the new account into the database once the form is validated on submit. We create a User model with the data given in the form, remembering to store the hashed password using the set_password()method instead of storing during model creation and then push it into the database.

Let’s also flash a message letting the user know that their account creation is successful and redirect them to the login page.

@app.route('/register', methods=['GET', 'POST'])
def register() -> Response:
form = RegistrationForm()
if form.validate_on_submit():
user_role = Role.query.filter_by(name='User').first()
user = User(first_name=form.first_name.data, last_name=form.last_name.data, email=form.email.data, role_id=user_role.id) # Defaults role to user role
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user! Please login')
return redirect(url_for('login'))
return render_template('register.html', form=form)

Notice we default the user role id by first querying it in the role table. This assumes that the roles will be present from the moment the app goes live and we will need to make sure of that.

Flask-Login

The extension Flask-Login manages the user logged-in state and so the application “remembers” that the user is logged in even if the browser window is closed. It does this by storing its unique identifier in Flask’s user session, a storage space assigned to each user who connects to the application. Each time the logged-in user navigates to a new page, Flask-Login retrieves the ID of the user from the session, and then loads that user into memory.

Before that, we first initialize it with the LoginManager class in our app/__init__.py:

...
from flask_login import LoginManager

app = Flask(__name__)
login = LoginManager(app)

...

Flask-Login has 4 requirements to work with our User models:

  • is_authenticated: True if the user is authenticated with valid credentials, False otherwise.
  • is_active: True if the user's account is active, False otherwise.
  • is_anonymous: True for anonymous user, False for regular users. Think of the inverse of is_authenticated .
  • get_id(): returns a unique identifier for the user as a string.

Thankfully, Flask-Login provides a mixin class called UserMixin that includes the requirements as well as generic implementations that are appropriate for most user model classes. We will extend to our User model in models.py:

...
from flask_login import UserMixin

class User(UserMixin, db.Model):
...

While Flask-Login is in charge of managing user’s logged in state, it knows nothing about databases, hence it expects the app to load the user — configuring a user loader function — that can be called to load a user given the ID. This function can be added in the app/models.py module with the @login.user_loader decorator. The id that Flask-Login passes to the function as an argument is going to be a string, so databases that use numeric IDs need to convert the string to integer.

from app import db, login

@login.user_loader
def load_user(id):
return User.query.get(int(id))

Now having a way to track the user’s logged in state, we can head back to our routes and modify our scripts to ensure that when a user who is logged in visits /register or /login, they get redirected to /index. Fortunately for us, Flask’s mixin class has the .is_authenticated method that does the checking:

...
from flask_login import current_user, login_user

@app.route('/register', methods=['GET', 'POST'])
def register() -> Response:

if current_user.is_authenticated:
return redirect(url_for('index'))

form = RegistrationForm()
if form.validate_on_submit():
user_role = Role.query.filter_by(name='User').first()
user = User(first_name=form.first_name.data, last_name=form.last_name.data, email=form.email.data, role_id=user_role.id) # Defaults role to user role
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user! Please login')
return redirect(url_for('login'))
return render_template('register.html', form=form)


@app.route('/login', methods=['GET', 'POST'])
def login() -> Response:
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)

The current_user variable comes from Flask-Login and can be used at any time during the handling to obtain the user object that represents the client of the request. The value of this variable can be a user object from the database (which Flask-Login reads through the user loader callback function above), or a special anonymous user object if the user did not log in yet.

During login, we first query for a user with the input email. If user exist, we check if the password matches. If any fails, we flash a message saying that either the email or password is wrong and redirect back to the login page. If credentials match, we will register the user as logged in using the login_user() function, so that means that any future pages the user navigates to will have the current_user variable set to that user.

Logout route

Let’s not forget to implement the logout route:

from flask_login import current_user, login_user, logout_user

@app.route('/logout')
def logout() -> Response:
logout_user()
return redirect(url_for('index'))

and make sure the following in the base.html script:

  • Users who are not logged in should not be able to view the logout anchor in the nav bar.
  • Users who are logged in should not be able to view the register and login anchors in the nav bar.
{% extends "bootstrap/base.html" %}
{% block title %}Bank{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar_header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>

</button>
<a class="navbar-brand" href="/">Bank</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
{% if current_user.is_anonymous %} <! conditionals for logged in status>
<li><a href="/register">Register</a></li>
<li><a href="/login">Login</a></li>
{% else %}
<li><a href="/logout">Logout</a></li>
{% endif %}

</ul>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-success">
<button type="button" class="close" data-dismiss="success">&times;</button>
{{message}}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}

We have also included a section below to allow flash messages to appear. Flash messages from the previous request gets pulled with the get_flashed_messages() function and we access it iteratively with a loop.

Let’s also have the /index page greet the user’s first name upon redirected from login:

{% extends "base.html" %}
{% block content %}
<h1>Hello {{current_user.first_name}}</h1>
{% endblock %}

Before we try these features and create our first user, we first need to create the entries to our roles table in the terminal, otherwise we are unable to query for the default ‘user’ role when new users register themselves:

flask shell
from app import db
from app.models import Role

admin_role = Role(name="Admin")
mod_role = Role(name="Moderator")
user_role = Role(name="User")

db.session.add_all([admin_role, mod_role, user_role])
db.session.commit()

You can choose to migrate and upgrade the database before running the app or to do after so there is no need to retype these commands every time.

This is how the home page looks like when users are not logged in:

Upon logging in:

Do give the app a go and try visiting /register and /login routes after you have logged in and make sure the flask-login and redirect are working as intended.

To check if your newly created individual has been assigned the user role, run the following query in flask shell:

from app import db
from app.models import User, Role
user_role = Role.query.filter_by(name='User').first()
User.query.filter_by(role=user_role).all()

In my case:

As we look to setup different environments to separate testing, development and production, we can look to find ways to initialize our role database with these default entries.

--

--