diff --git a/app/__init__.py b/app/__init__.py index 333a108b3..30795420a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -34,7 +34,7 @@ def create_app(config_name): init_csrf(application) login_manager.init_app(application) - login_manager.login_view = 'main.sign_in.render_sign_in' + login_manager.login_view = 'main.render_sign_in' from app.main import main as main_blueprint application.register_blueprint(main_blueprint) diff --git a/app/main/__init__.py b/app/main/__init__.py index 1305744ce..506599396 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -3,4 +3,4 @@ from flask import Blueprint main = Blueprint('main', __name__) -from app.main.views import index, sign_in, register +from app.main.views import index, sign_in, register, verify diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index d4d3a0b42..83521985e 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -30,3 +30,10 @@ def increment_failed_login_count(id): user = User.query.filter_by(id=id).first() user.failed_login_count += 1 db.session.commit() + + +def activate_user(id): + user = get_user_by_id(id) + user.state = 'active' + db.session.add(user) + db.session.commit() diff --git a/app/main/forms.py b/app/main/forms.py index ea5978fa3..4b8e400ba 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1,7 +1,9 @@ +from flask import session from flask_wtf import Form -from wtforms import StringField, PasswordField +from wtforms import StringField, PasswordField, IntegerField from wtforms.validators import DataRequired, Email, Length, Regexp +from app.main.encryption import checkpw from app.main.validators import Blacklist @@ -18,6 +20,7 @@ class LoginForm(Form): gov_uk_email = "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.gov.uk)" mobile_number = "^\\+44[\\d]{10}$" +verify_code = "[\\d]{5}$" class RegisterUserForm(Form): @@ -36,3 +39,28 @@ class RegisterUserForm(Form): validators=[DataRequired(message='Please enter your password'), Length(10, 255, message='Password must be at least 10 characters'), Blacklist(message='That password is blacklisted, too common')]) + + +class VerifyForm(Form): + sms_code = StringField("Text message confirmation code", + validators=[DataRequired(message='SMS code can not be empty'), + Regexp(regex=verify_code, message='Code must be 5 digits')]) + email_code = StringField("Email confirmation code", + validators=[DataRequired(message='Email code can not be empty'), + Regexp(regex=verify_code, message='Code must be 5 digits')]) + + def validate_email_code(self, a): + if self.email_code.data is not None: + if checkpw(str(self.email_code.data), session['email_code']) is False: + self.email_code.errors.append('Code does not match') + return False + else: + return True + + def validate_sms_code(self, a): + if self.sms_code.data is not None: + if checkpw(str(self.sms_code.data), session['sms_code']) is False: + self.sms_code.errors.append('Code does not match') + return False + else: + return True diff --git a/app/main/views/index.py b/app/main/views/index.py index f664ee835..6f46ae671 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -19,11 +19,6 @@ def registerfrominvite(): return render_template('register-from-invite.html') -@main.route("/verify") -def verify(): - return render_template('verify.html') - - @main.route("/verify-mobile") def verifymobile(): return render_template('verify-mobile.html') diff --git a/app/main/views/register.py b/app/main/views/register.py index 48c9d68fa..953d514b5 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -36,6 +36,7 @@ def process_register(): session['email_code'] = hashpw(email_code) session['expiry_date'] = str(datetime.now() + timedelta(hours=1)) users_dao.insert_user(user) + session['user_id'] = user.id except AdminApiClientException as e: return jsonify(admin_api_client_error=e.value) except SQLAlchemyError: diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 5fb1abc5b..aaa6d827b 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -1,13 +1,10 @@ -from datetime import datetime - from flask import render_template, redirect, jsonify from flask_login import login_user from app.main import main -from app.main.forms import LoginForm from app.main.dao import users_dao -from app.models import User from app.main.encryption import checkpw +from app.main.forms import LoginForm @main.route("/sign-in", methods=(['GET'])) diff --git a/app/main/views/verify.py b/app/main/views/verify.py new file mode 100644 index 000000000..3e3c139af --- /dev/null +++ b/app/main/views/verify.py @@ -0,0 +1,24 @@ +from flask import render_template, redirect, jsonify, session +from flask_login import login_user + +from app.main import main +from app.main.dao import users_dao +from app.main.forms import VerifyForm + + +@main.route('/verify', methods=['GET']) +def render_verify(): + return render_template('verify.html', form=VerifyForm()) + + +@main.route('/verify', methods=['POST']) +def process_verify(): + form = VerifyForm() + if form.validate_on_submit(): + user = users_dao.get_user_by_id(session['user_id']) + users_dao.activate_user(user.id) + login_user(user) + return redirect('/add-service') + else: + print(form.errors) + return jsonify(form.errors), 400 diff --git a/app/templates/verify.html b/app/templates/verify.html index a6ab5b8f5..0248e2348 100644 --- a/app/templates/verify.html +++ b/app/templates/verify.html @@ -12,20 +12,23 @@ GOV.UK Notify | Confirm email address and mobile number

We've sent you confirmation codes by email and text message. You need to enter both codes here.

-

-

-

-

+
+ {{ form.hidden_tag() }} +

+ + {{ form.email_code(class="form-control-1-4", autocomplete="off") }}
+ I haven't received an email +

+

+ + {{ form.sms_code(class="form-control-1-4", autocomplete="off") }}
+ I haven't received a text +

-

- Continue -

+

+ +

+
diff --git a/tests/app/main/dao/test_users_dao.py b/tests/app/main/dao/test_users_dao.py index 9fde6bd9b..f6d8d5f87 100644 --- a/tests/app/main/dao/test_users_dao.py +++ b/tests/app/main/dao/test_users_dao.py @@ -119,3 +119,23 @@ def test_user_is_active_is_false_if_state_is_inactive(notifications_admin, notif saved_user = users_dao.get_user_by_id(user.id) assert saved_user.is_active() is False + + +def test_should_update_user_to_active(notifications_admin, notifications_admin_db): + user = User(name='Make user active', + password='somepassword', + email_address='activate@user.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1, + state='pending') + users_dao.insert_user(user) + users_dao.activate_user(user.id) + updated_user = users_dao.get_user_by_id(user.id) + assert updated_user.state == 'active' + + +def test_should_throws_error_when_id_does_not_exist(notifications_admin, notifications_admin_db): + with pytest.raises(AttributeError) as error: + users_dao.activate_user(123) + assert '''object has no attribute 'state''''' in str(error.value) diff --git a/tests/app/main/views/test_verify.py b/tests/app/main/views/test_verify.py new file mode 100644 index 000000000..17ebf35bf --- /dev/null +++ b/tests/app/main/views/test_verify.py @@ -0,0 +1,157 @@ +from datetime import datetime + +from app.main.dao import users_dao +from app.main.encryption import hashpw +from app.models import User + + +def test_should_return_verify_template(notifications_admin, notifications_admin_db): + response = notifications_admin.test_client().get('/verify') + assert response.status_code == 200 + assert 'Activate your account' in response.get_data(as_text=True) + + +def test_should_redirect_to_add_service_when_code_are_correct(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('12345') + session['email_code'] = hashpw('23456') + response = client.post('/verify', + data={'sms_code': '12345', + 'email_code': '23456'}) + assert response.status_code == 302 + assert response.location == 'http://localhost/add-service' + + +def test_should_activate_user_after_verify(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('12345') + session['email_code'] = hashpw('23456') + client.post('/verify', + data={'sms_code': '12345', + 'email_code': '23456'}) + + after_verify = users_dao.get_user_by_id(user.id) + assert after_verify.state == 'active' + + +def test_should_return_400_when_sms_code_is_wrong(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('12345') + session['email_code'] = hashpw('23456') + response = client.post('/verify', + data={'sms_code': '98765', + 'email_code': '23456'}) + assert response.status_code == 400 + assert 'sms_code' in response.get_data(as_text=True) + assert 'Code does not match' in response.get_data(as_text=True) + + +def test_should_return_400_when_email_code_is_wrong(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('12345') + session['email_code'] = hashpw('98456') + response = client.post('/verify', + data={'sms_code': '12345', + 'email_code': '23456'}) + assert response.status_code == 400 + print(response.get_data(as_text=True)) + assert 'email_code' in response.get_data(as_text=True) + assert 'Code does not match' in response.get_data(as_text=True) + + +def test_should_return_400_when_sms_code_is_missing(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('12345') + session['email_code'] = hashpw('98456') + response = client.post('/verify', + data={'email_code': '23456'}) + assert response.status_code == 400 + assert 'SMS code can not be empty' in response.get_data(as_text=True) + + +def test_should_return_400_when_email_code_is_missing(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('23456') + session['email_code'] = hashpw('23456') + response = client.post('/verify', + data={'sms_code': '23456'}) + assert response.status_code == 400 + assert 'Email code can not be empty' in response.get_data(as_text=True) + + +def test_should_return_400_when_email_code_has_letter(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('23456') + session['email_code'] = hashpw('23456') + response = client.post('/verify', + data={'sms_code': '23456', + 'email_code': 'abcde'}) + data = response.get_data(as_text=True) + assert response.status_code == 400 + assert 'email_code' in data + assert 'Code does not match' in data + assert 'Code must be 5 digits' in data + + +def test_should_return_400_when_sms_code_is_too_short(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('23456') + session['email_code'] = hashpw('23456') + response = client.post('/verify', + data={'sms_code': '2345', + 'email_code': '23456'}) + assert response.status_code == 400 + data = response.get_data(as_text=True) + assert 'sms_code' in data + assert 'Code must be 5 digits' in data + assert 'Code does not match' in data + + +def test_should_return_302_when_email_code_starts_with_zero(notifications_admin, notifications_admin_db): + with notifications_admin.test_client() as client: + with client.session_transaction() as session: + user = _create_test_user() + session['user_id'] = user.id + session['sms_code'] = hashpw('23456') + session['email_code'] = hashpw('09765') + response = client.post('/verify', + data={'sms_code': '23456', + 'email_code': '09765'}) + assert response.status_code == 302 + assert response.location == 'http://localhost/add-service' + + +def _create_test_user(): + user = User(name='Test User', + password='somepassword', + email_address='test@user.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1, + state='pending') + users_dao.insert_user(user) + return user