From 6696426dbc8a1430b3a82a9236eddae4f8575561 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 4 Jan 2016 14:00:39 +0000 Subject: [PATCH] Add endpoints for forgot-password. --- app/main/__init__.py | 3 +-- app/main/dao/users_dao.py | 10 ++++++++ app/main/forms.py | 14 +++++++++--- app/main/views/__init__.py | 14 ++++++++++++ app/main/views/forgot_password.py | 21 +++++++++++++++++ app/main/views/index.py | 24 ++++++++++++++++++++ app/templates/views/forgot-password.html | 14 +++++++----- app/templates/views/password-reset-sent.html | 18 +++++++++++++++ tests/app/main/dao/test_users_dao.py | 22 ++++++++++++++++++ tests/app/main/views/test_forgot_password.py | 16 +++++++++++++ 10 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 app/main/views/forgot_password.py create mode 100644 app/templates/views/password-reset-sent.html create mode 100644 tests/app/main/views/test_forgot_password.py diff --git a/app/main/__init__.py b/app/main/__init__.py index 9354e06db..2c62f8207 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -2,8 +2,7 @@ from flask import Blueprint main = Blueprint('main', __name__) - from app.main.views import ( index, sign_in, sign_out, register, two_factor, verify, sms, add_service, - code_not_received, jobs, dashboard, templates, service_settings + code_not_received, jobs, dashboard, templates, service_settings, forgot_password ) diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index ea065e9d6..f0237719a 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -1,3 +1,5 @@ +from datetime import datetime + from app import db, login_manager from app.models import User from app.main.encryption import hashpw @@ -53,3 +55,11 @@ def update_mobile_number(id, mobile_number): user.mobile_number = mobile_number db.session.add(user) db.session.commit() + + +def update_password(id, password): + user = get_user_by_id(id) + user.password = hashpw(password) + user.password_changed_at = datetime.now() + db.session.add(user) + db.session.commit() diff --git a/app/main/forms.py b/app/main/forms.py index 5b7230477..a9be2364d 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1,10 +1,7 @@ -from datetime import datetime from flask_wtf import Form from wtforms import StringField, PasswordField, ValidationError from wtforms.validators import DataRequired, Email, Length, Regexp -from app.main.dao import verify_codes_dao -from app.main.encryption import check_hash from app.main.validators import Blacklist, ValidateUserCodes @@ -123,3 +120,14 @@ class AddServiceForm(Form): def validate_service_name(self, a): if self.service_name.data in self.service_names: raise ValidationError('Service name already exists') + + +class ForgotPassword(Form): + email_address = StringField('Email address', + validators=[Length(min=5, max=255), + DataRequired(message='Email cannot be empty'), + Email(message='Please enter a valid email address'), + Regexp(regex=gov_uk_email, message='Please enter a gov.uk email address') + ]) + + diff --git a/app/main/views/__init__.py b/app/main/views/__init__.py index e092ba59c..f145200ba 100644 --- a/app/main/views/__init__.py +++ b/app/main/views/__init__.py @@ -31,3 +31,17 @@ def send_email_code(user_id, email): raise AdminApiClientException('Exception when sending email.') return email_code + + +def send_change_password_email(email): + code = create_verify_code() + link_to_change_password = 'thelink' + code + # TODO needs an expiry date to check? + try: + admin_api_client.send_email(email_address=email, + from_str='notify@digital.cabinet-office.gov.uk', + message=link_to_change_password, + subject='Verification code', + token=admin_api_client.auth_token) + except: + raise AdminApiClientException('Exception when sending email.') diff --git a/app/main/views/forgot_password.py b/app/main/views/forgot_password.py new file mode 100644 index 000000000..ece51e59e --- /dev/null +++ b/app/main/views/forgot_password.py @@ -0,0 +1,21 @@ +from flask import render_template, jsonify + +from app.main import main +from app.main.forms import ForgotPassword +from app.main.views import send_change_password_email + + +@main.route('/forgot-password', methods=['GET']) +def render_forgot_my_password(): + return render_template('views/forgot-password.html', form=ForgotPassword()) + + +@main.route('/forgot-password', methods=['POST']) +def change_password(): + form = ForgotPassword() + if form.validate_on_submit(): + send_change_password_email(form.email_address) + + return 'You have been sent an email with a link to change your password' + else: + return jsonify(form.errors), 400 diff --git a/app/main/views/index.py b/app/main/views/index.py index dec87cf89..33d8f7e7d 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -39,6 +39,21 @@ def forgotpassword(): return render_template('views/forgot-password.html') +@main.route("/jobs") +def showjobs(): + return render_template('views/jobs.html') + + +@main.route("/jobs/job") +def showjob(): + return render_template('views/job.html') + + +@main.route("/jobs/job/notification") +def shownotification(): + return render_template('views/notification.html') + + @main.route("/new-password") def newpassword(): return render_template('views/new-password.html') @@ -62,3 +77,12 @@ def apikeys(): @main.route("/verification-not-received") def verificationnotreceived(): return render_template('views/verification-not-received.html') + +@main.route("/manage-templates") +def managetemplates(): + return render_template('views/manage-templates.html') + + +@main.route("/edit-template") +def edittemplate(): + return render_template('views/edit-template.html') diff --git a/app/templates/views/forgot-password.html b/app/templates/views/forgot-password.html index 5ca218fb4..6db3a81b9 100644 --- a/app/templates/views/forgot-password.html +++ b/app/templates/views/forgot-password.html @@ -12,15 +12,17 @@ GOV.UK Notify

If you have forgotten your password, we can send you an email to create a new password.

-

- - +

+ {{ form.hidden_tag() }} +

+ + {{ form.email_address(class="form-control-2-3", autocomplete="off") }}
+ Your email address must end in .gov.uk

- -

- Send email +

+
diff --git a/app/templates/views/password-reset-sent.html b/app/templates/views/password-reset-sent.html new file mode 100644 index 000000000..c51c206d3 --- /dev/null +++ b/app/templates/views/password-reset-sent.html @@ -0,0 +1,18 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | +{% endblock %} + +{% block content %} + +
+
+

GOV.UK Notify

+ +

You have been sent an email containing a url to reset your password.

+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/tests/app/main/dao/test_users_dao.py b/tests/app/main/dao/test_users_dao.py index 75affe58d..ae6579c63 100644 --- a/tests/app/main/dao/test_users_dao.py +++ b/tests/app/main/dao/test_users_dao.py @@ -3,6 +3,7 @@ from datetime import datetime import pytest import sqlalchemy +from app.main.encryption import check_hash from app.models import User from app.main.dao import users_dao @@ -161,3 +162,24 @@ def test_should_update_email_address(notifications_admin, notifications_admin_db users_dao.update_email_address(user.id, 'new_email@testit.gov.uk') updated = users_dao.get_user_by_id(user.id) assert updated.email_address == 'new_email@testit.gov.uk' + + +def test_should_update_password(notifications_admin, notifications_admin_db, notify_db_session): + user = User(name='Update Email', + password='somepassword', + email_address='test@it.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1, + state='active') + start = datetime.now() + users_dao.insert_user(user) + + saved = users_dao.get_user_by_id(user.id) + assert check_hash('somepassword', saved.password) + assert saved.password_changed_at is None + users_dao.update_password(saved.id, 'newpassword') + updated = users_dao.get_user_by_id(user.id) + assert check_hash('newpassword', updated.password) + assert updated.password_changed_at < datetime.now() + assert updated.password_changed_at > start diff --git a/tests/app/main/views/test_forgot_password.py b/tests/app/main/views/test_forgot_password.py new file mode 100644 index 000000000..95f0cce2a --- /dev/null +++ b/tests/app/main/views/test_forgot_password.py @@ -0,0 +1,16 @@ +from flask import current_app + + +def test_should_render_forgot_password(notifications_admin, notifications_admin_db, notify_db_session): + response = notifications_admin.test_client().get('/forgot-password') + assert response.status_code == 200 + assert 'If you have forgotten your password, we can send you an email to create a new password.' \ + in response.get_data(as_text=True) + + +def test_should_return_400_when_email_is_invalid(notifications_admin, notifications_admin_db, notify_db_session): + response = notifications_admin.test_client().post('/forgot-password', + data={'email_address': 'not_a_valid_email'}) + x = current_app._get_current_object() + assert response.status_code == 400 + assert 'Please enter a valid email address' in response.get_data(as_text=True)