PolarSPARC

Web Applications using Python Flask - Part II


Bhaskar S 09/01/2021


Hands-on Python Flask - Part II

Shifting gears, we will now move into the next phase of integrating a database for our web application. Currently, the user registration infomation is not being persisted anywhere. We will use the sqlite database to create a table called user_tbl for storing the user registration information.

The following is the modified version of the Python script called config.py which configuration details for the sqlite database via sqlalchemy:


config.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask import Flask
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import logging
import os

app_name = 'SecureNotes'

# Flask related config

app = Flask(app_name)

gunicorn_logger = logging.getLogger('gunicorn.error')
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)

app.config['SECRET_KEY'] = 's3_b4nd_$_1'

app.logger.debug('Flask application root path: %s' % app.root_path)
app.logger.debug('Flask application static folder: %s' % app.static_folder)
app.logger.debug('Flask application template folder: %s' % os.path.join(app.root_path, app.template_folder))

# sqlalchemy related config

engine = create_engine('sqlite:///db/secure_notes.db')

Base = declarative_base()
Base.metadata.bind = engine

DBSession = sessionmaker(bind=engine)
session = DBSession()

Some aspects of the config.py from the above needs a little explanation.

We will need a domain object class in Python that corresponds to the table user_tbl.

The following is the Python script called user.py that will be located in the directory SecureNotes/model:


user.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   01 Sep 2021
#

from sqlalchemy import Column, String
from werkzeug.security import generate_password_hash, check_password_hash
from config.config import Base, engine, session

class User(Base):
    __tablename__ = 'user_tbl'
    email_id = Column(String(64), primary_key=True)
    password_hash = Column(String(64))

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def __repr__(self):
        return ''.format(self.email_id)

    @staticmethod
    def register(email, password):
        user = User(email_id=email)
        user.set_password(password)
        session.add(user)
        session.commit()
        return user

    @staticmethod
    def query_by_email(email):
        return session.query(User).filter(User.email_id == email).first()

Base.metadata.create_all(engine, checkfirst=True)

Some aspects of the user.py from the above needs a little explanation.

Next, we need to make changes to the view function signup() in the main Flask application to save the valid user registration details to the database and redirect to the login HTML page.

The following is the modified version of the Python script main.py:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask import request, session
from flask.templating import render_template
from config.config import app
from model.user import User

@app.route('/')
def index():
    return render_template('welcome.html')

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'GET':
        return render_template('signup.html')
    email = None
    if 'email' in request.form:
        email = request.form['email']
    if email is None or len(email.strip()) == 0:
        return render_template('signup_error.html', message='Invalid email !!!')
    password1 = None
    if 'password1' in request.form:
        password1 = request.form['password1']
    if password1 is None or len(password1.strip()) == 0:
        return render_template('signup_error.html', message='Invalid password !!!')
    password2 = None
    if 'password2' in request.form:
        password2 = request.form['password2']
    if password1 != password2:
        return render_template('signup_error.html', message='Password confirmation failed !!!')
    user = User.register(email, password1)
    app.logger.info('User %s successfully registered!' % user)
    return render_template('welcome.html')

Restart the gunicorn server and launch a browser and access the URL http://127.0.0.1:8080/. Once the login page loads, click on the 'Sign Up' link. When the sign-up page loads, enter an email-id, a password, re-enter the same password for confirmation and then click on the 'Register' button. The browser be redirected to the login page on success. The following would be a typical output on the web server terminal:

Output.3

[2021-09-01 08:49:19 -0400] [5897] [DEBUG] GET /
[2021-09-01 08:49:20 -0400] [5897] [DEBUG] GET /static/bootstrap.min.css
[2021-09-01 08:49:20 -0400] [5897] [DEBUG] GET /static/images/polarsparc.png
[2021-09-01 08:49:20 -0400] [5897] [DEBUG] GET /favicon.ico
[2021-09-01 08:49:22 -0400] [5897] [DEBUG] GET /signup
[2021-09-01 08:49:22 -0400] [5897] [DEBUG] GET /static/bootstrap.min.css
[2021-09-01 08:49:22 -0400] [5897] [DEBUG] GET /static/images/polarsparc.png
[2021-09-01 08:49:24 -0400] [5898] [DEBUG] Closing connection. 
[2021-09-01 08:49:40 -0400] [5898] [DEBUG] POST /signup
[2021-09-01 08:49:40 -0400] [5898] [INFO] User <User alice@test.org> successfully registered!
[2021-09-01 08:49:40 -0400] [5898] [DEBUG] GET /static/bootstrap.min.css
[2021-09-01 08:49:40 -0400] [5898] [DEBUG] GET /static/images/polarsparc.png

Now that we have the user registration working, we will handle the user login. There are two paths - successful and the unsucessful path. For the unsuccessful path, we direct the user to an login error page. On successful login, we will direct the user to the secure part of the web application.

The following is the HTML page called login_error.html that will be located in the directory SecureNotes/templates:


login_error.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Login Error</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Login Error</h3>
                </div>
            </div>
            <br/>
            <div class="alert alert-danger" role="alert">
                <p>{{ message }}</p>
            </div>
            <br/>
            <span class="p-1 rounded-sm border border-primary">
                <a href="javascript:history.go(-1)" class="alert-link">Back</a>
            </span>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

The following is the HTML page called secure_notes.html that will be located in the directory SecureNotes/templates and is routed to on successful login:


secure_notes.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Authenticated</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Authenticated</h3>
                </div>
            </div>
            <br/>
            <div class="alert alert-primary" role="alert">
                <p>This is a SECURE area !!!</p>
            </div>
            <br/>
            <span class="p-1 rounded-sm border border-primary">
                <a href="/logout" class="alert-link">Logout</a>
            </span>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

The following is the modified version of the HTML page welcome.html to route the login request to the Flask application at the URL endpoint /login:


welcome.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Login</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Login</h3>
                </div>
            </div>
            <form action="/login" method="POST">
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" required placeholder="Enter email...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password" name="password" required placeholder="Enter password...">
                </div>
                <button type="submit" class="btn btn-primary">Login</button>
            </form>
            <br/>
            <div class="alert alert-primary" role="alert">
                Don't have an account - <a href="/signup" class="alert-link">Sign Up</a>
            </div>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

Notice the use of action="/login" and method="POST".

The following is the modified version of the Python script main.py to handle additional URL endpoints such as /login, /secure, and /logout:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask import request, session, redirect
from flask.templating import render_template
from config.config import app
from model.user import User

@app.before_request
def verify_logged():
    app.logger.debug('Reuqest path: %s' % request.path)
    if 'logged_user_id' not in session and request.path not in ['/', '/static/bootstrap.min.css', '/signup', '/login']:
        return redirect('/login')

@app.route('/')
def index():
    return render_template('welcome.html')

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'GET':
        return render_template('signup.html')
    email = None
    if 'email' in request.form:
        email = request.form['email']
    if email is None or len(email.strip()) == 0:
        return render_template('signup_error.html', message='Invalid email !!!')
    password1 = None
    if 'password1' in request.form:
        password1 = request.form['password1']
    if password1 is None or len(password1.strip()) == 0:
        return render_template('signup_error.html', message='Invalid password !!!')
    password2 = None
    if 'password2' in request.form:
        password2 = request.form['password2']
    if password1 != password2:
        return render_template('signup_error.html', message='Password confirmation failed !!!')
    user = User.register(email, password1)
    app.logger.info('User %s successfully registered!' % user)
    return render_template('welcome.html')

@app.route('/login', methods=['POST'])
def login():
    email = None
    if 'email' in request.form:
        email = request.form['email']
    if email is None or len(email.strip()) == 0:
        return render_template('login_error.html', message='Invalid email !!!')
    password1 = None
    if 'password' in request.form:
        password = request.form['password']
    if password is None or len(password.strip()) == 0:
        return render_template('login_error.html', message='Invalid password !!!')
    user = User.query_by_email(email)
    if user is None:
        return render_template('login_error.html', message='Invalid email !!!')
    if not user.verify_password(password):
        return render_template('login_error.html', message='Invalid password !!!')
    session['logged_user_id'] = email
    return redirect('/secure', code=307)

@app.route('/secure', methods=['POST'])
def secure():
    return render_template('secure_notes.html')

@app.route('/logout', methods=['GET'])
def logoff():
    session.pop('logged_user_id', None)
    return render_template('welcome.html')

Some aspects of the main.py from the above needs a little explanation.

Restart the gunicorn server and launch a browser and access the URL http://127.0.0.1:8080/. We need to ensure we have registered at least one user to test the login. Once the login page loads, enter an invalid email-id, and any string for the password and then click on the 'Login' button. The following illustration shows the response on the browser:

Invalid Email
Figure.7

Click on the Back button to go back and re-enter the correct email and the correct password and then click on the 'Login' button. The following illustration shows the response on the browser:

Successful Login
Figure.8

WALLA !!! Click on the Logout button to go back to the login page.

References

GitHub - Source Code

Web Applications using Python Flask - Part I

Introduction to SQLAlchemy :: Part - 2


© PolarSPARC