This tutorial details how to verify their email address during the user registration process.
On the workflow, a confirmation letter will be sent to the user after registering for a new account. Until the user has completed the "verification" in the message, their account will remain in an "unverified" state. This is the workflow that most Web applications use.
One of the important things about this is what permissions do unauthenticated users have? Or do they have full access to your app, or are they restricted or have no permissions at all? For the app in this tutorial, unauthenticated users who sign in to a page will be alerted that they can enter the app only if they have verified the account.
Before we start, a lot of the features we want to add are flask-users and flask-security extensions--the problem is, why not just use these two extensions? Well, first of all, it's a learning opportunity. At the same time, both extensions have limitations, such as supported databases. What if you want to use RETHINKDB?
Here we go
Flask Basic Registration
We are going to start a flask sample, which includes the basic user registration. Get the code base from this GitHub repository. Once you have created and activated the VIRTUALENV, run the following command to get started quickly:
$ pip install -r requirements.txt
$ export APP_SETTINGS="project.config.DevelopmentConfig"
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
$ python manage.py runserver
With the app running, visit the Http://localhost:5000/register page and register for a new user. Note that the app will automatically log in and guide you to the main page after registration. Take a look and then run the code--especially the user's Blueprint (Blueprint is a concept of flask).
Stop the server when finished.
Update Current App
Model
First, let's add the Confirmed field to the user model in our project/models.py:
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
registered_on = db.Column(db.DateTime, nullable=False)
admin = db.Column(db.Boolean, nullable=False, default=False)
confirmed = db.Column(db.Boolean, nullable=False, default=False)
confirmed_on = db.Column(db.DateTime, nullable=True)
def __init__(self, email, password, confirmed,
paid=False, admin=False, confirmed_on=None):
self.email = email
self.password = bcrypt.generate_password_hash(password)
self.registered_on = datetime.datetime.now()
self.admin = admin
self.confirmed = confirmed
self.confirmed_on = confirmed_on
Notice how this area defaults to "False". Also add a confirmed_on field, which is a DateTime. I want to include this datetime in order to analyze the difference between registered_on and confirmed_on dates with queue analysis.
Let's create the database completely from scratch and migrate it! So, first delete the database dev.sqlite, and the Migration folder.
Control commands
Next, in manage.py, update the create_admin command to make the new database field effective:
@manager.command
def create_admin():
"""Creates the admin user."""
db.session.add(User(
email="ad@min.com",
password="admin",
admin=True,
confirmed=True,
confirmed_on=datetime.datetime.now())
)
db.session.commit()
Be sure to import datetime. Now, let's run the following command again:
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
Register () View function
Finally, before we register the user again, we need to change the register () view function in project/user/views.py ...
user = User(
email=form.email.data,
password=form.password.data
)
Change to the following:
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
Do you understand? Think about why the confirmed default to False.
Yes, good. Run the application again. Transfer to Http://localhost:5000/register, and then register a new user. If you open your SQLite database in the SQLite browser, you will see:
Well, this is my newly registered user, michael@realpython.com is not verified. Let's verify it.
Add Email verification
Generating a validation token
Message validation should include a special URL that allows the user to verify his or her account simply by clicking on it. Ideally, this URL should look like this –http://yourapp.com/confirm/. The key here is its ID. In this ID, use the itsdangerous package to encode the user's message (including the timestamp).
To create a file called project/token.py, add the following code:
# project/token.py
from itsdangerous import URLSafeTimedSerializer
from project import app
def generate_confirmation_token(email):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
def confirm_token(token, expiration=3600):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
try:
email = serializer.loads(
token,
salt=app.config['SECURITY_PASSWORD_SALT'],
max_age=expiration
)
except:
return False
return email
So in the Generate_confirmation_token () function, a token is generated by Urlsafetimedserializer with the email address that is obtained when the user registers. The _ Real _email was coded in the token. After confirming the token, in the Confirm_token () function, we can use the loads () method, which takes over the token and its expiration time--one hours (3,600 seconds) in effect--as a parameter. As long as the token does not expire, it will return an email.
In your app's configuration (Baseconfig ()), make sure to add Security_password_salt:
Security_password_salt = ' My_precious_two '
Update register () view function
Now update the Register () view function from project/user/views.py:
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
db.session.add(user)
db.session.commit()
token = generate_confirmation_token(user.email)
Also, make sure that these import modules are updated:
From Project.token import Generate_confirmation_token, Confirm_token
Process Email Verification
Next, add a new view to resolve the email verification:
@user_blueprint.route('/confirm/')
@login_required
def confirm_email(token):
try:
email = confirm_token(token)
except:
flash('The confirmation link is invalid or has expired.', 'danger')
user = User.query.filter_by(email=email).first_or_404()
if user.confirmed:
flash('Account already confirmed. Please login.', 'success')
else:
user.confirmed = True
user.confirmed_on = datetime.datetime.now()
db.session.add(user)
db.session.commit()
flash('You have confirmed your account. Thanks!', 'success')
return redirect(url_for('main.home'))
Add this to the project/user/views.py. Again, make sure that these imports are updated:
Import datetime
Now we call the Confirm_token () function from the token. If successful, we update the user, change the Email_confirmed property to True, and set the DateTime to the time the validation occurred. Also, if the user has already done the verification process-and has verified-we want to remind the user of this.
Create an email template
Next, add a basic email template:
welcome! Thanks for signing up. Please follow the link to activate the Your account:
{{Confirm_url}}
cheers!
Save this in "Project/templates/user" as a activate.html. This uses a simple variable called Confirm_url, which is created in the Register () view function.
Send mail
Create a basic function to send mail with a little help from the Flask-mail already installed and set in project/__init__.py.
To create a file called email.py:
# project/email.py
from flask.ext.mail import Message
from project import app, mail
def send_email(to, subject, template):
msg = Message(
subject,
recipients=[to],
html=template,
sender=app.config['MAIL_DEFAULT_SENDER']
)
mail.send(msg)
Save it in the project folder.
So, we simply have to deal with the recipient list, the subject, the template. We'll handle the settings for the mail configuration at 1.1.
In the project/user/views.py (again!) ) Update register () view function
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
db.session.add(user)
db.session.commit()
token = generate_confirmation_token(user.email)
confirm_url = url_for('user.confirm_email', token=token, _external=True)
html = render_template('user/activate.html', confirm_url=confirm_url)
subject = "Please confirm your email"
send_email(user.email, subject, html)
login_user(user)
flash('A confirmation email has been sent via email.', 'success')
return redirect(url_for("main.home"))
return render_template('user/register.html', form=form)
Also add the following import module:
From Project.email import Send_email
We are here to integrate everything together. The basic function of this function is as a controller (direct or indirect):
- Process Initial Registration,
- Generates a token and a confirmation URL,
- Send confirmation email,
- Quick verification,
- User log in,
- Change the user.
Did you notice the _external=true parameter? This adds the full URL that contains the hostname and port (in our case, http://localhost:5000).
Before we test this, we're going to set the email settings.
Mail
Update Baseconfig () in project/config.py:
class BaseConfig(object):
"""Base configuration."""
# main config
SECRET_KEY = 'my_precious'
SECURITY_PASSWORD_SALT = 'my_precious_two'
DEBUG = False
BCRYPT_LOG_ROUNDS = 13
WTF_CSRF_ENABLED = True
DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
# mail settings
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
# gmail authentication
MAIL_USERNAME = os.environ['APP_MAIL_USERNAME']
MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']
# mail accounts
MAIL_DEFAULT_SENDER = 'from@example.com'
View official Flask-mail documentation for more information
If you already have a Gmail account, you can use it or sign up for a test Gmail account. The environment variables are then temporarily set in the current shell:
$ export App_mail_username= "foo" $ export app_mail_password= "bar"
If your Gmail account has two-step authorization, Google will block it out.
Start testing Now!
First Test
Open the app and transfer to Http://localhost:5000/register. Then register with the email address you can log in to. Well, you should receive an email that looks like this:
Click on the URL and you will be transferred to Http://localhost:5000/. Ensure that the user is in the database, that the ' Confirmed ' field is true, that there is a datetime and confirmed_on field bound together.
Good!
Handling licenses
If you remember, at the beginning of the tutorial, we decided that "unauthenticated users can log in but they will immediately be transferred to a page-what we call the/unconfirmed path-reminding users that they need to verify their account to use the app."
So, we're going to--
- Add/unconfirmed Path
- Add unconfirmed.html Template
- Update register () view function
- Creating adorners
- Update navigation.html templates
- Add/unconfirmed Path
Add the following path project/user/views.py:
@user_blueprint.route('/unconfirmed')
@login_required
def unconfirmed():
if current_user.confirmed:
return redirect('main.home')
flash('Please confirm your account!', 'warning')
return render_template('user/unconfirmed.html')
You've seen similar code, so let's go ahead.
Add unconfirmed.html Template
welcome!
You don't have a confirmed your account. Please check the your inbox (and your spam folder)-You should has received an e-mail with a confirmation link.
Didn ' t get the email? Resend.
{% Endblock%}
In "Project/templates/user", save this as unconfirmed.html. This should be straightforward. Now, in order to resend the verification email, only a fake URL is added. We're going to fix it next.
Update register () view function
Now simply put in:
Return Redirect (Url_for ("Main.home"))
Become:
Return Redirect (Url_for ("user.unconfirmed"))
Therefore, after sending the verification email, the user will enter the/unconfirmed path.
Creating adorners
# project/decorators.py
from functools import wraps
from flask import flash, redirect, url_for
from flask.ext.login import current_user
def check_confirmed(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if current_user.confirmed is False:
flash('Please confirm your account!', 'warning')
return redirect(url_for('user.unconfirmed'))
return func(*args, **kwargs)
return decorated_function
Here we use a basic function to check whether the user is authenticated. If not verified, the user enters the/unconfirmed path. In the "Project" directory, save this as decorators.py.
Now decorate the profile () View function:
@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
... snip ...
Ensure that the adorner is imported:
From project.decorators import check_confirmed
Update navigation.html templates
Finally, update the next section of the navigation.html template--
Put
{% if current_user.is_authenticated ()%}
- Profile
{% ENDIF%}
Become:
{% if current_user.confirmed and current_user.is_authenticated ()%}
- Profile
{% elif current_user.is_authenticated ()%}
- Confirm
{% ENDIF%}
It's time to test again!
Second Test
Open the app and register again with the email address you can login to. (You can delete the old user you previously registered from the database, so you can use it again) now, the registration will be transferred to Http://localhost:5000/unconfirmed.
Make sure to test the http://localhost:5000/profile path. This will bring you to http://localhost:5000/unconfirmed.
Verify your email and you'll have access to the full page. Go on, Get it!
Send email again
Finally, to do a re-send link. Add the following view function to project/user/views.py:
@user_blueprint.route('/resend')
@login_required
def resend_confirmation():
token = generate_confirmation_token(current_user.email)
confirm_url = url_for('user.confirm_email', token=token, _external=True)
html = render_template('user/activate.html', confirm_url=confirm_url)
subject = "Please confirm your email"
send_email(current_user.email, subject, html)
flash('A new confirmation email has been sent.', 'success')
return redirect(url_for('user.unconfirmed'))
Now update the unconfirmed.html template:
welcome!
You don't have a confirmed your account. Please check the your inbox (and your spam folder)-You should has received an e-mail with a confirmation link.
Didn ' t get the email? Resend.
{% Endblock%}
Third-time Test
You know the drill, this time promise to resend a new confirmation email, test link. There should be no problem.
Finally, if you send yourself several verification letters, what happens? Does every seal work? Test it for a moment. Register a new user and send some new verification letters. Verify the first Test. Is it OK? It should be possible. Is that OK? Do you think that if the new mail, the other email should be invalid?
Do some research on this kind of thing. Test the other web apps you use. How do they deal with this kind of behavior?
Update test Suite
Good. This is for the main function. How do we update the current test suite? Because of it, um, there's a problem.
To run the test:
$ python manage.py test
You will see the following error:
TypeError: __init__ () takes at least 4 arguments (3 given)
To correct it, just update the setup () method in project/util.py:
def setUp(self):
db.create_all()
user = User(email="ad@min.com", password="admin_user", confirmed=False)
db.session.add(user)
db.session.commit()
Before testing, in tests/test_models.py, comment on the test_user_registration () test, because we don't want to send a message for this test really.
Now test it again. It should be all-powerful!
Conclusion
You can also do more:
- Rich Text vs. plain text email--should all be mailed.
- Reset Password email--These should be sent when the user forgets the password.
- User management-should allow users to update email and password, when the email has changed, it should be re-verified again.
- Testing-more tests need to be written to involve more new features, including Test_user_registration () updated with Mock/patch, to prevent the actual email from being sent.