Flask Webapp - Verify Email after Registration - Best Practice - python

I've been following along to Corey Schafer's awesome youtube tutorial on the basic flaskblog. In addition to Corey's code, I`d like to add a logic, where users have to verify their email-address before being able to login. I've figured to do this with the URLSafeTimedSerializer from itsdangerous, like suggested by prettyprinted here.
The whole token creation and verification process seems to work. Unfortunately due to my very fresh python knowledge in general, I can't figure out a clean way on my own how to get that saved into the sqlite3 db. In my models I've created a Boolean Column email_confirmed with default=False which I am intending to change to True after the verification process. My question is: how do I best identify the user (for whom to alter the email_confirmed Column) when he clicks on his custom url? Would it be a good practice to also save the token inside a db Column and then filter by that token to identify the user?
Here is some of the relevant code:
User Class in my modely.py
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
image_file = db.Column(db.String(20), nullable=False, default='default_profile.jpg')
password = db.Column(db.String(60), nullable=False)
date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
email_confirmed = db.Column(db.Boolean(), nullable=False, default=False)
email_confirm_date = db.Column(db.DateTime)
projects = db.relationship('Project', backref='author', lazy=True)
def get_mail_confirm_token(self, expires_sec=1800):
s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'], expires_sec)
return s.dumps(self.email, salt='email-confirm')
#staticmethod
def verify_mail_confirm_token(token):
s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
try:
return s.loads(token, salt='email-confirm', max_age=60)
except SignatureExpired:
return "PROBLEM"
Registration Logic in my routes (using a users blueprint):
#users.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('dash.dashboard'))
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user = User(username=form.username.data, email=form.email.data, password=hashed_password)
db.session.add(user)
db.session.commit()
send_mail_confirmation(user)
return redirect(url_for('users.welcome'))
return render_template('register.html', form=form)
#users.route('/welcome')
def welcome():
return render_template('welcome.html')
#users.route('/confirm_email/<token>')
def confirm_email(token):
user = User.verify_mail_confirm_token(token)
current_user.email_confirmed = True
current_user.email_confirm_date = datetime.utcnow
return user
The last parts current_user.email_confirmed = True and current_user.email_confirm_date =datetime.utcnow are probably the lines in question. Like stated above the desired entries aren't made because the user is not logged in at this stage, yet.
Im grateful for any help on this!
Thanks a lot in advance!

Thanks to #exhuma. Here is how I eventually got it to work - also in addition I'm posting the previously missing part of email-sending.
User Class in my models.py
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
image_file = db.Column(db.String(20), nullable=False, default="default_profile.jpg")
password = db.Column(db.String(60), nullable=False)
date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
email_confirmed = db.Column(db.Boolean(), nullable=False, default=False)
email_confirm_date = db.Column(db.DateTime)
projects = db.relationship("Project", backref="author", lazy=True)
def get_mail_confirm_token(self):
s = URLSafeTimedSerializer(
current_app.config["SECRET_KEY"], salt="email-comfirm"
)
return s.dumps(self.email, salt="email-confirm")
#staticmethod
def verify_mail_confirm_token(token):
try:
s = URLSafeTimedSerializer(
current_app.config["SECRET_KEY"], salt="email-confirm"
)
email = s.loads(token, salt="email-confirm", max_age=3600)
return email
except (SignatureExpired, BadSignature):
return None
Send Mail function in my utils.py
def send_mail_confirmation(user):
token = user.get_mail_confirm_token()
msg = Message(
"Please Confirm Your Email",
sender="noreply#demo.com",
recipients=[user.email],
)
msg.html = render_template("mail_welcome_confirm.html", token=token)
mail.send(msg)
Registration Logic in my routes.py (using a users blueprint):
#users.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("dash.dashboard"))
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode(
"utf-8"
)
user = User(
username=form.username.data, email=form.email.data, password=hashed_password
)
db.session.add(user)
db.session.commit()
send_mail_confirmation(user)
return redirect(url_for("users.welcome"))
return render_template("register.html", form=form)
#users.route("/welcome")
def welcome():
return render_template("welcome.html")
#users.route("/confirm_email/<token>")
def confirm_email(token):
email = User.verify_mail_confirm_token(token)
if email:
user = db.session.query(User).filter(User.email == email).one_or_none()
user.email_confirmed = True
user.email_confirm_date = datetime.utcnow()
db.session.add(user)
db.session.commit()
return redirect(url_for("users.login"))
flash(
f"Your email has been verified and you can now login to your account",
"success",
)
else:
return render_template("errors/token_invalid.html")
Only missing from my point of view is a simple conditional logic, to check if email_confirmed = True before logging in, as well as the same check inside the confirm_email(token) function to not make this process repeatable in case the user clicks on the confirmation link several times. Thanks again! Hope this is of some help to anyone else!

The key to your question is this:
My question is: how do I best identify the user (for whom to alter the email_confirmed Column) when he clicks on his custom url?
The answer can be seen in the example on URL safe serialisation using itsdangerous.
The token itself contains the e-mail address, because that's what you are using inside your get_mail_confirm_token() function.
You can then use the serialiser to retrieve the e-mail address from that token. You can do that inside your verify_mail_confirm_token() function, but, because it's a static-method you still need a session. You can pass this in as a separate argument though without problem. You also should treat the BadSignature exception from itsdangerous. It would then become:
#staticmethod
def verify_mail_confirm_token(session, token):
s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
try:
email = s.loads(token, salt='email-confirm', max_age=60)
except (BadSignature, SignatureExpired):
return "PROBLEM"
user = session.query(User).filter(User.email == email).one_or_none()
return user
Would it be a good practice to also save the token inside a db Column and then filter by that token to identify the user?
No. The token should be short-lived and should not be kept around.
Finally, in your get_mail_confirm_token implementation you are not using the URLSafeTimedSerializer class correctly. You pass in a second argument called expires_sec, but if you look at the docs you will see that the second argument is the salt, which might lead to unintended problems.

Related

Custom Flask Validation Error not triggering getting Unique constraint error instead

I have created a registration form with validation but instead of this error Unique Constraint error is coming how can I fix this
Below is the register class
class Registerform(FlaskForm):
username = StringField(label="Username", validators=[
Length(min=3, max=20), DataRequired()])
password1 = PasswordField(label="Password", validators=[
Length(min=6, max=20), DataRequired()])
password2 = PasswordField(label="Confirm Password", validators=[
EqualTo('password1', message='Both password fields must be equal!'), DataRequired()])
Submit = SubmitField(label='Submit')
def validate_existing_user(self, username):
checkexisting = User.query.filter_by(username=username.data).first()
print(checkexisting)
if checkexisting:
raise ValidationError(
"Oops Username already exists please try with a new Username")
Below is the registration route
#app.route("/register", methods=['POST', 'GET'])
def registerpage():
form = Registerform()
if form.validate_on_submit():
usernameinput = form.username.data
userpasswordinput = form.password1.data
hashedpass = bcrypt.generate_password_hash(userpasswordinput)
print(f"Normal pass is {userpasswordinput} & its hash is {hashedpass}")
table_row = User(username=usernameinput,
password_hash=hashedpass, )
db.session.add(table_row)
db.session.commit()
print("User added successfully")
flash('User added successfully you can Login now',
category='success')
return redirect(url_for('login_page'))
return render_template("registration.html", title='Registration', form=form)
Below is the DB model its sqllite
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(120), nullable=False)
def __repr__(self):
return '<Username %s> <pwd %s>' % (self.username, self.password_hash)
Kindly assist I am new to flask
The Documentation explains how to build a custom validator. The key is to name the validation methode like the attribute of your form.
So instead of using validate_existing_user() you have to name the methode validate_username. That way wtforms will know that it hast to map the custom-validation methode to the username attribute.

Flask Form Won't Write to SQLAlchemy DB

I have created ContactForm as a quick WTForm within my HTML templates. When I go through my application and try to use the contact form to add the name and email values in the ContactForm to my User class it doesn't work. When I query User class in DB I just get empty brackets []. Somebody, please help me!
class ContactForm(FlaskForm):
name = StringField('Name', [InputRequired()])
email = StringField('Email Address', [InputRequired()])
submit = SubmitField('Sign Up')
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=False, nullable=False)
email = db.Column(db.String, unique=False, nullable=False)
def __init__(self, name, email):
self.name = name
self.email = email
def __repr__(self, name, email):
return f"{name},{email}"
#app.route('/contact', methods=['GET'])
def contact():
form = ContactForm()
if form.validate_on_submit():
name = form.name.data
email = form.email.data
user = User(name, email)
db.session.add(user)
db.session.commit()
flash('Success')
return redirect(url_for('home'))
else:
return render_template('contact.html', form=form)
return render_template('contact.html', form=form)
Please try with methods POST in your route.

Passing different form data entries to the same row in a database with Python Flask and SQLAlchemy

I've created a form which takes user's name and their email address. I get this data from the form and put it into a sqlite3 database in the following way:
#app.route('/my_form', methods=["GET", "POST"])
def form_data():
if request.method == "POST":
user_name = request.form["name"]
new_user = form_database(name=user_name)
user_email = request.form["email"]
new_user_email = form_database(email=user_email)
try:
db.session.add(new_user)
db.session.add(new_user_email)
db.session.commit()
return redirect("/my_form")
Current result: each data entry gets recorded into a new row:
1|Jack||||||||||
2||svisto#hotmail.com|||||||||
Desirable result: each data entry gets recorded into the same row:
1|Jack|svisto#hotmail.com|||||||||
Question: How can I change the code such that I get the desirable result?
Lets say you have a User class in your model like this:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer,
primary_key=True)
username = db.Column(db.String(32),
index=False,
unique=True,
nullable=False)
email = db.Column(db.String(64),
index=True,
unique=True,
nullable=False)
Then you can do this in your try block
try:
new_user = User(user_name=user_name,
email=email)
db.session.add(new_user)
db.session.commit()
Solution I found:
I fused:
new_user = form_database(name=user_name) and new_user_email = form_database(email=user_email)
together such that the code looks like this:
#app.route('/my_form', methods=["GET", "POST"])
def form_data():
if request.method == "POST":
user_name = request.form["name"]
user_email = request.form["email"]
new_user_details = form_database(name=user_name, email=user_email)#assigns 2 form inputs for both columns in the database model to the same variable
try:
db.session.add(new_user_details)#adds that variable to the database as one entry, hence in one row but different columns
db.session.commit()

Receiving 'TypeError' when attempting to import and create a db using SQLAlchemy

I have started work on a project and have decided before I get too deep to get my ORM sorted out (this means going back over and re writing my classes). I believe my code is correct but I am getting a 'TypeError' whenever I try to create the database schema using Python. I load up the Python console whilst in my project directory, and type 'from app import db' which works fine. After the command 'db.create_all()' (which worked using a basic example) I am thrown the error:
'File
"C:\Users\owner\AppData\Local\Programs\Python\Python38-32\lib\site-packages\sqlalchemy\dialects\mysql\base.py",
line 2016, in visit_VARCHAR
return self._extend_string(type_, {}, "VARCHAR(%d)" % type_.length) TypeError: %d format: a number is required, not type'
I am given no clue as to where in my actual code the cause could be, and am at a total loss.
Any help would be great - a lot of code is commented out and some other code left in for now as I'm in the process of adding my ORM but it was implemented without SQLAlchemy initially. Below is my code for 'init.py':
from flask import Flask, render_template, url_for, flash, redirect, g, request, session, send_from_directory, send_file
from heychef.config import Config
from flask_sqlalchemy import SQLAlchemy
import os
import bcrypt
import uuid
import json
import ssl
from datetime import datetime
from datetime import timedelta
from heychef.models.Agency import Agency
from heychef.models.User import User
from heychef.models.Shift import Shift
from flaskext.mysql import MySQL
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
from heychef.models.Agent import Agent
#app.before_request
def before_request():
g.agent = None
g.agency = None
if 'agent' in session:
g.agent = session['agent']
if 'agency' in session:
g.agency = session['agency']
##########################################################
############# Agent Routes ################
##########################################################
#app.route("/")
def home():
return render_template('agent-views/signup.html'), 200
#app.route("/agent-signup", methods = ['POST', 'GET'])
def agentSignup():
if request.method == 'POST':
email = request.form['inputEmail']
firstName = request.form['inputFirstName']
secondName = request.form['inputSecondName']
password = request.form['inputPassword']
rPassword = request.form['inputConfirmPassword']
if(password != rPassword):
flash('Passwords do not match.', 'danger')
return render_template('agent-views/signup.html')
else:
hashedPwd = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
agent = Agent(firstName, secondName, email, hashedPwd)
agent.addAgent(self)
flash('Account created, you can log in now.', 'success')
return render_template('agent-views/signup.html')
#app.route("/agent-login", methods = ['POST','GET'])
def agentLogin():
if request.method == 'POST':
session.pop('agent', None)
email = request.form['inputEmail']
pwd = request.form['inputPassword']
connection = mysql.get_db()
cursor = connection.cursor()
result, msg = Agent.agentLogin(connection, cursor, email, pwd)
if(result):
session['agent'] = email
flash(msg, 'success')
return redirect(url_for('agentDashboard'))
else:
flash(msg, 'danger')
return render_template('agent-views/login.html')
else:
return render_template('agent-views/login.html')
#app.route("/agent-dashboard", methods = ['GET'])
def agentDashboard():
if g.agent:
return render_template('agent-views/dashboard.html'), 200
else:
msg = "Please log in"
flash(msg, 'warning')
return redirect(url_for('agentLogin'))
if __name__ == "__main__":
app.run()
and here is my Agent Class:
from heychef.models.User import User
#from heychef.models.data.AgentDb import AgentDb
from heychef import db
class Agent(db.Model):
agentId = db.Column(db.Integer, unique=True, nullable=False, primary_key=True)
firstName = db.Column(db.String(80), unique=False, nullable=False, primary_key=False)
secondName = db.Column(db.String(80), unique=False, nullable=False, primary_key=False)
email = db.Column(db.String(80), unique=False, nullable=False, primary_key=False)
level = db.Column(db.String(80), unique=False, nullable=True, primary_key=False)
agencyId = db.Column(db.Integer, unique=False, nullable=True, primary_key=False)
addressLine1 = db.Column(db.String(80), unique=False, nullable=True, primary_key=False)
addressLine2 = db.Column(db.String(80), unique=False, nullable=True, primary_key=False)
addressLine3 = db.Column(db.String(80), unique=False, nullable=True, primary_key=False)
postcode = db.Column(db.String(20), unique=False, nullable=True, primary_key=False)
passwordHash = db.Column(db.String(256), unique=False, nullable=False, primary_key=False)
def __repr__(self):
return "<Agent(firstName='%s', lastName='%s')>" % (self.firstName, self.lastName)
def __init__(self, firstName, secondName, email, hashedPwd):
self.agentId = 1001
self.firstName = firstName
self.secondName = secondName
self.email = email
self.passwordHash = hashedPwd
#staticmethod
def agentEmailExists(cursor, email):
exists = AgentDb.agentEmailExistsDb(cursor, email)
return exists
#staticmethod
def addAgent(agent):
db.session.add(self)
db.session.commit()
#staticmethod
def agentLogin(connection, cursor, email, pwd):
failMsg = 'Email or Password does not match.'
successMsg = 'Successfully logged in.'
result = AgentDb.agentLoginDb(connection, cursor, email, pwd)
if(result):
msg = successMsg
else:
msg = failMsg
return result, msg
#staticmethod
def getWork(cursor):
work = AgentDb.getWorkDb(cursor)
return work
any help would be ace as I'm really struggling!
Many thanks
The error seems to be related to a VARCHAR (String)db column and points out that it has received a Non-Integer value ,so there might be a db.string() model field with a Non-Integer or empty parenthesis in the other model Classes in your project.

flask add a new row based on id of other tables

I have two tables, named projects and actions and every project contain several action
class Projet(db.Model):
__tablename__ = 'projets'
id = db.Column(db.Integer, primary_key=True)
nom_projet = db.Column(db.String(100))
description_projet = db.Column(db.String(800))
date_affectation = db.Column(db.DateTime, nullable = False)
statut_projet = db.Column(db.String(100))
admin_id = db.Column(db.Integer, db.ForeignKey('admins.id'))
actions = db.relationship('Action', backref='projet',
lazy='dynamic')
def __repr__(self):
return '<Projet: {}>'.format(self.id)
class Action(db.Model):
__tablename__ = 'actions'
id = db.Column(db.Integer, primary_key=True)
projet_id = db.Column(db.Integer, db.ForeignKey('projets.id'))
description = db.Column(db.String(1000))
statut_action = db.Column(db.String(100))
date_action = db.Column(db.DateTime, nullable = False)
date_execution = db.Column(db.DateTime, nullable = True)
def __repr__(self):
return '<Action: {}>'.format(self.id)
my problem is, I need to create a new action based on an existing project as shown in image,
I need to click on add button and he must redirect me to action form with the name of project auto-selected, and I entre the action details.
this is my first code to add action:
#auth.route('/action/add', methods=['GET', 'POST'])
#login_required
def add_action():
form = ActionForm()
if form.validate_on_submit():
action = Action(
projet = form.projet.data,
description = form.description.data,
statut_action = form.statut_action.data,
date_action = form.date_action.data,
date_execution = form.date_execution.data
)
try:
db.session.add(action)
db.session.commit()
flash('You have successfully added a new action.')
except:
flash('Error: action name already exists.')
return redirect(url_for('auth.list_projets'))
return render_template('admin/actions/action.html', action="Add", form=form,
title="ADD ACTION")
Steps:
Update the URL to include project_id as path param: ex: /project/1/actions/add is meant to load a page with new action form for project with id 1
Update the links to add new action in the previous page(as shown in the screenshot) as per step 1
Remove project field from ActionForm as it is handled using path param
Update "new action form" page to show product name coming in product_name variable
Try,
#auth.route('/project/<project_id>/action/add', methods=['GET', 'POST'])
#login_required
def add_action(project_id):
form = ActionForm()
project = Project.query.get(project_id)
if not project:
flash('Error: invalid project')
abort(404)
if form.validate_on_submit():
action = Action(
project = project,
description = form.description.data,
statut_action = form.statut_action.data,
date_action = form.date_action.data,
date_execution = form.date_execution.data
)
try:
db.session.add(action)
db.session.commit()
flash('You have successfully added a new action.')
except:
flash('Error: action name already exists.')
return redirect(url_for('auth.list_projets'))
return render_template('admin/actions/action.html', action="Add", form=form,
title="ADD ACTION", project_name=project.name)

Categories

Resources