Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add email verification features #95

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Change Log

All library changes, in descending order.

UNRELEASED
----------

- Add email verification implementation.


Version 0.4.8
-------------
Expand Down
23 changes: 23 additions & 0 deletions flask_stormpath/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
login,
logout,
register,
verify_email,
verify_email_tokens,
welcome,
)


Expand Down Expand Up @@ -209,6 +212,26 @@ def init_routes(self, app):
facebook_login,
)

if app.config['STORMPATH_VERIFY_EMAIL']:
app.add_url_rule(
app.config['STORMPATH_VERIFY_EMAIL_URL'],
'stormpath.verify_email',
verify_email,
methods=['GET', 'POST'],
)

app.add_url_rule(
'/emailVerificationTokens',
'stormpath.verify_email_tokens',
verify_email_tokens,
)

app.add_url_rule(
app.config['STORMPATH_WELCOME_URL'],
'stormpath.welcome',
welcome,
)

@property
def client(self):
"""
Expand Down
6 changes: 5 additions & 1 deletion flask_stormpath/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from flask_wtf import FlaskForm
from flask_wtf.form import _Auto
from wtforms.fields import PasswordField, StringField
from wtforms.fields import HiddenField, PasswordField, StringField
from wtforms.validators import Email, EqualTo, InputRequired, ValidationError


Expand Down Expand Up @@ -97,3 +97,7 @@ class ChangePasswordForm(FlaskForm):
InputRequired('Please verify the password.'),
EqualTo('password', 'Passwords do not match.')
])


class ResendVerificationForm(FlaskForm):
username = HiddenField('Username')
6 changes: 6 additions & 0 deletions flask_stormpath/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def init_settings(config):
# Configure URL mappings. These URL mappings control which URLs will be
# used by Flask-Stormpath views.
config.setdefault('STORMPATH_REGISTRATION_URL', '/register')
config.setdefault('STORMPATH_VERIFY_EMAIL_URL', '/verify_email')
config.setdefault('STORMPATH_WELCOME_URL', '/welcome')
config.setdefault('STORMPATH_LOGIN_URL', '/login')
config.setdefault('STORMPATH_LOGOUT_URL', '/logout')
config.setdefault('STORMPATH_FORGOT_PASSWORD_URL', '/forgot')
Expand All @@ -78,6 +80,10 @@ def init_settings(config):
# used to render the Flask-Stormpath views.
config.setdefault('STORMPATH_BASE_TEMPLATE', 'flask_stormpath/base.html')
config.setdefault('STORMPATH_REGISTRATION_TEMPLATE', 'flask_stormpath/register.html')
config.setdefault('STORMPATH_VERIFY_EMAIL_TEMPLATE', 'flask_stormpath/verify_email.html')
config.setdefault('STORMPATH_VERIFY_EMAIL_SENT_TEMPLATE', 'flask_stormpath/verify_email_sent.html')
config.setdefault('STORMPATH_VERIFY_EMAIL_COMPLETE_TEMPLATE', 'flask_stormpath/verify_email_complete.html')
config.setdefault('STORMPATH_WELCOME_TEMPLATE', 'flask_stormpath/welcome.html')
config.setdefault('STORMPATH_LOGIN_TEMPLATE', 'flask_stormpath/login.html')
config.setdefault('STORMPATH_FORGOT_PASSWORD_TEMPLATE', 'flask_stormpath/forgot.html')
config.setdefault('STORMPATH_FORGOT_PASSWORD_EMAIL_SENT_TEMPLATE', 'flask_stormpath/forgot_email_sent.html')
Expand Down
46 changes: 46 additions & 0 deletions flask_stormpath/templates/flask_stormpath/verify_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% extends config['STORMPATH_BASE_TEMPLATE'] %}

{% block title %}Verify Email{% endblock %}
{% block description %}Verify your email address{% endblock %}
{% block bodytag %}login{% endblock %}

{% block body %}
<div class="container custom-container">
<div class="va-wrapper">
<div class="view login-view container">
<div class="box row">
<div class="email-password-area col-xs-12 large col-sm-12">
<div class="header">
<span>Your email address has not been verified yet!</span>
</div>
<p>
You need to verify your email address before you can log in.
Please check your inbox and follow the instructions in the
verification email to continue.
</p>
<p>
<form id="verify_resend_form" class="login-form form-horizontal" role="form" method="post">
{{ form.hidden_tag() }}
If you've lost the verification email, we can always
<a href="#" onclick="$('#verify_resend_form').submit(); return false;"> resend </a>
it to you at: <b>{{ session['verify_email_for'] }}</b>.
</form>
</p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-danger bad-login">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% if config['STORMPATH_ENABLE_LOGIN'] %}
<a class="forgot" href="{{ url_for('stormpath.login') }}">Back to Log In</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends config['STORMPATH_BASE_TEMPLATE'] %}

{% block title %}Email Verified!{% endblock %}
{% block description %}Email verification completed!{% endblock %}
{% block bodytag %}login{% endblock %}

{% block body %}
<script type="text/javascript" nonce="{{ csrf_token() }}">
document.addEventListener("DOMContentLoaded", function(event) {
setTimeout(function () {
window.location.href = "{{ config['STORMPATH_LOGIN_URL'] }}?href={{ href | safe }}"
}, 5000)
})
</script>
<div class="container custom-container">
<div class="va-wrapper">
<div class="view login-view container">
<div class="box row">
<div class="email-password-area col-xs-12 large col-sm-12">
<div class="header">
<span>Email Verification Complete!</span>
</div>
<p>
Your email has been verified, you should be redirected back to
the <a href="{{ url_for('stormpath.login') }}"> login </a> page in 5 seconds...
</p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-danger bad-login">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% if config['STORMPATH_ENABLE_LOGIN'] %}
<a class="forgot" href="{{ url_for('stormpath.login') }}">Back to Log In</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
41 changes: 41 additions & 0 deletions flask_stormpath/templates/flask_stormpath/verify_email_sent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% extends config['STORMPATH_BASE_TEMPLATE'] %}

{% block title %}Email Sent!{% endblock %}
{% block description %}Verification email sent!{% endblock %}
{% block bodytag %}login{% endblock %}

{% block body %}
<div class="container custom-container">
<div class="va-wrapper">
<div class="view login-view container">
<div class="box row">
<div class="email-password-area col-xs-12 large col-sm-12">
<div class="header">
<span>Your verification email has been sent!</span>
</div>
<p>
We have resent the email verification message
to: <b>{{ session['verify_email_for'] }}</b>.
</p>
<p>
Please check your inbox before proceeding to the
<a href="{{ url_for('public.index') }}"> home </a> page.
</p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-danger bad-login">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% if config['STORMPATH_ENABLE_LOGIN'] %}
<a class="forgot" href="{{ url_for('stormpath.login') }}">Back to Log In</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
42 changes: 42 additions & 0 deletions flask_stormpath/templates/flask_stormpath/welcome.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends config['STORMPATH_BASE_TEMPLATE'] %}

{% block title %}Welcome!{% endblock %}
{% block description %}Welcome!{% endblock %}
{% block bodytag %}login{% endblock %}

{% block body %}
<div class="container custom-container">
<div class="va-wrapper">
<div class="view login-view container">
<div class="box row">
<div class="email-password-area col-xs-12 large col-sm-12">
<div class="header">
<span>Thank you for registering!</span>
</div>
<p>
You need to verify your email address before you can log in.
Please check your inbox and follow the instructions in the
verification email to continue.
</p>
<p>
Once you've verified your email address, you should be able
to proceed to the <a href="{{ url_for('public.index') }}"> home </a> page.
</p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-danger bad-login">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% if config['STORMPATH_ENABLE_LOGIN'] %}
<a class="forgot" href="{{ url_for('stormpath.login') }}">Back to Log In</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
65 changes: 64 additions & 1 deletion flask_stormpath/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
redirect,
render_template,
request,
session,
)
from flask_login import login_user
from six import string_types
Expand All @@ -21,6 +22,7 @@
ForgotPasswordForm,
LoginForm,
RegistrationForm,
ResendVerificationForm,
)
from .models import User

Expand Down Expand Up @@ -58,6 +60,11 @@ def register():
data.get('surname', 'Anonymous') or 'Anonymous',
**optional_params
)
if account.is_unverified():
# Don't log in if the account has not been verified yet.
return redirect(
current_app.config['STORMPATH_WELCOME_URL']
)

# If we're able to successfully create the user's account,
# we'll log the user in (creating a secure session using
Expand All @@ -66,7 +73,7 @@ def register():
login_user(account, remember=True)

# The email address must be verified, so pop an alert about it.
if current_app.config['STORMPATH_VERIFY_EMAIL'] is True:
if account.is_unverified() and current_app.config['STORMPATH_VERIFY_EMAIL'] is True:
flash('You must validate your email address before logging in. Please check your email for instructions.')

if 'STORMPATH_REGISTRATION_REDIRECT_URL' in current_app.config:
Expand Down Expand Up @@ -112,6 +119,25 @@ def login():
login_user(account, remember=True)

return redirect(request.args.get('next') or current_app.config['STORMPATH_REDIRECT_URL'])

except StormpathError as err:
if err.code == 7102:
# User's email has not been verified yet
session['verify_email_for'] = form.login.data

return redirect(
current_app.config['STORMPATH_VERIFY_EMAIL_URL']
)
else:
flash(err.message)

# Pre-fill fields with the username, if it is available.
href = request.args.get('href')
if href:
try:
account = current_app.stormpath_manager.client.accounts.get(href)
form.login.data = account.username

except StormpathError as err:
flash(err.message)

Expand Down Expand Up @@ -391,3 +417,40 @@ def logout():
"""
logout_user()
return redirect('/')


def welcome():
return render_template(current_app.config['STORMPATH_WELCOME_TEMPLATE'])


def verify_email():
form = ResendVerificationForm()

if form.validate_on_submit():
try:
account = current_app.stormpath_manager.application.accounts.search({'username': form.username.data})[0]
current_app.stormpath_manager.application.verification_emails.resend(account, account.directory)

return render_template(
current_app.config['STORMPATH_VERIFY_EMAIL_SENT_TEMPLATE']
)
except StormpathError as err:
flash(err.message)
abort(400)

form.username.data = session['verify_email_for']
return render_template(
current_app.config['STORMPATH_VERIFY_EMAIL_TEMPLATE'],
form=form
)


def verify_email_tokens():
try:
account = current_app.stormpath_manager.client.accounts.verify_email_token(request.args.get('sptoken'))
return render_template(
current_app.config['STORMPATH_VERIFY_EMAIL_COMPLETE_TEMPLATE'], href=account.href
)
except StormpathError as err:
flash(err.message)
abort(400)
Loading