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:
<!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:
// // @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.
document.getElementById(...) :: returns an DOM object representing the element whose id property matches the specified string
document.getElementById(...).value :: allows one to access the value associated with the corresponding DOM element
axios.post(url, data, config) :: asynchronous AJAX style POST of the specified JSON data from the browser to the specified url
.then((response) => { ... }) :: represents the Axios callback on successful response from the server
.catch((error) => { ... }) :: represents the Axios callback on failure
document.getElementById(...).innerText :: allows one to access the text represented by the corresponding DOM element
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:
# # @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.
request.json :: returns the parsed JSON data if the incoming request data is of type 'application/json'
jsonify :: serializes the response data as JSON data and sets the response data type as 'application/json'
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:
# # @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:
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:
<!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:
// // @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:
# # @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:
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