3
\$\begingroup\$

I'm working on a personal project (Flask CRUD app) and I am currently building the user service. I am trying to use as less as libraries as possible (that's why I do not use WTF-forms for example, just for learning purposes). I am really not sure about a couple of things.

I am aware that form validation should be handled in both front and back-end (or at the very least on the back-end). In order to validate a new user, I am (currently) checking 3 things:

1) email does not already exist on the db (can only be verified on the back-end)

2) password is strong enough (can be done in both front and back end)

3) password and password_confirm matches (can be done in both front and back end)

My goal is to display a small error message on the registration page if either at least one those errors appears. Currently, I am checking option 1, 2 and 3 and the back-end but only return True if the registration is valid otherwise, False.

Since case 1 and 2 can directly be handle also on the front-end, can I display the error message using JavaScript and not from the back-end? I am still confused on what would be the best return for validate_registration (currently a Boolean).

Also, I am not quite sure my code is over-engineered, I made a RegistrationForm class just to validate a registration form. I did that because I am able to pick validations methods from my validators.py which may be used in other Classes as well.

So this is what I did so for for the registration form:

user/blueprints/routes.py

@user.route('/new-user',methods = ['POST'])
def register_user():
    form_email = request.form.get('email')
    form_password = request.form.get('psw')
    form_password_repeat = request.form.get('psw-repeat')
    registration_form = RegistrationForm(form_email, form_password, form_password_repeat).validate_registration()
    
    if registration_form:
        new_user = UserService().register_user(form_email, form_password)
        user_repository = UserRepository(conn, 'users')
        user_repository.add_user(new_user)
        user_repository.save()
        return "ok" #will probably change return statements later on
    return "not ok" #will probably change return statements later on

user/blueprints/forms.py

from prepsmarter.blueprints.user.validators import email_already_in_use, password_matching

class RegistrationForm():
    def __init__(self, email, pwd_1, pwd_2):
        self.email = email
        self.pwd_1 = pwd_1
        self.pwd_2 = pwd_2

    def validate_registration(self):
        is_valid = email_already_in_use(self.email) and is_strong_password(self.pwd_1) and password_matching(self.pwd_1, self.pwd_2)
        return is_valid

class LoginForm():
 # to do

user/blueprints/validators.py

import re 
from email_validator import validate_email, EmailNotValidError
from prepsmarter.extensions import conn

def is_strong_password(password):
    length_error = len(password) < 8
    digit_error = re.search(r"\d", password) is None
    uppercase_error = re.search(r"[A-Z]", password) is None
    return length_error and digit_error and uppercase_error
    
def is_email_formated_correctly(email):
    is_correct = True
    try:
        validate_email(email)
    except EmailNotValidError:
        is_correct = False
    return is_correct

def password_matching(pwd_1, pwd_2):
    return pwd_1 == pwd_2

    
def email_already_in_use(email):
    sql = "SELECT CASE WHEN EXISTS ( SELECT * FROM users WHERE email = (%s)) THEN 1 ELSE 0 END;"
    cursor = conn.cursor()
    cursor.execute(sql, (email))
    res = cursor.fetchall()
    return res == 1 
\$\endgroup\$
3
\$\begingroup\$

I have not thought carefully about all of your code and questions, but here's one suggested technique for how to handle validation code more simply. In most contexts, people find it easier to understand affirmative boolean logic rather than negated logic. My guess is that the bug in is_strong_password() stems from that phenomenon (the function tells me hi is strong, for example). You've created 3 variables to be true when something is wrong but then combined them under the opposite assumption. Here are two alternatives to consider:

# This alternative uses affirmative variables: True means OK. Also notice
# how the variable names are a bit more intuitive because of the
# affirmative framing. When possible, try to name boolean variables to
# convey their nature: is_X, has_X, contains_X, etc. Your current code has
# variables names implying that they each hold an "error" of some kind --
# but they do not. Finally, the usage of bool() here is not strictly
# necessary, but it does convey intent explicitly.

def is_strong_password(password):
    is_long = len(password) >= 8
    has_digit = bool(re.search(r"\d", password))
    has_upper = bool(re.search(r"[A-Z]", password))
    return is_long and has_digit and has_upper
    
# This alternative illustrates a handy code-layout technique
# that often works well when doing validation. This version
# is shorter (sometimes nice) but less explicit. In this
# case, I would tend to favor the short version, because the
# checks are so simple. If the complexity were higher, I would
# prefer the approach that names every check to increase clarity

def is_strong_password(password):
    return (
        len(password) >= 8 and
        re.search(r"\d", password) and  
        re.search(r"[A-Z]", password)
    )

The is_email_formated_correctly() can be simplified by doing things more directly. In addition, if validate_email() returns True/False you could simplify further by just returning its return value.

def is_email_formated_correctly(email):
    try:
        validate_email(email)
        return True
    except EmailNotValidError:
        return False
\$\endgroup\$
1
  • \$\begingroup\$ Thank you again for your feedback, very constructive. Today I am working on the email confirmation during registration, I may submit again today \$\endgroup\$ Mar 6 at 15:31
2
\$\begingroup\$

Since case 1 and 2 can directly be handle also on the front-end, can I display the error message using JavaScript and not from the back-end?

Sure, you can do some front-end validation using Javascript, when the user submits the form. But you should still perform the same validation on the backend because there is no guarantee that Javascript is enabled on the client. Remember the client cannot be trusted.

I don't know if this is already part of your code, but the goal also should be to present a clear message outlining the errors detected while processing the form eg:

  • password must contain at least 8 characters including digits and uppercase letters
  • password and password confirmation do not match

Personally I would use a list like this if I want to test multiple conditions:

errors = []
if not password_matching(pwd_1, pwd_2):
    errors.append('Password and password confirmation do not match')
...

And then if len(errors) > 0, you know that errors were found and you can display them back to the client. You can pass the list of errors as a parameter using render_template for example. In fact you will probably be passing the form object back to render_template to populate the form as it was submitted.

If you will be using a CSS/JS framework like Bootstrap, there is already some client-side validation baked in that you can use: https://getbootstrap.com/docs/5.0/forms/validation/

Instead of:

sql = "SELECT CASE WHEN EXISTS ( SELECT * FROM users WHERE email = (%s)) THEN 1 ELSE 0 END;"

You could simply have:

sql = "SELECT COUNT(*) FROM users WHERE email = (%s)"

That should either return 0 or 1 but make sure email has a unique constraint in your DB.

And although you say you want to use as few libraries as possible you have actually implemented some WTF-forms functionality in the form of validators. This is good for learning purposes but I think there is no real advantage here.

I also have the impression that you implemented your own ORM in lieu of SQL Alchemy. I am not sure it is worth reinventing the wheel unless there is a clear gain in simplicity of flexibility that is not easily achieved with the existing tools.

Note that there are Flask implementations for WTF and Sql Alchemy respectively.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.