Implementation of the new_password endpoint.

Found a way to create the token that does not need to persist it to the database.
This requires proper error messages, written by people who speak menglis good.
This commit is contained in:
Rebecca Law
2016-01-07 17:13:49 +00:00
parent 8057a138a8
commit a860f713d2
17 changed files with 87 additions and 156 deletions

View File

@@ -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';

View File

@@ -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()

View File

@@ -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():

View File

@@ -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',

View File

@@ -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

View File

@@ -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)

View File

@@ -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/<path:token>', 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)

View File

@@ -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())

View File

@@ -43,6 +43,7 @@
</div>
{% endblock %}
{% set global_header_text = "GOV.UK Notify" %}
@@ -53,9 +54,20 @@
{% endif %}
{% block content %}
<main id="content" role="main" class="page-container">
<main id="content" role="main" class="page-container">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="error-summary">
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
{% block fullwidth_content %}{% endblock %}
</main>
</main>
{% endblock %}
{% block body_end %}

View File

@@ -2,7 +2,7 @@
<dt>{{ field.label }}
<dd>{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=error-summary>
<ul class=error>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}