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:
# # @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.
app.config['SECRET_KEY'] = 's3_b4nd_$_1' :: allows one to set a secret key which will be used to secure the web session, such as signing of the cookies to prevent against cookie tampering
Engine :: represents the entry point through which one can interact with the underlying database. It wraps a connection pool and a dialect to the underlying database
create_engine('sqlite:///db/secure_notes.db') :: creates an instance of Engine using the specified database url. A database called secure_notes.db is created in the directory SecureNotes/db
declarative_base() :: a method that creates the object model base class, which needs be extended by the user defined class to create the domain model objects
Session :: an object that manages the database persistence operations of the domain model object(s)
sessionmaker(bind=engine) :: a factory method that creates an instance of Session object
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:
# # @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.
__tablename__ :: sqlalchemy internal attribute that specifies the database table name this user defined class represents
Column :: represents a database column. One *DOES NOT* have to supply the column name as the attribute name will be used as the column name
generate_password_hash(password) :: a utility method that generates a SHA256 hash for the specified password and is defined in the Python module werkzeug.security
check_password_hash(self.password_hash, password) :: a utility method that checks the the specified password against the stored hash and is defined in the Python module werkzeug.security
session.add(user) :: method to add the specified domain model object into the Session object for persistence
session.commit() :: method to flush and commit all the changes to the Session object
session.query(User) :: method that represents a database query operation and translates into a SQL SELECT operation
session.query(User).filter(User.email_id == email) :: method that applies the specified search criteria and translates into a SQL WHERE clause
session.query(User).filter(User.email_id == email).first() :: method that returns the first row result from the SQL query
Base.metadata.create_all(engine, checkfirst=True) :: method that creates database table(s) for all the domain object class(es) in the metadata if not already present
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:
# # @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:
[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:
<!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:
<!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:
<!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:
# # @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.
session['logged_user_id'] = email :: is a Python dictionary under-the-hood and is used to remember information from one request to another. The way Flask does the session tracking is using a signed cookie. This is why it is *VERY IMPORTANT* to set the app.config['SECRET_KEY']
redirect('/secure', code=307) :: this method will handle redirect to the specified URL endpoint using the HTTP POST method because we have specified the HTTP code of 307
login() :: this view function will handle any HTTP POST request made to the URL endpoint /login. On any error, the user is routed to the html error page login_error.html . On successful user verification, the key logged_user_id (in the session) is set to the logged in user's email-id and then redirect to the URL endpoint /secure
logoff() :: this view function will handle the HTTP request made to the URL endpoint /logout. This is effectively terminating the user session
@app.before_request :: the Flask decorator for a method that is invoked before each request. This allows one to check if a user is already logged-in before letting into the secure parts of the web application
secure() :: this view function will handle any HTTP POST request made to the URL endpoint /secure. It will display the html page secure_notes.html
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:
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:
WALLA !!! Click on the Logout button to go back to the login page.
References