diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 9beb5dd10..7a0a95feb 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -20,6 +20,7 @@ @import '../govuk_elements/public/sass/elements/details'; @import '../govuk_elements/public/sass/elements/elements-typography'; @import '../govuk_elements/public/sass/elements/forms'; +@import '../govuk_elements/public/sass/elements/forms/form-validation'; @import '../govuk_elements/public/sass/elements/forms/form-block-labels'; @import '../govuk_elements/public/sass/elements/icons'; @import '../govuk_elements/public/sass/elements/layout'; diff --git a/app/main/dao/password_reset_token_dao.py b/app/main/dao/password_reset_token_dao.py deleted file mode 100644 index c43cc92bd..000000000 --- a/app/main/dao/password_reset_token_dao.py +++ /dev/null @@ -1,19 +0,0 @@ -from datetime import datetime, timedelta -from app import db -from app.models import PasswordResetToken - - -def insert(token, user_id): - password_reset_token = PasswordResetToken(token=token, - user_id=user_id, - expiry_date=datetime.now() + timedelta(hours=1)) - insert_token(password_reset_token) - - -def insert_token(password_reset_token): - db.session.add(password_reset_token) - db.session.commit() - - -def get_token(token): - return PasswordResetToken.query.filter_by(token=token).first() diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index 8329d057d..64e306b79 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -59,12 +59,13 @@ def update_mobile_number(id, mobile_number): db.session.commit() -def update_password(id, password): - user = get_user_by_id(id) +def update_password(email, password): + user = get_user_by_email(email) user.password = hashpw(password) user.password_changed_at = datetime.now() db.session.add(user) db.session.commit() + return user def find_all_email_address(): diff --git a/app/main/forms.py b/app/main/forms.py index 3652f5708..46321303d 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -24,7 +24,6 @@ verify_code = '^\d{5}$' class RegisterUserForm(Form): - def __init__(self, existing_email_addresses, existing_mobile_numbers, *args, **kwargs): self.existing_emails = existing_email_addresses self.existing_mobiles = existing_mobile_numbers @@ -125,10 +124,6 @@ class AddServiceForm(Form): class ForgotPasswordForm(Form): - def __init__(self, email_addresses, *args, **kargs): - self.email_addresses = email_addresses - super(ForgotPasswordForm, self).__init__(*args, **kargs) - email_address = StringField('Email address', validators=[Length(min=5, max=255), DataRequired(message='Email cannot be empty'), @@ -136,10 +131,6 @@ class ForgotPasswordForm(Form): Regexp(regex=gov_uk_email, message='Please enter a gov.uk email address') ]) - def validate_email_address(self, a): - if self.email_address.data not in self.email_addresses: - raise ValidationError('Please enter the email address that you registered with') - class NewPasswordForm(Form): new_password = StringField('Create a password', diff --git a/app/main/views/__init__.py b/app/main/views/__init__.py index 88a894ee8..d85bbfbc9 100644 --- a/app/main/views/__init__.py +++ b/app/main/views/__init__.py @@ -1,12 +1,11 @@ import traceback -import uuid from random import randint -from flask import url_for +from flask import url_for, current_app from app import admin_api_client +from app.main.dao import verify_codes_dao from app.main.exceptions import AdminApiClientException -from app.main.dao import verify_codes_dao, password_reset_token_dao def create_verify_code(): @@ -38,11 +37,9 @@ def send_email_code(user_id, email): return email_code -def send_change_password_email(email, user_id): +def send_change_password_email(email): try: - reset_password_token = str(uuid.uuid4()).replace('-', '') - link_to_change_password = url_for('.new_password', token=reset_password_token, _external=True) - password_reset_token_dao.insert(reset_password_token, user_id) + link_to_change_password = url_for('.new_password', token=generate_token(email), _external=True) admin_api_client.send_email(email_address=email, from_str='notify@digital.cabinet-office.gov.uk', message=link_to_change_password, @@ -51,3 +48,20 @@ def send_change_password_email(email, user_id): except: traceback.print_exc() raise AdminApiClientException('Exception when sending email.') + + +def generate_token(email): + from itsdangerous import TimestampSigner + signer = TimestampSigner(current_app.config['SECRET_KEY']) + return signer.sign(email).decode('utf8') + + +def check_token(token): + from itsdangerous import TimestampSigner, SignatureExpired + signer = TimestampSigner(current_app.config['SECRET_KEY']) + try: + email = signer.unsign(token, max_age=current_app.config['TOKEN_MAX_AGE_SECONDS']) + return email + except SignatureExpired as e: + current_app.logger.info('token expired %s' % e) + return None diff --git a/app/main/views/forgot_password.py b/app/main/views/forgot_password.py index 2d21eff09..8413d7270 100644 --- a/app/main/views/forgot_password.py +++ b/app/main/views/forgot_password.py @@ -1,5 +1,4 @@ -from flask import render_template - +from flask import render_template, flash from app.main import main from app.main.dao import users_dao from app.main.forms import ForgotPasswordForm @@ -8,10 +7,13 @@ from app.main.views import send_change_password_email @main.route('/forgot-password', methods=['GET', 'POST']) def forgot_password(): - form = ForgotPasswordForm(users_dao.find_all_email_address()) + form = ForgotPasswordForm() if form.validate_on_submit(): - user = users_dao.get_user_by_email(form.email_address.data) - send_change_password_email(form.email_address.data, user.id) - return render_template('views/password-reset-sent.html') + if users_dao.get_user_by_email(form.email_address.data): + send_change_password_email(form.email_address.data) + return render_template('views/password-reset-sent.html') + else: + flash('The email address is not recognized. Try again.') + return render_template('views/forgot-password.html', form=form) else: return render_template('views/forgot-password.html', form=form) diff --git a/app/main/views/new_password.py b/app/main/views/new_password.py index 0c479d59f..4be020e74 100644 --- a/app/main/views/new_password.py +++ b/app/main/views/new_password.py @@ -1,26 +1,26 @@ from datetime import datetime -from flask import (Markup, render_template, url_for, redirect) +from flask import (render_template, url_for, redirect, flash, current_app) from app.main import main -from app.main.dao import (password_reset_token_dao, users_dao) +from app.main.dao import users_dao from app.main.forms import NewPasswordForm -from app.main.views import send_sms_code +from app.main.views import send_sms_code, check_token @main.route('/new-password/', methods=['GET', 'POST']) def new_password(token): form = NewPasswordForm() if form.validate_on_submit(): - password_reset_token = password_reset_token_dao.get_token(str(Markup.escape(token))) - if not valid_token(password_reset_token): - form.new_password.errors.append('token is invalid') # Is there a better way - return render_template('views/new-password.html', form=form) - else: - users_dao.update_password(password_reset_token.user_id, form.new_password.data) - user = users_dao.get_user_by_id(password_reset_token.user_id) + email_address = check_token(token) + if email_address: + user = users_dao.update_password(email_address.decode('utf-8'), form.new_password.data) send_sms_code(user.id, user.mobile_number) return redirect(url_for('main.two_factor')) + else: + flash('expired token request again') + current_app.logger.info('we got here') + return redirect(url_for('.forgot_password')) else: return render_template('views/new-password.html', toke=token, form=form) diff --git a/app/models.py b/app/models.py index cff7d18a6..3608f040f 100644 --- a/app/models.py +++ b/app/models.py @@ -111,13 +111,6 @@ class Service(db.Model): return filter_null_value_fields(serialized) -class PasswordResetToken(db.Model): - id = db.Column(db.Integer, primary_key=True) - token = db.Column(db.String, unique=True, index=True, nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=False, nullable=False) - expiry_date = db.Column(db.DateTime, nullable=False) - - def filter_null_value_fields(obj): return dict( filter(lambda x: x[1] is not None, obj.items()) diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 4613ca442..a03e47612 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -43,6 +43,7 @@ {% endblock %} + {% set global_header_text = "GOV.UK Notify" %} @@ -53,9 +54,20 @@ {% endif %} {% block content %} -
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+
+ {% endif %} + {% endwith %} {% block fullwidth_content %}{% endblock %} -
+
{% endblock %} {% block body_end %} diff --git a/app/templates/components/form-field.html b/app/templates/components/form-field.html index 643099498..691b4c579 100644 --- a/app/templates/components/form-field.html +++ b/app/templates/components/form-field.html @@ -2,7 +2,7 @@
{{ field.label }}
{{ field(**kwargs)|safe }} {% if field.errors %} -