diff --git a/app/user/rest.py b/app/user/rest.py index 9af27bd99..2e6e6b021 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -34,6 +34,7 @@ from app.errors import ( register_errors, InvalidRequest ) +from app.utils import url_with_token user = Blueprint('user', __name__) register_errors(user) @@ -43,7 +44,6 @@ register_errors(user) def create_user(): user_to_create, errors = user_schema.load(request.get_json()) req_json = request.get_json() - # TODO password policy, what is valid password if not req_json.get('password', None): errors.update({'password': ['Missing data for required field.']}) raise InvalidRequest(errors, status_code=400) @@ -147,6 +147,35 @@ def send_user_sms_code(user_id): return jsonify({}), 204 +@user.route('//change-email-verification', methods=['POST']) +def send_user_confirm_new_email(user_id): + user_to_send_to = get_model_users(user_id=user_id) + email, errors = email_data_request_schema.load(request.get_json()) + if errors: + raise InvalidRequest(message=errors, status_code=400) + + template = dao_get_template_by_id(current_app.config['CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID']) + message = { + 'template': str(template.id), + 'template_version': template.version, + 'to': email['email'], + 'personalisation': { + 'name': user_to_send_to.name, + 'url': _create_confirmation_url(user=user_to_send_to, email_address=email['email']), + 'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/feedback' + } + } + + send_email.apply_async(( + current_app.config['NOTIFY_SERVICE_ID'], + str(uuid.uuid4()), + encryption.encrypt(message), + datetime.utcnow().strftime(DATETIME_FORMAT) + ), queue='notify') + + return jsonify({}), 204 + + @user.route('//email-verification', methods=['POST']) def send_user_email_verification(user_id): user_to_send_to = get_model_users(user_id=user_id) @@ -258,16 +287,18 @@ def send_user_reset_password(): def _create_reset_password_url(email): - from notifications_utils.url_safe_token import generate_token data = json.dumps({'email': email, 'created_at': str(datetime.utcnow())}) - token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT']) - - return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token + url = '/new-password/' + return url_with_token(data, url, current_app.config) def _create_verification_url(user, secret_code): - from notifications_utils.url_safe_token import generate_token data = json.dumps({'user_id': str(user.id), 'email': user.email_address, 'secret_code': secret_code}) - token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT']) + url = '/verify-email/' + return url_with_token(data, url, current_app.config) - return current_app.config['ADMIN_BASE_URL'] + '/verify-email/' + token + +def _create_confirmation_url(user, email_address): + data = json.dumps({'user_id': str(user.id), 'email': email_address}) + url = '/user-profile/email/confirm/' + return url_with_token(data, url, current_app.config) diff --git a/app/utils.py b/app/utils.py index f67643290..ace301040 100644 --- a/app/utils.py +++ b/app/utils.py @@ -11,3 +11,10 @@ def pagination_links(pagination, endpoint, **kwargs): links['next'] = url_for(endpoint, page=pagination.next_num, **kwargs) links['last'] = url_for(endpoint, page=pagination.pages, **kwargs) return links + + +def url_with_token(data, url, config): + from notifications_utils.url_safe_token import generate_token + token = generate_token(data, config['SECRET_KEY'], config['DANGEROUS_SALT']) + base_url = config['ADMIN_BASE_URL'] + url + return base_url + token diff --git a/config.py b/config.py index 255444bb8..2b5000eae 100644 --- a/config.py +++ b/config.py @@ -65,6 +65,7 @@ class Config(object): EMAIL_VERIFY_CODE_TEMPLATE_ID = 'ece42649-22a8-4d06-b87f-d52d5d3f0a27' PASSWORD_RESET_TEMPLATE_ID = '474e9242-823b-4f99-813d-ed392e7f1201' ALREADY_REGISTERED_EMAIL_TEMPLATE_ID = '0880fbb1-a0c6-46f0-9a8e-36c986381ceb' + CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID = 'eb4d9930-87ab-4aef-9bce-786762687884' BROKER_URL = 'sqs://' BROKER_TRANSPORT_OPTIONS = { diff --git a/tests/app/conftest.py b/tests/app/conftest.py index d5940f887..002c7c13c 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -713,144 +713,100 @@ def mock_firetext_client(mocker, statsd_client=None): @pytest.fixture(scope='function') def sms_code_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) - - template = Template.query.get(current_app.config['SMS_CODE_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['SMS_CODE_TEMPLATE_ID'], - 'name': 'Sms code template', - 'template_type': 'sms', - 'content': '((verify_code))', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + service, user = notify_service(notify_db, notify_db_session) + return create_notify_template(service=service, + user=user, + template_config_name='SMS_CODE_TEMPLATE_ID', + content='((verify_code))', + template_type='sms') @pytest.fixture(scope='function') def email_verification_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) - - template = Template.query.get(current_app.config['EMAIL_VERIFY_CODE_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['EMAIL_VERIFY_CODE_TEMPLATE_ID'], - 'name': 'Email verification template', - 'template_type': 'email', - 'content': '((user_name)) use ((url)) to complete registration', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + service, user = notify_service(notify_db, notify_db_session) + return create_notify_template(service=service, + user=user, + template_config_name='EMAIL_VERIFY_CODE_TEMPLATE_ID', + content='((user_name)) use ((url)) to complete registration', + template_type='email') @pytest.fixture(scope='function') def invitation_email_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) - - template = Template.query.get(current_app.config['INVITATION_EMAIL_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['INVITATION_EMAIL_TEMPLATE_ID'], - 'name': 'Invitaion template', - 'template_type': 'email', - 'content': '((user_name)) is invited to Notify by ((service_name)) ((url)) to complete registration', - 'subject': 'Invitation to ((service_name))', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + service, user = notify_service(notify_db, notify_db_session) + content = '((user_name)) is invited to Notify by ((service_name)) ((url)) to complete registration', + return create_notify_template(service=service, + user=user, + template_config_name='INVITATION_EMAIL_TEMPLATE_ID', + content=content, + subject='Invitation to ((service_name))', + template_type='email') @pytest.fixture(scope='function') def password_reset_email_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) + service, user = notify_service(notify_db, notify_db_session) - template = Template.query.get(current_app.config['PASSWORD_RESET_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['PASSWORD_RESET_TEMPLATE_ID'], - 'name': 'Password reset template', - 'template_type': 'email', - 'content': '((user_name)) you can reset password by clicking ((url))', - 'subject': 'Reset your password', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + return create_notify_template(service=service, + user=user, + template_config_name='PASSWORD_RESET_TEMPLATE_ID', + content='((user_name)) you can reset password by clicking ((url))', + subject='Reset your password', + template_type='email') @pytest.fixture(scope='function') def already_registered_template(notify_db, notify_db_session): + service, user = notify_service(notify_db, notify_db_session) + + content = """Sign in here: ((signin_url)) If you’ve forgotten your password, + you can reset it here: ((forgot_password_url)) feedback:((feedback_url))""" + return create_notify_template(service=service, user=user, + template_config_name='ALREADY_REGISTERED_EMAIL_TEMPLATE_ID', + content=content, + template_type='email') + + +@pytest.fixture(scope='function') +def change_email_confirmation_template(notify_db, + notify_db_session): + service, user = notify_service(notify_db, notify_db_session) + content = """Hi ((name)), + Click this link to confirm your new email address: + ((url)) + If you didn’t try to change the email address for your GOV.UK Notify account, let us know here: + ((feedback_url))""" + template = create_notify_template(service=service, + user=user, + template_config_name='CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID', + content=content, + template_type='email') + return template + + +def create_notify_template(service, user, template_config_name, content, template_type, subject=None): + template = Template.query.get(current_app.config[template_config_name]) + if not template: + data = { + 'id': current_app.config[template_config_name], + 'name': template_config_name, + 'template_type': template_type, + 'content': content, + 'service': service, + 'created_by': user, + 'subject': subject, + 'archived': False + } + template = Template(**data) + db.session.add(template) + return template + + +def notify_service(notify_db, notify_db_session): user = sample_user(notify_db, notify_db_session) service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) if not service: @@ -865,22 +821,7 @@ def already_registered_template(notify_db, } service = Service(**data) db.session.add(service) - - template = Template.query.get(current_app.config['ALREADY_REGISTERED_EMAIL_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['ALREADY_REGISTERED_EMAIL_TEMPLATE_ID'], - 'name': 'ALREADY_REGISTERED_EMAIL_TEMPLATE_ID', - 'template_type': 'email', - 'content': """Sign in here: ((signin_url)) If you’ve forgotten your password, - you can reset it here: ((forgot_password_url)) feedback:((feedback_url))""", - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + return service, user @pytest.fixture(scope='function') diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index 9d1bf35c7..14b26d387 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -6,6 +6,7 @@ from freezegun import freeze_time import app from app.models import (User, Permission, MANAGE_SETTINGS, MANAGE_TEMPLATES) from app.dao.permissions_dao import default_service_permissions +from app.utils import url_with_token from tests import create_authorization_header @@ -516,3 +517,50 @@ def test_send_already_registered_email_returns_400_when_data_is_missing(notify_a headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 400 assert json.loads(resp.get_data(as_text=True))['message'] == {'email': ['Missing data for required field.']} + + +@freeze_time("2016-01-01T11:09:00.061258") +def test_send_user_confirm_new_email_returns_204(notify_api, sample_user, change_email_confirmation_template, mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocked = mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('uuid.uuid4', return_value='some_uuid') # for the notification id + new_email = 'new_address@dig.gov.uk' + data = json.dumps({'email': new_email}) + auth_header = create_authorization_header() + + resp = client.post(url_for('user.send_user_confirm_new_email', user_id=str(sample_user.id)), + data=data, + headers=[('Content-Type', 'application/json'), auth_header]) + assert resp.status_code == 204 + token_data = json.dumps({'user_id': str(sample_user.id), 'email': new_email}) + url = url_with_token(data=token_data, url='/user-profile/email/confirm/', config=current_app.config) + message = { + 'template': current_app.config['CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID'], + 'template_version': 1, + 'to': 'new_address@dig.gov.uk', + 'personalisation': { + 'name': sample_user.name, + 'url': url, + 'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/feedback' + } + } + mocked.assert_called_once_with(( + str(current_app.config['NOTIFY_SERVICE_ID']), + "some_uuid", + app.encryption.encrypt(message), + "2016-01-01T11:09:00.061258"), queue="notify") + + +def test_send_user_confirm_new_email_returns_400_when_email_missing(notify_api, sample_user, mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocked = mocker.patch('app.celery.tasks.send_email.apply_async') + data = json.dumps({}) + auth_header = create_authorization_header() + resp = client.post(url_for('user.send_user_confirm_new_email', user_id=str(sample_user.id)), + data=data, + headers=[('Content-Type', 'application/json'), auth_header]) + assert resp.status_code == 400 + assert json.loads(resp.get_data(as_text=True))['message'] == {'email': ['Missing data for required field.']} + mocked.assert_not_called()