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
2 Answers 2
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
-
\$\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\$Pierre-Alexandre– Pierre-Alexandre2021年03月06日 15:31:42 +00:00Commented Mar 6, 2021 at 15:31
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.
Explore related questions
See similar questions with these tags.