PolarSPARC

Web Applications using Python Flask - Part III


Bhaskar S 09/11/2021


Hands-on Python Flask - Part III

Until now, all the interactions between the client (the browser) and the server (the web server) have been web page based request-response, in the sense the client made a form request to a URI and the server processed the request and responsed back with a web page (including an error page on validation failures). Modern web applications don't operate this way. Instead, once a web page is loaded on the client, the user interacts with the elements on the web page as though it was a single page and behind-the-scenes the web page makes requests to the server asynchronously to update the page elements.

Curious to know the client-side technology behind this ??? It is called AJAX and stands for Asynchronous Javascript And XML. In reality, it is not one technology, but a collection of technologies such as HTML, CSS, Javascript, Document Object Model (DOM), XML, and JSON.

For AJAX, we will leverage the Axios framework. We need to download axios.min.js and save it in the directory located at SecureNotes/static/js. We will also need the Bootstrap related Javascript file called bootstrap.min.js that needs to be downloaded and saved it in the directory located at SecureNotes/static/js.

The following is the modified version of the HTML page signup.html that makes an AJAX request to the URL at /signup:


signup.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">
        <script src="static/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
        <script src="static/js/axios.min.js"></script>
        <script src="static/js/main.js"></script>
        <title>Secure Notes - Sign Up</title>
    </head>
    <body>
        <div class="container">
            <nav class="navbar navbar-expand-md navbar-dark bg-dark">
                <p class="text-white">Secure Notes - Sign Up</a>
            </nav>
            <br/>
            <form>
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" onblur="clearSignup()" required placeholder="Enter email...">
                    <div class="col-sm-3">
                        <small id="email-err" class="text-danger"></small>      
                    </div>
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password1" name="password1" onblur="clearSignup()" required placeholder="Enter password...">
                    <div class="col-sm-3">
                        <small id="pass1-err" class="text-danger"></small>      
                    </div>
                </div>
                <div class="form-group">
                    <label for="passwordInput">Confirm Password</label>
                    <input type="password" class="form-control" id="password2" name="password2" onblur="clearSignup()" required placeholder="Confirm password...">
                    <div class="col-sm-3">
                        <small id="pass2-err" class="text-danger"></small>      
                    </div>
                </div>
                <button type="submit" class="btn btn-primary" onclick="mySignup(); return false;">Register</button>
            </form>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

Notice the use of <div class="col-sm-3"><small> under each of the input elements in the sign-up HTML page above. This is the section where we display any error response from the server.

Also, notice the use of the attribute onclick="mySignup(); return false;" on the submit button. When the user clicks on the submit button, we issue an AJAX POST request (using the Axios framework) to the server.

The following is the Javascript script called main.js that will be located in the directory SecureNotes/static/js:


main.js
//
// @Author: Bhaskar S
// @Blog:   https://www.polarsparc.com
// @Date:   01 Sep 2021
//

const config = {
    headers: {  
        'content-type': 'application/json'
    }
};

function clearSignup() {
    if (document.getElementById('email').value.length > 0)
        document.getElementById('email-err').innerText = "";
    if (document.getElementById('password1').value.length > 0)
        document.getElementById('pass1-err').innerText = "";
    if (document.getElementById('password2').value.length > 0)
        document.getElementById('pass2-err').innerText = "";
}

function mySignup() {
    const url = 'http://127.0.0.1:8080/signup';
    var data = {
        email: document.getElementById('email').value,
        password1: document.getElementById('password1').value,
        password2: document.getElementById('password2').value
    };
    axios.post(url, data, config)
        .then(
            (response) => {
                location.replace('http://127.0.0.1:8080/login');
        })
        .catch((error) => {
            if (error.response) {
                // Got an error response
                if (error.response.data.code == 1001) {
                    document.getElementById('email-err').innerText = error.response.data.error;
                } else if (error.response.data.code == 1002) {
                    document.getElementById('pass1-err').innerText = error.response.data.error;
                } else {
                    document.getElementById('pass2-err').innerText = error.response.data.error;
                }
            }
        });
}

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

When the user clicks on the submit button, we make an AJAX POST request to the URL endpoint for /signup and send the form data as a JSON payload (hence the need for the header 'content-type': 'application/json'). The server will respond with a JSON response irrespective of the outcome. If we get an error response, we accordingly update the corresponding element that caused the error.

The following is the modified version of the Python script main.py to handle the HTTP POST request at the URL endpoint for /signup:


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

from flask import request, session, redirect, jsonify
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.json:
        email = request.json['email']
    if email is None or len(email.strip()) == 0:
        return jsonify({'url': '/signup', 'code': 1001, 'error': 'Invalid email !!!'}), 400
    password1 = None
    if 'password1' in request.json:
        password1 = request.json['password1']
    if password1 is None or len(password1.strip()) == 0:
        return jsonify({'url': '/signup', 'code': 1002, 'error': 'Invalid password !!!'}), 400
    password2 = None
    if 'password2' in request.json:
        password2 = request.json['password2']
    if password1 != password2:
        return jsonify({'url': '/signup', 'code': 1003, 'error': 'Password confirmation failed !!!'}), 400
    user = User.register(email, password1)
    msg = 'User %s successfully registered!' % user
    app.logger.info(msg)
    return jsonify({'url': '/signup', 'code': 0, 'email-id': email})

@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.

Notice that there is no more references to the HTML page signup_error.html in the main Flask application and so it can be deleted from the directory SecureNotes/templates.

The following is the modified version of the Python script called config.py to allow the client (the browser) to make cross-origin AJAX requests:


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

from flask import Flask
from flask_cors import CORS
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'

CORS(app)

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()

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, click on the 'Register' button without entering any details. The following illustration shows the response on the browser:

Error Response
Figure.9

Notice how the error is now displayed right in the sign-up page below the 'Enter email...' input box. There is no new HTML page being reloaded.

Go ahead and fill in the correct information and click the 'Register' button. Now, the user is redirected to the welcome HTML page which prompts the user to login.

Shifting gears, we will now modify the welcome page in a similar way and take out the login_error.html page.

The following is the modified version of the HTML page welcome.html that makes an AJAX request to the URL at /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">
        <script src="static/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
        <script src="static/js/axios.min.js"></script>
        <script src="static/js/main.js"></script>
        <title>Secure Notes - Login</title>
    </head>
    <body>
        <div class="container">
            <nav class="navbar navbar-expand-md navbar-dark bg-dark">
                <p class="text-white">Secure Notes - Login</a>
            </nav>
            <br/>
            <form>
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" onblur="clearLogin()" required placeholder="Enter email...">
                    <div class="col-sm-3">
                        <small id="email-err" class="text-danger"></small>      
                    </div>
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password" name="password" onblur="clearLogin()" required placeholder="Enter password...">
                    <div class="col-sm-3">
                        <small id="pass-err" class="text-danger"></small>      
                    </div>
                </div>
                <button type="submit" class="btn btn-primary" onclick="myLogin(); return false;">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>

The following is the modified version of the Javascript script called main.js that will be located in the directory SecureNotes/static/js:


main.js
//
// @Author: Bhaskar S
// @Blog:   https://www.polarsparc.com
// @Date:   01 Sep 2021
//

const config = {
    headers: {  
        'content-type': 'application/json',
        'access-control-allow-origin': '*'
    }
};

function clearSignup() {
    if (document.getElementById('email').value.length > 0)
        document.getElementById('email-err').innerText = "";
    if (document.getElementById('password1').value.length > 0)
        document.getElementById('pass1-err').innerText = "";
    if (document.getElementById('password2').value.length > 0)
        document.getElementById('pass2-err').innerText = "";
}

function mySignup() {
    const url = 'http://127.0.0.1:8080/signup';
    var data = {
        email: document.getElementById('email').value,
        password1: document.getElementById('password1').value,
        password2: document.getElementById('password2').value
    };
    axios.post(url, data, config)
        .then(
            (response) => {
                location.replace('http://127.0.0.1:8080/login');
        })
        .catch((error) => {
            if (error.response) {
                // Got an error response
                if (error.response.data.code == 1001) {
                    document.getElementById('email-err').innerText = error.response.data.error;
                } else if (error.response.data.code == 1002) {
                    document.getElementById('pass1-err').innerText = error.response.data.error;
                } else {
                    document.getElementById('pass2-err').innerText = error.response.data.error;
                }
            }
        });
}

function clearLogin() {
    if (document.getElementById('email').value.length > 0)
        document.getElementById('email-err').innerText = "";
    if (document.getElementById('password1').value.length > 0)
        document.getElementById('pass-err').innerText = "";
}

function myLogin() {
    const url = 'http://127.0.0.1:8080/login';
    var data = {
        email: document.getElementById('email').value,
        password: document.getElementById('password').value
    };
    axios.post(url, data, config)
        .then(
            (response) => {
                location.replace('http://127.0.0.1:8080/secure');
        })
        .catch((error) => {
            if (error.response) {
                // Got an error response
                if (error.response.data.code == 1004) {
                    document.getElementById('email-err').innerText = error.response.data.error;
                } else {
                    document.getElementById('pass-err').innerText = error.response.data.error;
                }
            }
        });
}

The following is the modified version of the Python script main.py to handle the HTTP POST request to the URL endpoint such as /login:


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

from flask import request, session, redirect, jsonify
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 in ['/secure']:
        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.json:
        email = request.json['email']
    if email is None or len(email.strip()) == 0:
        return jsonify({'url': '/signup', 'code': 1001, 'error': 'Invalid email !!!'}), 400
    password1 = None
    if 'password1' in request.json:
        password1 = request.json['password1']
    if password1 is None or len(password1.strip()) == 0:
        return jsonify({'url': '/signup', 'code': 1002, 'error': 'Invalid password !!!'}), 400
    password2 = None
    if 'password2' in request.json:
        password2 = request.json['password2']
    if password1 != password2:
        return jsonify({'url': '/signup', 'code': 1003, 'error': 'Password confirmation failed !!!'}), 400
    user = User.register(email, password1)
    msg = 'User %s successfully registered!' % user
    app.logger.info(msg)
    return jsonify({'url': '/signup', 'code': 0, 'email-id': email})

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('welcome.html')
    email = None
    if 'email' in request.json:
        email = request.json['email']
    if email is None or len(email.strip()) == 0:
        return jsonify({'url': '/login', 'code': 1004, 'error': 'Invalid email !!!'}), 400
    password = None
    if 'password' in request.json:
        password = request.json['password']
    if password is None or len(password.strip()) == 0:
        return jsonify({'url': '/login', 'code': 1005, 'error': 'Invalid password !!!'}), 400
    user = User.query_by_email(email)
    if user is None:
        return jsonify({'url': '/login', 'code': 1004, 'error': 'Invalid email !!!'}), 400
    if not user.verify_password(password):
        return jsonify({'url': '/login', 'code': 1005, 'error': 'Invalid password !!!'}), 400
    msg = 'User %s successfully logged in!' % user
    app.logger.info(msg)
    session['logged_user_id'] = email
    return jsonify({'url': '/login', 'code': 0, 'email-id': email})

@app.route('/secure', methods=['GET'])
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')

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 a valid registered email-id, and and a wrong password and then click on the 'Login' button. The following illustration shows the response on the browser:

Invalid Password
Figure.10

Re-enter the correct email, the correct password, and then click on the 'Login' button. This time we will be taken to the secured area of the web application.

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

References

GitHub - Source Code (with AJAX)

Web Applications using Python Flask - Part II

Web Applications using Python Flask - Part I


© PolarSPARC