From 7f96ef5a25203f89a75f74622ebe89a76e298958 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Fri, 27 Nov 2015 09:47:29 +0000 Subject: [PATCH] 108536490: Initial effort to implement log in Add endpoint for post to /sign-in Initialise role data --- app/main/__init__.py | 2 +- app/main/dao/users_dao.py | 6 ++++ app/main/encryption.py | 7 ++++ app/main/forms.py | 14 ++++++++ app/main/views/index.py | 5 --- app/main/views/sign_in.py | 43 +++++++++++++++++++++++ app/templates/signin.html | 31 +++++++++------- config.py | 1 + migrations/versions/20_initialise_data.py | 16 +++++++++ requirements.txt | 2 ++ tests/app/main/dao/test_get_all_users.py | 32 ----------------- tests/app/main/dao/test_roles_dao.py | 9 +++-- tests/app/main/dao/test_users_dao.py | 41 +++++++++++++++++++++ tests/app/main/test_encyption.py | 9 +++++ tests/app/main/views/__init__.py | 0 tests/app/main/views/test_sign_in.py | 17 +++++++++ tests/conftest.py | 4 +-- 17 files changed, 183 insertions(+), 56 deletions(-) create mode 100644 app/main/encryption.py create mode 100644 app/main/forms.py create mode 100644 app/main/views/sign_in.py create mode 100644 migrations/versions/20_initialise_data.py delete mode 100644 tests/app/main/dao/test_get_all_users.py create mode 100644 tests/app/main/test_encyption.py create mode 100644 tests/app/main/views/__init__.py create mode 100644 tests/app/main/views/test_sign_in.py diff --git a/app/main/__init__.py b/app/main/__init__.py index e3a397f4d..5cf60b5fe 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 +from app.main.views import index, sign_in diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index 97bdfb9f3..95d68eafa 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -1,8 +1,10 @@ from app import db from app.models import Users +from app.main.encryption import encrypt def insert_user(user): + user.password = encrypt(user.password) db.session.add(user) db.session.commit() @@ -13,3 +15,7 @@ def get_user_by_id(id): def get_all_users(): return Users.query.all() + + +def get_user_by_email(email_address): + return Users.query.filter_by(email_address=email_address).first() diff --git a/app/main/encryption.py b/app/main/encryption.py new file mode 100644 index 000000000..e42a42f22 --- /dev/null +++ b/app/main/encryption.py @@ -0,0 +1,7 @@ +import hashlib +from flask import current_app + + +def encrypt(value): + key = current_app.config['SECRET_KEY'] + return hashlib.sha256((key + value).encode('UTF-8')).hexdigest() diff --git a/app/main/forms.py b/app/main/forms.py new file mode 100644 index 000000000..a2cf5d9e9 --- /dev/null +++ b/app/main/forms.py @@ -0,0 +1,14 @@ +from flask_wtf import Form +from wtforms import StringField, PasswordField +from wtforms.validators import DataRequired, Email, Length + + +class LoginForm(Form): + email_address = StringField('Email address', validators=[ + Length(255), + DataRequired(message='Email cannot be empty'), + Email(message='Please enter a valid email address') + ]) + password = PasswordField('Password', validators=[ + DataRequired(message='Please enter your password') + ]) diff --git a/app/main/views/index.py b/app/main/views/index.py index 34cbd3e4c..de48cb9e6 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -43,11 +43,6 @@ def dashboard(): return render_template('dashboard.html') -@main.route("/sign-in") -def signin(): - return render_template('signin.html') - - @main.route("/add-service") def addservice(): return render_template('add-service.html') diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py new file mode 100644 index 000000000..e95f661bf --- /dev/null +++ b/app/main/views/sign_in.py @@ -0,0 +1,43 @@ +from datetime import datetime + +from flask import render_template, redirect, url_for, 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 Users +from app.main.encryption import encrypt + + +@main.route("/sign-in", methods=(['GET'])) +def render_sign_in(): + return render_template('signin.html', form=LoginForm()) + + +@main.route('/sign-in', methods=(['POST'])) +def process_sign_in(): + form = LoginForm() + if form.validate_on_submit(): + user = users_dao.get_user_by_email(form.email_address) + if user is None: + return jsonify(authorization=False), 404 + if user.password == encrypt(form.password): + login_user(user) + else: + return jsonify(authorization=False), 404 + + return redirect('/two-factor') + + +@main.route('/create_user', methods=(['POST'])) +def create_user_for_test(): + form = LoginForm() + user = Users(email_address=form.email_address, + name=form.email_address, + password=form.password, + created_at=datetime.now(), + role_id=1) + users_dao.insert_user(user) + + return 'created' diff --git a/app/templates/signin.html b/app/templates/signin.html index 3d6609010..7aea22245 100644 --- a/app/templates/signin.html +++ b/app/templates/signin.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +Sign in {% endblock %} {% block content %} @@ -12,19 +12,24 @@ Hello world!

If you do not have an account, you can register.

-

- -
-

-

- -
- Forgotten password? -

+
-

- Continue -

+

+ + {{ form.email_address(class="form-control-2-3", autocomplete="off") }}
+

+

+ + {{ form.password(class="form-control-1-4", autocomplete="off") }}
+

+

+ Forgotten password? +

+ +

+ Continue +

+
diff --git a/config.py b/config.py index 002f99162..5c1ecfea1 100644 --- a/config.py +++ b/config.py @@ -9,6 +9,7 @@ class Config(object): SQLALCHEMY_RECORD_QUERIES = True SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notifications_admin' MAX_FAILED_LOGIN_COUNT = 10 + SECRET_KEY = 'secret-key-unique-changeme' class Development(Config): diff --git a/migrations/versions/20_initialise_data.py b/migrations/versions/20_initialise_data.py new file mode 100644 index 000000000..4784310e6 --- /dev/null +++ b/migrations/versions/20_initialise_data.py @@ -0,0 +1,16 @@ +# revision identifiers, used by Alembic. +revision = '20_initialise_data' +down_revision = None + +from alembic import op + +def upgrade(): + op.bulk_insert('roles', + [ + {'role': 'plaform_admin'}, + {'role': 'service_user'} + ]) + +def downgrade(): + op.drop_table('users') + op.drop_table('roles') diff --git a/requirements.txt b/requirements.txt index b415c290c..b66dabd3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ Flask-SQLAlchemy==2.0 psycopg2==2.6.1 SQLAlchemy==1.0.5 SQLAlchemy-Utils==0.30.5 +Flask-WTF==0.11 +Flask-Login==0.2.11 \ No newline at end of file diff --git a/tests/app/main/dao/test_get_all_users.py b/tests/app/main/dao/test_get_all_users.py deleted file mode 100644 index 96229db72..000000000 --- a/tests/app/main/dao/test_get_all_users.py +++ /dev/null @@ -1,32 +0,0 @@ -from datetime import datetime - -from app.main.dao import users_dao -from app.models import Users - - -def test_get_all_users_returns_all_users(notifications_admin, notifications_admin_db): - user1 = Users(name='test one', - password='somepassword', - email_address='test1@get_all.gov.uk', - mobile_number='+441234123412', - created_at=datetime.now(), - role_id=1) - user2 = Users(name='test two', - password='some2ndpassword', - email_address='test2@get_all.gov.uk', - mobile_number='+441234123412', - created_at=datetime.now(), - role_id=1) - user3 = Users(name='test three', - password='some2ndpassword', - email_address='test2@get_all.gov.uk', - mobile_number='+441234123412', - created_at=datetime.now(), - role_id=1) - - users_dao.insert_user(user1) - users_dao.insert_user(user2) - users_dao.insert_user(user3) - users = users_dao.get_all_users() - assert len(users) == 3 - assert users == [user1, user2, user3] diff --git a/tests/app/main/dao/test_roles_dao.py b/tests/app/main/dao/test_roles_dao.py index ac2ce2193..14d28102d 100644 --- a/tests/app/main/dao/test_roles_dao.py +++ b/tests/app/main/dao/test_roles_dao.py @@ -13,9 +13,12 @@ def test_insert_role_should_be_able_to_get_role(notifications_admin, notificatio assert saved_role == role -def test_insert_role_will_throw_error_if_role_already_exists(): +def test_insert_role_will_throw_error_if_role_already_exists(notifications_admin, notifications_admin_db): + role1 = roles_dao.get_role_by_id(1) + assert role1.id == 1 + role = Roles(id=1, role='cannot create a duplicate') - with pytest.raises(sqlalchemy.exc.IntegrityError) as error: + with pytest.raises(sqlalchemy.orm.exc.FlushError) as error: roles_dao.insert_role(role) - assert 'duplicate key value violates unique constraint "roles_pkey"' in str(error.value) + assert 'conflicts with persistent instance' in str(error.value) diff --git a/tests/app/main/dao/test_users_dao.py b/tests/app/main/dao/test_users_dao.py index a4b8a910c..3f734ff87 100644 --- a/tests/app/main/dao/test_users_dao.py +++ b/tests/app/main/dao/test_users_dao.py @@ -30,3 +30,44 @@ def test_insert_user_with_role_that_does_not_exist_fails(notifications_admin, no with pytest.raises(sqlalchemy.exc.IntegrityError) as error: users_dao.insert_user(user) assert 'insert or update on table "users" violates foreign key constraint "users_role_id_fkey"' in str(error.value) + + +def test_get_user_by_email(notifications_admin, notifications_admin_db): + user = Users(name='test_get_by_email', + password='somepassword', + email_address='email@example.gov.uk', + mobile_number='+441234153412', + created_at=datetime.now(), + role_id=1) + + users_dao.insert_user(user) + retrieved = users_dao.get_user_by_email(user.email_address) + assert retrieved == user + + +def test_get_all_users_returns_all_users(notifications_admin, notifications_admin_db): + user1 = Users(name='test one', + password='somepassword', + email_address='test1@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + user2 = Users(name='test two', + password='some2ndpassword', + email_address='test2@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + user3 = Users(name='test three', + password='some2ndpassword', + email_address='test2@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + + users_dao.insert_user(user1) + users_dao.insert_user(user2) + users_dao.insert_user(user3) + users = users_dao.get_all_users() + assert len(users) == 3 + assert users == [user1, user2, user3] diff --git a/tests/app/main/test_encyption.py b/tests/app/main/test_encyption.py new file mode 100644 index 000000000..a85aa59d9 --- /dev/null +++ b/tests/app/main/test_encyption.py @@ -0,0 +1,9 @@ +from app.main import encryption + + +def test_encryption(notifications_admin): + value = 's3curePassword!' + + encrypted = encryption.encrypt(value) + + assert encrypted == encryption.encrypt(value) diff --git a/tests/app/main/views/__init__.py b/tests/app/main/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py new file mode 100644 index 000000000..66e2ebaa2 --- /dev/null +++ b/tests/app/main/views/test_sign_in.py @@ -0,0 +1,17 @@ + + +def test_render_sign_in_returns_sign_in_template(notifications_admin): + response = notifications_admin.test_client().get('/sign-in') + assert response.status_code == 200 + assert 'Sign in' in response.get_data(as_text=True) + assert 'Email address' in response.get_data(as_text=True) + assert 'Password' in response.get_data(as_text=True) + assert 'Forgotten password?' in response.get_data(as_text=True) + + +def test_process_sign_in_return_2fa_template(notifications_admin): + response = notifications_admin.test_client().post('/sign-in', + data={'email_address': 'valid@example.gov.uk', + 'password': 'val1dPassw0rd!'}) + assert response.status_code == 302 + assert response.location == 'http://localhost/two-factor' diff --git a/tests/conftest.py b/tests/conftest.py index faedd70b1..338e4d63b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ from app import create_app, db from app.models import Roles -@pytest.fixture(scope='module') +@pytest.fixture(scope='function') def notifications_admin(request): app = create_app('test') ctx = app.app_context() @@ -18,7 +18,7 @@ def notifications_admin(request): return app -@pytest.fixture(scope='module') +@pytest.fixture(scope='function') def notifications_admin_db(notifications_admin, request): metadata = MetaData(db.engine) metadata.reflect()