diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2cd31d2..3343b05 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -6,6 +6,11 @@ Change Log
All library changes, in descending order.
+UNRELEASED
+----------
+
+- Add email verification implementation.
+
Version 0.4.8
-------------
diff --git a/flask_stormpath/__init__.py b/flask_stormpath/__init__.py
index bd7798f..876634b 100644
--- a/flask_stormpath/__init__.py
+++ b/flask_stormpath/__init__.py
@@ -58,6 +58,9 @@
login,
logout,
register,
+ verify_email,
+ verify_email_tokens,
+ welcome,
)
@@ -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):
"""
diff --git a/flask_stormpath/forms.py b/flask_stormpath/forms.py
index d99728c..fdc6831 100644
--- a/flask_stormpath/forms.py
+++ b/flask_stormpath/forms.py
@@ -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
@@ -97,3 +97,7 @@ class ChangePasswordForm(FlaskForm):
InputRequired('Please verify the password.'),
EqualTo('password', 'Passwords do not match.')
])
+
+
+class ResendVerificationForm(FlaskForm):
+ username = HiddenField('Username')
diff --git a/flask_stormpath/settings.py b/flask_stormpath/settings.py
index 29c06c1..c6faa2c 100644
--- a/flask_stormpath/settings.py
+++ b/flask_stormpath/settings.py
@@ -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')
@@ -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')
diff --git a/flask_stormpath/templates/flask_stormpath/verify_email.html b/flask_stormpath/templates/flask_stormpath/verify_email.html
new file mode 100644
index 0000000..bcd3938
--- /dev/null
+++ b/flask_stormpath/templates/flask_stormpath/verify_email.html
@@ -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 %}
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+ {% if config['STORMPATH_ENABLE_LOGIN'] %}
+
Back to Log In
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/flask_stormpath/templates/flask_stormpath/verify_email_complete.html b/flask_stormpath/templates/flask_stormpath/verify_email_complete.html
new file mode 100644
index 0000000..31506cd
--- /dev/null
+++ b/flask_stormpath/templates/flask_stormpath/verify_email_complete.html
@@ -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 %}
+
+
+
+
+
+
+
+
+ Your email has been verified, you should be redirected back to
+ the login page in 5 seconds...
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+ {% if config['STORMPATH_ENABLE_LOGIN'] %}
+
Back to Log In
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/flask_stormpath/templates/flask_stormpath/verify_email_sent.html b/flask_stormpath/templates/flask_stormpath/verify_email_sent.html
new file mode 100644
index 0000000..8d93f6f
--- /dev/null
+++ b/flask_stormpath/templates/flask_stormpath/verify_email_sent.html
@@ -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 %}
+
+
+
+
+
+
+
+ We have resent the email verification message
+ to: {{ session['verify_email_for'] }}.
+
+
+ Please check your inbox before proceeding to the
+ home page.
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+ {% if config['STORMPATH_ENABLE_LOGIN'] %}
+
Back to Log In
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/flask_stormpath/templates/flask_stormpath/welcome.html b/flask_stormpath/templates/flask_stormpath/welcome.html
new file mode 100644
index 0000000..c2d5d0c
--- /dev/null
+++ b/flask_stormpath/templates/flask_stormpath/welcome.html
@@ -0,0 +1,42 @@
+{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+
+{% block title %}Welcome!{% endblock %}
+{% block description %}Welcome!{% endblock %}
+{% block bodytag %}login{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+
+ 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.
+
+
+ Once you've verified your email address, you should be able
+ to proceed to the home page.
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+ {% if config['STORMPATH_ENABLE_LOGIN'] %}
+
Back to Log In
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/flask_stormpath/views.py b/flask_stormpath/views.py
index f00f4a5..50c39b8 100644
--- a/flask_stormpath/views.py
+++ b/flask_stormpath/views.py
@@ -10,6 +10,7 @@
redirect,
render_template,
request,
+ session,
)
from flask_login import login_user
from six import string_types
@@ -21,6 +22,7 @@
ForgotPasswordForm,
LoginForm,
RegistrationForm,
+ ResendVerificationForm,
)
from .models import User
@@ -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
@@ -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:
@@ -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)
@@ -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)
\ No newline at end of file
diff --git a/tests/test_views.py b/tests/test_views.py
index 1aa8186..6f989aa 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -285,6 +285,34 @@ def test_redirect_to_register_url(self):
self.assertTrue('redirect_for_login' in location)
self.assertFalse('redirect_for_registration' in location)
+ def test_redirect_to_verify_email_url(self):
+ # Enable email verification
+ self.app.config['STORMPATH_VERIFY_EMAIL'] = True
+ account_creation_policy = self.client.directories.search(self.application.name).items[0].account_creation_policy
+ account_creation_policy.verification_email_status = 'ENABLED'
+ # account_creation_policy.verification_success_email_status = 'ENABLED'
+ account_creation_policy.save()
+
+ # Create a user.
+ with self.app.app_context():
+ User.create(
+ username = 'rdegges',
+ given_name = 'Randall',
+ surname = 'Degges',
+ email = 'r@testmail.stormpath.com',
+ password = 'woot1LoveCookies!',
+ )
+
+ with self.app.test_client() as c:
+ # Attempt a login using username and password.
+ resp = c.post(
+ '/login',
+ data={'login': 'rdegges', 'password': 'woot1LoveCookies!',})
+
+ self.assertEqual(resp.status_code, 302)
+ location = resp.headers.get('location')
+ self.assertTrue('verify_email' in location)
+
class TestLogout(StormpathTestCase):
"""Test our logout view."""