From d36f86d2c24e86d8b527a93d41da7107eb22461b Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:08:12 +0000 Subject: [PATCH 01/44] Added shell page for sending activity (jobs index) --- app/templates/jobs.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/templates/jobs.html diff --git a/app/templates/jobs.html b/app/templates/jobs.html new file mode 100644 index 000000000..c3127a92c --- /dev/null +++ b/app/templates/jobs.html @@ -0,0 +1,21 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | Notifications activity +{% endblock %} + +{% block content %} + +
+
+

Notifications activity

+ +

This page will be where we show the list of jobs that this service has processed

+ +

+ view a particular notification job +

+
+
+ +{% endblock %} From 2cb2d538eaa00a795e3b818be1619e802336dafd Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:13:25 +0000 Subject: [PATCH 02/44] Added shell page for sending activity (job contents) --- app/templates/job.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/templates/job.html diff --git a/app/templates/job.html b/app/templates/job.html new file mode 100644 index 000000000..1a97f2653 --- /dev/null +++ b/app/templates/job.html @@ -0,0 +1,21 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | Notifications activity +{% endblock %} + +{% block content %} + +
+
+

Notifications for a specific job

+ +

This page will be where we list the notifications for a specific job.

+ +

+ view a specific notification +

+
+
+ +{% endblock %} From 3dd81b9b12a68b4312f56f8765466e9a7be171d9 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:23:18 +0000 Subject: [PATCH 03/44] Added shell page for sending activity (notification) --- app/templates/notification.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/templates/notification.html diff --git a/app/templates/notification.html b/app/templates/notification.html new file mode 100644 index 000000000..9601e5e03 --- /dev/null +++ b/app/templates/notification.html @@ -0,0 +1,21 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | Notifications activity +{% endblock %} + +{% block content %} + +
+
+

A specific notification

+ +

This page will be where we show what happened for a specific notification.

+ +

+ View other notifications in this job +

+
+
+ +{% endblock %} From 90603ed2ca32c6806be8b43ed5ffcb42b8f91c45 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:26:58 +0000 Subject: [PATCH 04/44] Added link back to activity hub to the jobs page --- app/templates/job.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/templates/job.html b/app/templates/job.html index 1a97f2653..53f62f4ab 100644 --- a/app/templates/job.html +++ b/app/templates/job.html @@ -12,9 +12,10 @@ GOV.UK Notify | Notifications activity

This page will be where we list the notifications for a specific job.

-

- view a specific notification -

+ From b70c05ab8fa387839b5104b255d0da8170b3978a Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:34:31 +0000 Subject: [PATCH 05/44] Added activity views to the index --- app/main/views/index.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/main/views/index.py b/app/main/views/index.py index fb7d299c9..aa3f28495 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -76,3 +76,18 @@ def sendemail(): @main.route("/check-email") def checkemail(): return render_template('check_email.html') + + +@main.route("/jobs") +def showjobs(): + return render_template('jobs.html') + + +@main.route("/jobs/job") +def showjob(): + return render_template('job.html') + + +@main.route("/jobs/job/notification") +def shownotification(): + return render_template('notification.html') From 9309d3ee6cc6d83b103dabbd23467a74aaa21f47 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:35:01 +0000 Subject: [PATCH 06/44] Updated path from jobs to job --- app/templates/jobs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/jobs.html b/app/templates/jobs.html index c3127a92c..b38f5da9f 100644 --- a/app/templates/jobs.html +++ b/app/templates/jobs.html @@ -13,7 +13,7 @@ GOV.UK Notify | Notifications activity

This page will be where we show the list of jobs that this service has processed

- view a particular notification job + view a particular notification job

From 6d17951ef94382024e84411ef72d67840e510e4c Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:35:39 +0000 Subject: [PATCH 07/44] Updated path from job to notification --- app/templates/job.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/job.html b/app/templates/job.html index 53f62f4ab..c1e5b7e12 100644 --- a/app/templates/job.html +++ b/app/templates/job.html @@ -13,7 +13,7 @@ GOV.UK Notify | Notifications activity

This page will be where we list the notifications for a specific job.

From 6e1d21b1adfa1577b136fd099f3e1d4431589f61 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 15:46:21 +0000 Subject: [PATCH 08/44] Added link to activity from the dashboard --- app/templates/dashboard.html | 1 + 1 file changed, 1 insertion(+) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index d566dbb39..1608f5851 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -13,6 +13,7 @@ GOV.UK Notify | Dashboard From b60fbf85ff85c2bcb42374a8f0367a9ea632eec0 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:06:33 +0000 Subject: [PATCH 09/44] Fixed path back to all jobs [ci skip] --- app/templates/job.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/job.html b/app/templates/job.html index c1e5b7e12..ccaee802c 100644 --- a/app/templates/job.html +++ b/app/templates/job.html @@ -14,7 +14,7 @@ GOV.UK Notify | Notifications activity From b624b9d39f1019c11dd9d591846820b179bf02dc Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:07:02 +0000 Subject: [PATCH 10/44] Fixed path back to parent job [ci skip] --- app/templates/notification.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/notification.html b/app/templates/notification.html index 9601e5e03..ba5d6f976 100644 --- a/app/templates/notification.html +++ b/app/templates/notification.html @@ -13,7 +13,7 @@ GOV.UK Notify | Notifications activity

This page will be where we show what happened for a specific notification.

- View other notifications in this job + View other notifications in this job

From 96648ed58b846544a5f8a08eca75c41431df0d6d Mon Sep 17 00:00:00 2001 From: Chris Heathcote Date: Mon, 30 Nov 2015 16:08:44 +0000 Subject: [PATCH 11/44] Added email and text message not received pages in registration flow --- app/main/views/index.py | 10 +++++++++ app/templates/email-not-received.html | 28 +++++++++++++++++++++++++ app/templates/text-not-received-2.html | 29 ++++++++++++++++++++++++++ app/templates/text-not-received.html | 27 ++++++++++++++++++++++++ app/templates/verify-mobile.html | 2 +- app/templates/verify.html | 4 ++-- 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 app/templates/email-not-received.html create mode 100644 app/templates/text-not-received-2.html create mode 100644 app/templates/text-not-received.html diff --git a/app/main/views/index.py b/app/main/views/index.py index a40d3a0cf..89bf7efca 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -66,3 +66,13 @@ def sendsms(): @main.route("/check-sms") def checksms(): return render_template('check_sms.html') + + +@main.route("/email-not-received") +def emailnotreceived(): + return render_template('email-not-received.html') + + +@main.route("/text-not-received") +def textnotreceived(): + return render_template('text-not-received.html') diff --git a/app/templates/email-not-received.html b/app/templates/email-not-received.html new file mode 100644 index 000000000..462fbb961 --- /dev/null +++ b/app/templates/email-not-received.html @@ -0,0 +1,28 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify +{% endblock %} + +{% block content %} + +
+
+

Check your email address

+ +

Check your email address is correct and resend a confirmation code.

+ +

+ +
+ Your email address must end in .gov.uk +

+ + +

+ Resend confirmation code +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/text-not-received-2.html b/app/templates/text-not-received-2.html new file mode 100644 index 000000000..c5ffb8e8e --- /dev/null +++ b/app/templates/text-not-received-2.html @@ -0,0 +1,29 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify +{% endblock %} + +{% block content %} + + + +
+
+

Check your mobile number

+ +

Check your mobile phone number is correct and resend a confirmation code.

+ +

+ + +

+ + +

+ Resend confirmation code +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/text-not-received.html b/app/templates/text-not-received.html new file mode 100644 index 000000000..ce8fff18c --- /dev/null +++ b/app/templates/text-not-received.html @@ -0,0 +1,27 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify +{% endblock %} + +{% block content %} + +
+
+

Check your mobile number

+ +

Check your mobile phone number is correct and resend a confirmation code.

+ +

+ + +

+ + +

+ Resend confirmation code +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/verify-mobile.html b/app/templates/verify-mobile.html index 92e77f3b5..178cc00d0 100644 --- a/app/templates/verify-mobile.html +++ b/app/templates/verify-mobile.html @@ -17,7 +17,7 @@ Hello world!
- I haven't received a text + I haven't received a text

diff --git a/app/templates/verify.html b/app/templates/verify.html index 9d407edff..ff462664f 100644 --- a/app/templates/verify.html +++ b/app/templates/verify.html @@ -17,13 +17,13 @@ Hello world!
- I haven't received an email + I haven't received an email


- I haven't received a text + I haven't received a text

From 87cadc3ddeea46b5dfb30a3b8e908a280579c62e Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:13:33 +0000 Subject: [PATCH 12/44] Added the shell user profile page [ci skip] --- app/templates/user-profile.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/templates/user-profile.html diff --git a/app/templates/user-profile.html b/app/templates/user-profile.html new file mode 100644 index 000000000..a394b8bd8 --- /dev/null +++ b/app/templates/user-profile.html @@ -0,0 +1,20 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | User settings +{% endblock %} + +{% block content %} + +

+
+

User profile

+ +

Here's where users can update their profile, password etc.

+ +

Back to dashboard

+ +
+
+ +{% endblock %} From 68f7dfb814f2bc1a68d6fb6e75aaeeb4125fddc2 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:15:51 +0000 Subject: [PATCH 13/44] Added the shell manage users page [ci skip] --- app/templates/manage-users.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/templates/manage-users.html diff --git a/app/templates/manage-users.html b/app/templates/manage-users.html new file mode 100644 index 000000000..8a93b2072 --- /dev/null +++ b/app/templates/manage-users.html @@ -0,0 +1,20 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | Manage users +{% endblock %} + +{% block content %} + +
+
+

Manage users

+ +

Here's where you can add or remove users of a service.

+ +

Back to dashboard

+ +
+
+ +{% endblock %} From d534c97333103e1f8a3240ba86240ecca7344b79 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:16:58 +0000 Subject: [PATCH 14/44] Added the shell service settings page [ci skip] --- app/templates/service-settings.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/templates/service-settings.html diff --git a/app/templates/service-settings.html b/app/templates/service-settings.html new file mode 100644 index 000000000..3e6c7bcb4 --- /dev/null +++ b/app/templates/service-settings.html @@ -0,0 +1,20 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | Service settings +{% endblock %} + +{% block content %} + +
+
+

Service settings

+ +

Here's where users can update their service profile.

+ +

Back to dashboard

+ +
+
+ +{% endblock %} From 48f722b3b99eb3272b159fbf2f4a87e0b92bf9f8 Mon Sep 17 00:00:00 2001 From: Chris Heathcote Date: Mon, 30 Nov 2015 16:19:59 +0000 Subject: [PATCH 15/44] Added forgot password / create new password screens Create new password would be sent in an email to the user. --- app/main/views/index.py | 10 ++++++++++ app/templates/forgot-password.html | 27 +++++++++++++++++++++++++++ app/templates/new-password.html | 24 ++++++++++++++++++++++++ app/templates/signin.html | 2 +- 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/templates/forgot-password.html create mode 100644 app/templates/new-password.html diff --git a/app/main/views/index.py b/app/main/views/index.py index ee5e2dab1..02f61ff6c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -101,3 +101,13 @@ def showjob(): @main.route("/jobs/job/notification") def shownotification(): return render_template('notification.html') + + +@main.route("/forgot-password") +def forgotpassword(): + return render_template('forgot-password.html') + + +@main.route("/new-password") +def newpassword(): + return render_template('new-password.html') diff --git a/app/templates/forgot-password.html b/app/templates/forgot-password.html new file mode 100644 index 000000000..e94945d4f --- /dev/null +++ b/app/templates/forgot-password.html @@ -0,0 +1,27 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify +{% endblock %} + +{% block content %} + +
+
+

Create a new password

+ +

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

+ +

+ + +

+ + +

+ Send email +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/new-password.html b/app/templates/new-password.html new file mode 100644 index 000000000..0d2c811f7 --- /dev/null +++ b/app/templates/new-password.html @@ -0,0 +1,24 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify +{% endblock %} + +{% block content %} + +
+
+

Create a new password

+ +

+ + +

+ +

+ Continue +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/signin.html b/app/templates/signin.html index 3d8df9d9d..3d6609010 100644 --- a/app/templates/signin.html +++ b/app/templates/signin.html @@ -19,7 +19,7 @@ Hello world!


- Forgotten password? + Forgotten password?

From 9b1b6e567bc7fd6accda678a49d16325f1ffbd0f Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:20:45 +0000 Subject: [PATCH 16/44] Added the shell API keys page [ci skip] --- app/templates/api-keys.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/templates/api-keys.html diff --git a/app/templates/api-keys.html b/app/templates/api-keys.html new file mode 100644 index 000000000..557a0c0f0 --- /dev/null +++ b/app/templates/api-keys.html @@ -0,0 +1,20 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify | API keys and documentation +{% endblock %} + +{% block content %} + +

+
+

API keys and documentation

+ +

Here's where developers can access information about the API and access keys

+ +

Back to dashboard

+ +
+
+ +{% endblock %} From 021ace9a87af90c4fa4c7e029a77527a540b248f Mon Sep 17 00:00:00 2001 From: Chris Heathcote Date: Mon, 30 Nov 2015 16:22:26 +0000 Subject: [PATCH 17/44] Added text to the register screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prewar users they’ll need access to their email account and mobile phone when registering. --- app/templates/register.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/templates/register.html b/app/templates/register.html index 18a0131af..74a8eb2f0 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -12,6 +12,8 @@ Hello world!

If you've used GOV.UK Notify before, sign in to your account.

+

You need to have access to your email account and a mobile phone to register.

+

From e488abadd20e2f0ca016456ca6d7483fd2ca3913 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:22:35 +0000 Subject: [PATCH 18/44] Updated the dashboard with links to the admin type pages [ci skip] --- app/templates/dashboard.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 1608f5851..4266e5f76 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -14,6 +14,10 @@ GOV.UK Notify | Dashboard

  • Send text messages
  • Send email messages
  • View notifications activity
  • +
  • User profile
  • +
  • Manage users
  • +
  • Service settings
  • +
  • API keys and documentation
  • From 2a70bbe27678e43e35519ad8020a4b013697fdba Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:23:41 +0000 Subject: [PATCH 19/44] Updated the URL from `_` to `-` on send sms page --- app/templates/{send_sms.html => send-sms.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/templates/{send_sms.html => send-sms.html} (100%) diff --git a/app/templates/send_sms.html b/app/templates/send-sms.html similarity index 100% rename from app/templates/send_sms.html rename to app/templates/send-sms.html From d6b2ec83d867612fcac5065336cfd9b097fdb868 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:24:24 +0000 Subject: [PATCH 20/44] Updated the URL from `_` to `-` on send email page --- app/templates/{send_email.html => send-email.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/templates/{send_email.html => send-email.html} (100%) diff --git a/app/templates/send_email.html b/app/templates/send-email.html similarity index 100% rename from app/templates/send_email.html rename to app/templates/send-email.html From ed39494d5dffd5bfa26215afc26c95156493517c Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:25:16 +0000 Subject: [PATCH 21/44] Updated the URL from `_` to `-` on check sms page [ci skip] --- app/templates/{check_sms.html => check-sms.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/templates/{check_sms.html => check-sms.html} (100%) diff --git a/app/templates/check_sms.html b/app/templates/check-sms.html similarity index 100% rename from app/templates/check_sms.html rename to app/templates/check-sms.html From 911642e407214c4d0f5dc9cabdc55d24b657da2b Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:25:38 +0000 Subject: [PATCH 22/44] Updated the URL from `_` to `-` on check email page [ci skip] --- app/templates/{check_email.html => check-email.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/templates/{check_email.html => check-email.html} (100%) diff --git a/app/templates/check_email.html b/app/templates/check-email.html similarity index 100% rename from app/templates/check_email.html rename to app/templates/check-email.html From b9c5c374e9e7f5555ac083756821889aae570638 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:28:17 +0000 Subject: [PATCH 23/44] Added routes the new admin page shells /user-profile /manage-users /service-settings /api-keys --- app/main/views/index.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index aa3f28495..2c775dbe4 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -60,22 +60,22 @@ def twofactor(): @main.route("/send-sms") def sendsms(): - return render_template('send_sms.html') + return render_template('send-sms.html') @main.route("/check-sms") def checksms(): - return render_template('check_sms.html') + return render_template('check-sms.html') @main.route("/send-email") def sendemail(): - return render_template('send_email.html') + return render_template('send-email.html') @main.route("/check-email") def checkemail(): - return render_template('check_email.html') + return render_template('check-email.html') @main.route("/jobs") @@ -91,3 +91,23 @@ def showjob(): @main.route("/jobs/job/notification") def shownotification(): return render_template('notification.html') + + +@main.route("/user-profile") +def userprofile(): + return render_template('user-profile.html') + + +@main.route("/manage-users") +def manageusers(): + return render_template('manage-users.html') + + +@main.route("/service-settings") +def servicesettings(): + return render_template('service-settings.html') + + +@main.route("/api-keys") +def apikeys(): + return render_template('api-keys.html') From cd2285d78bc975c093172fb093173167ab0bd320 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Mon, 30 Nov 2015 16:38:29 +0000 Subject: [PATCH 24/44] Fixed broken return path to job from notification page --- app/templates/notification.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/notification.html b/app/templates/notification.html index ba5d6f976..bf7faa9e6 100644 --- a/app/templates/notification.html +++ b/app/templates/notification.html @@ -13,7 +13,7 @@ GOV.UK Notify | Notifications activity

    This page will be where we show what happened for a specific notification.

    - View other notifications in this job + View other notifications in this job

    From 1e08c9b1a80a371c01beb1d102fbd62d40af9202 Mon Sep 17 00:00:00 2001 From: Chris Heathcote Date: Mon, 30 Nov 2015 16:41:05 +0000 Subject: [PATCH 25/44] Added two-factor resending to sign in flow. Assumes user will have to get an admin to update phone number if lost. --- app/main/views/index.py | 5 ++++ app/templates/two-factor.html | 2 +- app/templates/verification-not-received.html | 24 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 app/templates/verification-not-received.html diff --git a/app/main/views/index.py b/app/main/views/index.py index 22d2db606..34cbd3e4c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -131,3 +131,8 @@ def servicesettings(): @main.route("/api-keys") def apikeys(): return render_template('api-keys.html') + + +@main.route("/verification-not-received") +def verificationnotreceived(): + return render_template('verification-not-received.html') diff --git a/app/templates/two-factor.html b/app/templates/two-factor.html index 338100141..9c61f9e68 100644 --- a/app/templates/two-factor.html +++ b/app/templates/two-factor.html @@ -16,7 +16,7 @@ Hello world!
    - I haven't received a text + I haven't received a text

    diff --git a/app/templates/verification-not-received.html b/app/templates/verification-not-received.html new file mode 100644 index 000000000..b21b16850 --- /dev/null +++ b/app/templates/verification-not-received.html @@ -0,0 +1,24 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +GOV.UK Notify +{% endblock %} + +{% block content %} + +

    +
    +

    Resend verification code

    + +

    Text messages sometimes take a few minutes to be received.
    If you have not received a text message, you can resend.

    + +

    If you no longer have access to your mobile phone and registered number, get in contact with your service manager to reset your number.

    P> + + +

    + Resend confirmation code +

    +
    +
    + +{% endblock %} \ No newline at end of file From f336eee18b1f5b33beb62cec136756ce3ff23d2f Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:50:20 +0000 Subject: [PATCH 26/44] Updated the title tag to be useful [ci skip] --- app/templates/add-service.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/add-service.html b/app/templates/add-service.html index 2191344c8..dd2f3b1a0 100644 --- a/app/templates/add-service.html +++ b/app/templates/add-service.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Set up service {% endblock %} {% block content %} @@ -28,4 +28,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From eb9a4e680ac6c1f5b92a8588e9311b9510fb361a Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:51:42 +0000 Subject: [PATCH 27/44] Remove the hello world page [ci skip] --- app/templates/hello-world.html | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 app/templates/hello-world.html diff --git a/app/templates/hello-world.html b/app/templates/hello-world.html deleted file mode 100644 index 04a97b8f5..000000000 --- a/app/templates/hello-world.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "admin_template.html" %} - -{% block page_title %} -GOV.UK notifications -{% endblock %} - -{% block content %} - -
    -
    -

    Hello world!

    - -

    Hello world.

    -
    -
    - -{% endblock %} \ No newline at end of file From 45f5ad5c30faacbe84833f921b138c355df60811 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:52:31 +0000 Subject: [PATCH 28/44] Updated html title [ci skip] --- app/templates/register-from-invite.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/register-from-invite.html b/app/templates/register-from-invite.html index ac4d49dcc..73169cc06 100644 --- a/app/templates/register-from-invite.html +++ b/app/templates/register-from-invite.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Create a user account {% endblock %} {% block content %} @@ -31,4 +31,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From 257d205f9a3587d267d9b4a5eef1660674a23187 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:53:14 +0000 Subject: [PATCH 29/44] Updated html title [ci skip] --- app/templates/register.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/register.html b/app/templates/register.html index 18a0131af..c66ee4adb 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Create an account {% endblock %} {% block content %} @@ -36,4 +36,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From bd2826f58408da76b180e7bddbba927759597ea3 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:54:58 +0000 Subject: [PATCH 30/44] Updated html title [ci skip] --- app/templates/signedout.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/signedout.html b/app/templates/signedout.html index 17bc940f4..d2beafabe 100644 --- a/app/templates/signedout.html +++ b/app/templates/signedout.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Get started {% endblock %} {% block content %} @@ -22,4 +22,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From 42fa80dd9027cecee4566f1f36c106d2852b58c8 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:55:37 +0000 Subject: [PATCH 31/44] Updated html title [ci skip] --- app/templates/two-factor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/two-factor.html b/app/templates/two-factor.html index 338100141..a5bced593 100644 --- a/app/templates/two-factor.html +++ b/app/templates/two-factor.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Text verification {% endblock %} {% block content %} @@ -25,4 +25,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From a541ef2164e8feea119b20985ab315663c310acd Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:56:06 +0000 Subject: [PATCH 32/44] Updated html title [ci skip] --- app/templates/verify-mobile.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/verify-mobile.html b/app/templates/verify-mobile.html index 92e77f3b5..cb2c90c37 100644 --- a/app/templates/verify-mobile.html +++ b/app/templates/verify-mobile.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Confirm mobile number {% endblock %} {% block content %} @@ -26,4 +26,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From 2a39d3a3da4527975ce8e6fdf3ecc0ccc150f75d Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:56:46 +0000 Subject: [PATCH 33/44] Updated html title [ci skip] --- app/templates/verify.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/verify.html b/app/templates/verify.html index 9d407edff..6b3e11b22 100644 --- a/app/templates/verify.html +++ b/app/templates/verify.html @@ -1,7 +1,7 @@ {% extends "admin_template.html" %} {% block page_title %} -Hello world! +GOV.UK Notify | Confirm email address and mobile number {% endblock %} {% block content %} @@ -32,4 +32,4 @@ Hello world! -{% endblock %} \ No newline at end of file +{% endblock %} From e1817038a771bb07c1baf2b734c2fb2066455769 Mon Sep 17 00:00:00 2001 From: Pete Herlihy Date: Tue, 1 Dec 2015 09:57:24 +0000 Subject: [PATCH 34/44] Removed the hello world route from the index --- app/main/views/index.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index 2c775dbe4..f5e7b50e8 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -13,11 +13,6 @@ def govuk(): return render_template('govuk_template.html') -@main.route("/hello-world") -def helloworld(): - return render_template('hello-world.html') - - @main.route("/register") def register(): return render_template('register.html') From 7f96ef5a25203f89a75f74622ebe89a76e298958 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Fri, 27 Nov 2015 09:47:29 +0000 Subject: [PATCH 35/44] 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() From 48b7a7dc373a7ce9739e16fe48bf4b1aca60a730 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Fri, 27 Nov 2015 16:25:56 +0000 Subject: [PATCH 36/44] 108536490: Adding the login manager and csrf token. Still need to figure out how to override the load_user method, currently it is not working. --- app/__init__.py | 6 ++++ app/main/encryption.py | 2 +- app/main/forms.py | 2 +- app/main/views/index.py | 8 ++++++ app/main/views/sign_in.py | 35 +++++++++++++++-------- app/models.py | 24 ++++++++++++++++ app/templates/signin.html | 4 +-- app/templates/temp-create-users.html | 33 +++++++++++++++++++++ config.py | 7 +++-- migrations/versions/10_create_users.py | 3 +- migrations/versions/20_initialise_data.py | 12 ++++---- tests/app/main/views/test_sign_in.py | 21 +++++++++++++- 12 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 app/templates/temp-create-users.html diff --git a/app/__init__.py b/app/__init__.py index 011e4cc2a..17f2828d0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,11 +4,15 @@ from flask import Flask from flask._compat import string_types from flask.ext import assets from flask.ext.sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_wtf import CsrfProtect from webassets.filter import get_filter from config import configs db = SQLAlchemy() +login_manager = LoginManager() +csrf = CsrfProtect() def create_app(config_name): @@ -18,6 +22,8 @@ def create_app(config_name): application.config.from_object(configs[config_name]) db.init_app(application) init_app(application) + csrf.init_app(application) + login_manager.init_app(application) from app.main import main as main_blueprint application.register_blueprint(main_blueprint) diff --git a/app/main/encryption.py b/app/main/encryption.py index e42a42f22..b070fe4aa 100644 --- a/app/main/encryption.py +++ b/app/main/encryption.py @@ -3,5 +3,5 @@ from flask import current_app def encrypt(value): - key = current_app.config['SECRET_KEY'] + key = current_app.config['PASS_SECRET_KEY'] return hashlib.sha256((key + value).encode('UTF-8')).hexdigest() diff --git a/app/main/forms.py b/app/main/forms.py index a2cf5d9e9..6b7119817 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -5,7 +5,7 @@ from wtforms.validators import DataRequired, Email, Length class LoginForm(Form): email_address = StringField('Email address', validators=[ - Length(255), + Length(min=5, max=255), DataRequired(message='Email cannot be empty'), Email(message='Please enter a valid email address') ]) diff --git a/app/main/views/index.py b/app/main/views/index.py index de48cb9e6..ff2c53bfa 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -1,4 +1,5 @@ from flask import render_template +from flask_login import login_required from app.main import main @@ -19,36 +20,43 @@ def helloworld(): @main.route("/register") +@login_required def register(): return render_template('register.html') @main.route("/register-from-invite") +@login_required def registerfrominvite(): return render_template('register-from-invite.html') @main.route("/verify") +@login_required def verify(): return render_template('verify.html') @main.route("/verify-mobile") +@login_required def verifymobile(): return render_template('verify-mobile.html') @main.route("/dashboard") +@login_required def dashboard(): return render_template('dashboard.html') @main.route("/add-service") +@login_required def addservice(): return render_template('add-service.html') @main.route("/two-factor") +@login_required def twofactor(): return render_template('two-factor.html') diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index e95f661bf..6ffa19144 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -1,6 +1,6 @@ from datetime import datetime -from flask import render_template, redirect, url_for, jsonify +from flask import render_template, redirect, jsonify from flask_login import login_user from app.main import main @@ -19,25 +19,36 @@ def render_sign_in(): def process_sign_in(): form = LoginForm() if form.validate_on_submit(): - user = users_dao.get_user_by_email(form.email_address) + user = users_dao.get_user_by_email(form.email_address.data) if user is None: return jsonify(authorization=False), 404 - if user.password == encrypt(form.password): + if user.password == encrypt(form.password.data): login_user(user) else: return jsonify(authorization=False), 404 - + else: + return jsonify(form.errors), 404 return redirect('/two-factor') -@main.route('/create_user', methods=(['POST'])) +@main.route('/temp-create-users', methods=(['GET'])) +def render_create_user(): + return render_template('temp-create-users.html', form=LoginForm()) + + +@main.route('/temp-create-users', 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) + if form.validate_on_submit(): + user = Users(email_address=form.email_address.data, + name=form.email_address.data, + password=form.password.data, + created_at=datetime.now(), + mobile_number='+447651234534', + role_id=1) + users_dao.insert_user(user) - return 'created' + return redirect('/sign-in') + else: + print(form.errors) + return redirect(form.errors), 400 diff --git a/app/models.py b/app/models.py index 56c65e7f9..08068b978 100644 --- a/app/models.py +++ b/app/models.py @@ -43,6 +43,30 @@ class Users(db.Model): return filter_null_value_fields(serialized) + def is_authenticated(self): + return True + + def is_active(self): + return True + + def is_locked(self): + if self.failed_login_count <= current_app.config['MAX_FAILED_LOGIN_COUNT']: + return False + else: + return True + + def is_anonymous(self): + return False + + def get_id(self): + return self.id + + @staticmethod + def load_user(user_id): + user = Users.query.filter_by(id=user_id).first() + if user.is_active(): + return user + def filter_null_value_fields(obj): return dict( diff --git a/app/templates/signin.html b/app/templates/signin.html index 7aea22245..b47f53475 100644 --- a/app/templates/signin.html +++ b/app/templates/signin.html @@ -13,7 +13,7 @@ Sign in

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

    - + {{ form.hidden_tag() }}

    {{ form.email_address(class="form-control-2-3", autocomplete="off") }}
    @@ -27,7 +27,7 @@ Sign in

    - Continue +

    diff --git a/app/templates/temp-create-users.html b/app/templates/temp-create-users.html new file mode 100644 index 000000000..e99abfed4 --- /dev/null +++ b/app/templates/temp-create-users.html @@ -0,0 +1,33 @@ +{% extends "admin_template.html" %} + +{% block page_title %} +Temp create users +{% endblock %} + +{% block content %} + +
    +
    +

    Temporary page to create user

    + +

    This is a temporary page to create users, the name will be the same as the email address.

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

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

    +

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

    + +

    + +

    +
    +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/config.py b/config.py index 5c1ecfea1..f0558df21 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,10 @@ 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' + PASS_SECRET_KEY = 'secret-key-unique-changeme' + + WTF_CSRF_ENABLED = True + SECRET_KEY = 'secret-key' class Development(Config): @@ -19,7 +22,7 @@ class Development(Config): class Test(Config): DEBUG = False SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notifications_admin' - + WTF_CSRF_ENABLED = False configs = { 'development': Development, diff --git a/migrations/versions/10_create_users.py b/migrations/versions/10_create_users.py index 4a1a0fe1d..fbd32f153 100644 --- a/migrations/versions/10_create_users.py +++ b/migrations/versions/10_create_users.py @@ -1,6 +1,6 @@ """empty message -Revision ID: create_users +Revision ID: 10_create_users Revises: None Create Date: 2015-11-24 10:39:19.827534 @@ -13,6 +13,7 @@ down_revision = None from alembic import op import sqlalchemy as sa + def upgrade(): op.create_table('roles', sa.Column('id', sa.Integer, primary_key=True), diff --git a/migrations/versions/20_initialise_data.py b/migrations/versions/20_initialise_data.py index 4784310e6..ce5726cfc 100644 --- a/migrations/versions/20_initialise_data.py +++ b/migrations/versions/20_initialise_data.py @@ -1,15 +1,13 @@ # revision identifiers, used by Alembic. revision = '20_initialise_data' -down_revision = None - +down_revision = '10_create_users' +from app.models import Roles from alembic import op def upgrade(): - op.bulk_insert('roles', - [ - {'role': 'plaform_admin'}, - {'role': 'service_user'} - ]) + op.execute("insert into roles(role) values('platform_admin')") + op.execute("insert into roles(role) values('service_user')") + def downgrade(): op.drop_table('users') diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 66e2ebaa2..90b6b1af0 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -1,3 +1,7 @@ +from datetime import datetime + +from app.main.dao import users_dao +from app.models import Users def test_render_sign_in_returns_sign_in_template(notifications_admin): @@ -9,9 +13,24 @@ def test_render_sign_in_returns_sign_in_template(notifications_admin): assert 'Forgotten password?' in response.get_data(as_text=True) -def test_process_sign_in_return_2fa_template(notifications_admin): +def test_process_sign_in_return_2fa_template(notifications_admin, notifications_admin_db): + user = Users(email_address='valid@example.gov.uk', + password='val1dPassw0rd!', + mobile_number='+441234123123', + name='valid', + created_at=datetime.now(), + role_id=1) + users_dao.insert_user(user) 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' + + +def test_temp_create_user(notifications_admin, notifications_admin_db): + response = notifications_admin.test_client().post('/temp-create-users', + data={'email_address': 'testing@example.gov.uk', + 'password': 'val1dPassw0rd!'}) + + assert response.status_code == 302 From 6f61906fd4deda9c545bd40041b9c4ebc5190776 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 11:21:51 +0000 Subject: [PATCH 37/44] 108536490: Implement LoginManager for the admin app. Also added csrf error handler, will make the session unauthorized if the csrf token is invalid. --- app/__init__.py | 26 +++++++++- app/main/dao/users_dao.py | 8 +-- app/main/views/sign_in.py | 20 +++++--- app/models.py | 4 +- tests/app/main/dao/test_users_dao.py | 74 ++++++++++++++-------------- tests/app/main/views/test_sign_in.py | 14 +++--- 6 files changed, 87 insertions(+), 59 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 17f2828d0..007b52aee 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,12 +1,13 @@ import os -from flask import Flask +from flask import Flask, session from flask._compat import string_types from flask.ext import assets from flask.ext.sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_wtf import CsrfProtect from webassets.filter import get_filter +from werkzeug.exceptions import abort from config import configs @@ -22,8 +23,10 @@ def create_app(config_name): application.config.from_object(configs[config_name]) db.init_app(application) init_app(application) - csrf.init_app(application) + init_csrf(application) + login_manager.init_app(application) + login_manager.login_view = 'main.sign_in.render_sign_in' from app.main import main as main_blueprint application.register_blueprint(main_blueprint) @@ -31,6 +34,25 @@ def create_app(config_name): return application +def init_csrf(application): + csrf.init_app(application) + + @csrf.error_handler + def csrf_handler(reason): + if 'user_id' not in session: + application.logger.info( + u'csrf.session_expired: Redirecting user to log in page' + ) + + return application.login_manager.unauthorized() + + application.logger.info( + u'csrf.invalid_token: Aborting request, user_id: {user_id}', + extra={'user_id': session['user_id']}) + + abort(400, reason) + + def init_app(app): for key, value in app.config.items(): if key in os.environ: diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index 95d68eafa..1cb7aeea2 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -1,5 +1,5 @@ from app import db -from app.models import Users +from app.models import User from app.main.encryption import encrypt @@ -10,12 +10,12 @@ def insert_user(user): def get_user_by_id(id): - return Users.query.filter_by(id=id).first() + return User.query.filter_by(id=id).first() def get_all_users(): - return Users.query.all() + return User.query.all() def get_user_by_email(email_address): - return Users.query.filter_by(email_address=email_address).first() + return User.query.filter_by(email_address=email_address).first() diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 6ffa19144..abf14e4b4 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -3,13 +3,19 @@ from datetime import datetime from flask import render_template, redirect, jsonify from flask_login import login_user +from app import login_manager 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.models import User from app.main.encryption import encrypt +@login_manager.user_loader +def load_user(user_id): + return users_dao.get_user_by_id(user_id) + + @main.route("/sign-in", methods=(['GET'])) def render_sign_in(): return render_template('signin.html', form=LoginForm()) @@ -40,12 +46,12 @@ def render_create_user(): def create_user_for_test(): form = LoginForm() if form.validate_on_submit(): - user = Users(email_address=form.email_address.data, - name=form.email_address.data, - password=form.password.data, - created_at=datetime.now(), - mobile_number='+447651234534', - role_id=1) + user = User(email_address=form.email_address.data, + name=form.email_address.data, + password=form.password.data, + created_at=datetime.now(), + mobile_number='+447651234534', + role_id=1) users_dao.insert_user(user) return redirect('/sign-in') diff --git a/app/models.py b/app/models.py index 08068b978..2ed53c257 100644 --- a/app/models.py +++ b/app/models.py @@ -12,7 +12,7 @@ class Roles(db.Model): role = db.Column(db.String, nullable=False, unique=True) -class Users(db.Model): +class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) @@ -63,7 +63,7 @@ class Users(db.Model): @staticmethod def load_user(user_id): - user = Users.query.filter_by(id=user_id).first() + user = User.query.filter_by(id=user_id).first() if user.is_active(): return user diff --git a/tests/app/main/dao/test_users_dao.py b/tests/app/main/dao/test_users_dao.py index 3f734ff87..694a0fe3b 100644 --- a/tests/app/main/dao/test_users_dao.py +++ b/tests/app/main/dao/test_users_dao.py @@ -3,17 +3,17 @@ from datetime import datetime import pytest import sqlalchemy -from app.models import Users +from app.models import User from app.main.dao import users_dao def test_insert_user_should_add_user(notifications_admin, notifications_admin_db): - user = Users(name='test insert', - password='somepassword', - email_address='test@insert.gov.uk', - mobile_number='+441234123412', - created_at=datetime.now(), - role_id=1) + user = User(name='test insert', + password='somepassword', + email_address='test@insert.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) users_dao.insert_user(user) saved_user = users_dao.get_user_by_id(user.id) @@ -21,24 +21,24 @@ def test_insert_user_should_add_user(notifications_admin, notifications_admin_db def test_insert_user_with_role_that_does_not_exist_fails(notifications_admin, notifications_admin_db): - user = Users(name='role does not exist', - password='somepassword', - email_address='test@insert.gov.uk', - mobile_number='+441234123412', - created_at=datetime.now(), - role_id=100) + user = User(name='role does not exist', + password='somepassword', + email_address='test@insert.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=100) 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) + user = User(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) @@ -46,24 +46,24 @@ def test_get_user_by_email(notifications_admin, notifications_admin_db): 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) + user1 = User(name='test one', + password='somepassword', + email_address='test1@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + user2 = User(name='test two', + password='some2ndpassword', + email_address='test2@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + user3 = User(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) diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 90b6b1af0..255c9d90c 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -1,7 +1,7 @@ from datetime import datetime from app.main.dao import users_dao -from app.models import Users +from app.models import User def test_render_sign_in_returns_sign_in_template(notifications_admin): @@ -14,12 +14,12 @@ def test_render_sign_in_returns_sign_in_template(notifications_admin): def test_process_sign_in_return_2fa_template(notifications_admin, notifications_admin_db): - user = Users(email_address='valid@example.gov.uk', - password='val1dPassw0rd!', - mobile_number='+441234123123', - name='valid', - created_at=datetime.now(), - role_id=1) + user = User(email_address='valid@example.gov.uk', + password='val1dPassw0rd!', + mobile_number='+441234123123', + name='valid', + created_at=datetime.now(), + role_id=1) users_dao.insert_user(user) response = notifications_admin.test_client().post('/sign-in', data={'email_address': 'valid@example.gov.uk', From af382885d3dad8fee12470a0a726284f15475b06 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 12:38:02 +0000 Subject: [PATCH 38/44] 108536490: Use ItsDangerousSessionInterface in the app. Start using http://flask.pocoo.org/snippets/51/ --- app/__init__.py | 2 ++ app/its_dangerous_session.py | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 app/its_dangerous_session.py diff --git a/app/__init__.py b/app/__init__.py index 007b52aee..d88b74707 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,6 +9,7 @@ from flask_wtf import CsrfProtect from webassets.filter import get_filter from werkzeug.exceptions import abort +from app.its_dangerous_session import ItsdangerousSessionInterface from config import configs db = SQLAlchemy() @@ -31,6 +32,7 @@ def create_app(config_name): from app.main import main as main_blueprint application.register_blueprint(main_blueprint) + application.session_interface = ItsdangerousSessionInterface() return application diff --git a/app/its_dangerous_session.py b/app/its_dangerous_session.py new file mode 100644 index 000000000..ab644f057 --- /dev/null +++ b/app/its_dangerous_session.py @@ -0,0 +1,50 @@ +from werkzeug.datastructures import CallbackDict +from flask.sessions import SessionInterface, SessionMixin +from itsdangerous import URLSafeTimedSerializer, BadSignature + + +class ItsdangerousSession(CallbackDict, SessionMixin): + + def __init__(self, initial=None): + def on_update(self): + self.modified = True + CallbackDict.__init__(self, initial, on_update) + self.modified = False + + +class ItsdangerousSessionInterface(SessionInterface): + salt = 'cookie-session' + session_class = ItsdangerousSession + + def get_serializer(self, app): + if not app.secret_key: + return None + return URLSafeTimedSerializer(app.secret_key, + salt=self.salt) + + def open_session(self, app, request): + s = self.get_serializer(app) + if s is None: + return None + val = request.cookies.get(app.session_cookie_name) + if not val: + return self.session_class() + max_age = app.permanent_session_lifetime.total_seconds() + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() + + def save_session(self, app, session, response): + domain = self.get_cookie_domain(app) + if not session: + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain) + return + expires = self.get_expiration_time(app, session) + val = self.get_serializer(app).dumps(dict(session)) + response.set_cookie(app.session_cookie_name, val, + expires=expires, httponly=True, + domain=domain) From 3f017b30f2a4235028346c99f6917b9ca18735ca Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 14:32:58 +0000 Subject: [PATCH 39/44] 108536490: add the proxy_fix --- .travis.yml | 2 ++ app/__init__.py | 5 +++++ app/its_dangerous_session.py | 4 ++-- app/main/dao/users_dao.py | 7 ++++++- app/main/views/sign_in.py | 12 +++--------- app/models.py | 16 +++++----------- app/proxy_fix.py | 17 +++++++++++++++++ config.py | 7 +++++++ requirements.txt | 2 +- 9 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 app/proxy_fix.py diff --git a/.travis.yml b/.travis.yml index 4c7783f29..912413eb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,3 +27,5 @@ deploy: app: notifications-admin on: repo: alphagov/notifications-admin + run: + - python app.py db upgrade diff --git a/app/__init__.py b/app/__init__.py index d88b74707..fc5c73b8e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,8 +10,10 @@ from webassets.filter import get_filter from werkzeug.exceptions import abort from app.its_dangerous_session import ItsdangerousSessionInterface +import app.proxy_fix from config import configs + db = SQLAlchemy() login_manager = LoginManager() csrf = CsrfProtect() @@ -32,7 +34,10 @@ def create_app(config_name): from app.main import main as main_blueprint application.register_blueprint(main_blueprint) + proxy_fix.init_app(application) + application.session_interface = ItsdangerousSessionInterface() + return application diff --git a/app/its_dangerous_session.py b/app/its_dangerous_session.py index ab644f057..d281b32d9 100644 --- a/app/its_dangerous_session.py +++ b/app/its_dangerous_session.py @@ -13,14 +13,14 @@ class ItsdangerousSession(CallbackDict, SessionMixin): class ItsdangerousSessionInterface(SessionInterface): - salt = 'cookie-session' session_class = ItsdangerousSession def get_serializer(self, app): + salt = app.config.get('DANGEROUS_SALT') if not app.secret_key: return None return URLSafeTimedSerializer(app.secret_key, - salt=self.salt) + salt=salt) def open_session(self, app, request): s = self.get_serializer(app) diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index 1cb7aeea2..0ebb54b6d 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -1,8 +1,13 @@ -from app import db +from app import db, login_manager from app.models import User from app.main.encryption import encrypt +@login_manager.user_loader +def load_user(user_id): + return get_user_by_id(user_id) + + def insert_user(user): user.password = encrypt(user.password) db.session.add(user) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index abf14e4b4..78011390f 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -3,7 +3,6 @@ from datetime import datetime from flask import render_template, redirect, jsonify from flask_login import login_user -from app import login_manager from app.main import main from app.main.forms import LoginForm from app.main.dao import users_dao @@ -11,11 +10,6 @@ from app.models import User from app.main.encryption import encrypt -@login_manager.user_loader -def load_user(user_id): - return users_dao.get_user_by_id(user_id) - - @main.route("/sign-in", methods=(['GET'])) def render_sign_in(): return render_template('signin.html', form=LoginForm()) @@ -27,13 +21,13 @@ def process_sign_in(): if form.validate_on_submit(): user = users_dao.get_user_by_email(form.email_address.data) if user is None: - return jsonify(authorization=False), 404 + return jsonify(authorization=False), 401 if user.password == encrypt(form.password.data): login_user(user) else: - return jsonify(authorization=False), 404 + return jsonify(authorization=False), 401 else: - return jsonify(form.errors), 404 + return jsonify(form.errors), 400 return redirect('/two-factor') diff --git a/app/models.py b/app/models.py index 2ed53c257..7767c91a8 100644 --- a/app/models.py +++ b/app/models.py @@ -49,23 +49,17 @@ class User(db.Model): def is_active(self): return True - def is_locked(self): - if self.failed_login_count <= current_app.config['MAX_FAILED_LOGIN_COUNT']: - return False - else: - return True - def is_anonymous(self): return False def get_id(self): return self.id - @staticmethod - def load_user(user_id): - user = User.query.filter_by(id=user_id).first() - if user.is_active(): - return user + def is_locked(self): + if self.failed_login_count <= current_app.config['MAX_FAILED_LOGIN_COUNT']: + return False + else: + return True def filter_null_value_fields(obj): diff --git a/app/proxy_fix.py b/app/proxy_fix.py new file mode 100644 index 000000000..a31496411 --- /dev/null +++ b/app/proxy_fix.py @@ -0,0 +1,17 @@ +from werkzeug.contrib.fixers import ProxyFix + + +class CustomProxyFix(object): + def __init__(self, app, forwarded_proto): + self.app = ProxyFix(app) + self.forwarded_proto = forwarded_proto + + def __call__(self, environ, start_response): + environ.update({ + "HTTP_X_FORWARDED_PROTO": self.forwarded_proto + }) + return self.app(environ, start_response) + + +def init_app(app): + app.wsgi_app = CustomProxyFix(app.wsgi_app, app.config.get('HTTP_PROTOCOL', 'http')) \ No newline at end of file diff --git a/config.py b/config.py index f0558df21..6c799b561 100644 --- a/config.py +++ b/config.py @@ -13,6 +13,8 @@ class Config(object): WTF_CSRF_ENABLED = True SECRET_KEY = 'secret-key' + HTTP_PROTOCOL = 'http' + DANGEROUS_SALT = 'itsdangeroussalt' class Development(Config): @@ -24,6 +26,11 @@ class Test(Config): SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notifications_admin' WTF_CSRF_ENABLED = False + +class Live(Config): + DEBUG = False + HTTP_PROTOCOL = 'https' + configs = { 'development': Development, 'test': Test diff --git a/requirements.txt b/requirements.txt index b66dabd3e..379db7cd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ Flask-Script==2.0.5 Flask-Assets==0.11 Flask-Migrate==1.3.1 Flask-SQLAlchemy==2.0 -psycopg2==2.6.1 +psycopg2==2.6.2 SQLAlchemy==1.0.5 SQLAlchemy-Utils==0.30.5 Flask-WTF==0.11 From ff9e98907e075ece02904863d5e17b3fd037b785 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 15:33:40 +0000 Subject: [PATCH 40/44] 108536490: Update encryption for password --- app/main/dao/users_dao.py | 4 ++-- app/main/encryption.py | 12 +++++++----- app/main/views/sign_in.py | 4 ++-- app/proxy_fix.py | 2 +- requirements.txt | 5 +++-- tests/app/main/test_encyption.py | 16 ++++++++++++---- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index 0ebb54b6d..59a23568c 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -1,6 +1,6 @@ from app import db, login_manager from app.models import User -from app.main.encryption import encrypt +from app.main.encryption import hashpw @login_manager.user_loader @@ -9,7 +9,7 @@ def load_user(user_id): def insert_user(user): - user.password = encrypt(user.password) + user.password = hashpw(user.password) db.session.add(user) db.session.commit() diff --git a/app/main/encryption.py b/app/main/encryption.py index b070fe4aa..27aff9e25 100644 --- a/app/main/encryption.py +++ b/app/main/encryption.py @@ -1,7 +1,9 @@ -import hashlib -from flask import current_app +from flask.ext.bcrypt import generate_password_hash, check_password_hash -def encrypt(value): - key = current_app.config['PASS_SECRET_KEY'] - return hashlib.sha256((key + value).encode('UTF-8')).hexdigest() +def hashpw(password): + return generate_password_hash(password.encode('UTF-8'), 10) + + +def checkpw(password, hashed_password): + return check_password_hash(hashed_password, password) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 78011390f..d5d3677c2 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -7,7 +7,7 @@ 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 encrypt +from app.main.encryption import checkpw @main.route("/sign-in", methods=(['GET'])) @@ -22,7 +22,7 @@ def process_sign_in(): user = users_dao.get_user_by_email(form.email_address.data) if user is None: return jsonify(authorization=False), 401 - if user.password == encrypt(form.password.data): + if checkpw(form.password.data, user.password): login_user(user) else: return jsonify(authorization=False), 401 diff --git a/app/proxy_fix.py b/app/proxy_fix.py index a31496411..a572672d7 100644 --- a/app/proxy_fix.py +++ b/app/proxy_fix.py @@ -14,4 +14,4 @@ class CustomProxyFix(object): def init_app(app): - app.wsgi_app = CustomProxyFix(app.wsgi_app, app.config.get('HTTP_PROTOCOL', 'http')) \ No newline at end of file + app.wsgi_app = CustomProxyFix(app.wsgi_app, app.config.get('HTTP_PROTOCOL', 'http')) diff --git a/requirements.txt b/requirements.txt index 379db7cd1..9ac4a3971 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,9 @@ Flask-Script==2.0.5 Flask-Assets==0.11 Flask-Migrate==1.3.1 Flask-SQLAlchemy==2.0 -psycopg2==2.6.2 +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 +Flask-Login==0.2.11 +Flask-Bcrypt==0.6.2 \ No newline at end of file diff --git a/tests/app/main/test_encyption.py b/tests/app/main/test_encyption.py index a85aa59d9..9339dd1e3 100644 --- a/tests/app/main/test_encyption.py +++ b/tests/app/main/test_encyption.py @@ -1,9 +1,17 @@ -from app.main import encryption +from app.main.encryption import hashpw, checkpw -def test_encryption(notifications_admin): +def test_should_hash_password(): + password = 'passwordToHash' + assert password != hashpw(password) + + +def test_should_check_password(): value = 's3curePassword!' + encrypted = hashpw(value) + assert checkpw(value, encrypted) is True - encrypted = encryption.encrypt(value) - assert encrypted == encryption.encrypt(value) +def test_checkpw_should_fail_when_pw_does_not_match(): + value = hashpw('somePassword') + assert checkpw('somethingDifferent', value) is False From 3b27db98ff6dac10876593aea213ba5400294f23 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 16:33:45 +0000 Subject: [PATCH 41/44] 108536490: Implement locked out function. User is locked if they fail to login 10 times or more. --- app/main/dao/users_dao.py | 6 +++++ app/main/views/sign_in.py | 3 +++ app/models.py | 2 +- tests/app/main/dao/test_users_dao.py | 34 ++++++++++++++++++++++++++++ tests/app/main/views/test_sign_in.py | 21 +++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py index 59a23568c..d4d3a0b42 100644 --- a/app/main/dao/users_dao.py +++ b/app/main/dao/users_dao.py @@ -24,3 +24,9 @@ def get_all_users(): def get_user_by_email(email_address): return User.query.filter_by(email_address=email_address).first() + + +def increment_failed_login_count(id): + user = User.query.filter_by(id=id).first() + user.failed_login_count += 1 + db.session.commit() diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index d5d3677c2..492b3225c 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -20,11 +20,14 @@ def process_sign_in(): form = LoginForm() if form.validate_on_submit(): user = users_dao.get_user_by_email(form.email_address.data) + if user.is_locked(): + return jsonify(locked_out=True), 401 if user is None: return jsonify(authorization=False), 401 if checkpw(form.password.data, user.password): login_user(user) else: + users_dao.increment_failed_login_count(user.id) return jsonify(authorization=False), 401 else: return jsonify(form.errors), 400 diff --git a/app/models.py b/app/models.py index 7767c91a8..c7a388423 100644 --- a/app/models.py +++ b/app/models.py @@ -56,7 +56,7 @@ class User(db.Model): return self.id def is_locked(self): - if self.failed_login_count <= current_app.config['MAX_FAILED_LOGIN_COUNT']: + if self.failed_login_count < current_app.config['MAX_FAILED_LOGIN_COUNT']: return False else: return True diff --git a/tests/app/main/dao/test_users_dao.py b/tests/app/main/dao/test_users_dao.py index 694a0fe3b..55b64e508 100644 --- a/tests/app/main/dao/test_users_dao.py +++ b/tests/app/main/dao/test_users_dao.py @@ -71,3 +71,37 @@ def test_get_all_users_returns_all_users(notifications_admin, notifications_admi users = users_dao.get_all_users() assert len(users) == 3 assert users == [user1, user2, user3] + + +def test_increment_failed_lockout_count_should_increade_count_by_1(notifications_admin, notifications_admin_db): + user = User(name='cannot remember password', + password='somepassword', + email_address='test1@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + users_dao.insert_user(user) + + savedUser = users_dao.get_user_by_id(user.id) + assert savedUser.failed_login_count == 0 + users_dao.increment_failed_login_count(user.id) + assert users_dao.get_user_by_id(user.id).failed_login_count == 1 + + +def test_user_is_locked_if_failed_login_count_is_10_or_greater(notifications_admin, notifications_admin_db): + user = User(name='cannot remember password', + password='somepassword', + email_address='test1@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1) + users_dao.insert_user(user) + saved_user = users_dao.get_user_by_id(user.id) + assert saved_user.is_locked() is False + + for _ in range(10): + users_dao.increment_failed_login_count(user.id) + + saved_user = users_dao.get_user_by_id(user.id) + assert saved_user.failed_login_count == 10 + assert saved_user.is_locked() is True diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 255c9d90c..1c84650c3 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -34,3 +34,24 @@ def test_temp_create_user(notifications_admin, notifications_admin_db): 'password': 'val1dPassw0rd!'}) assert response.status_code == 302 + + +def test_should_return_locked_out_true_when_user_is_locked(notifications_admin, notifications_admin_db): + user = User(email_address='valid@example.gov.uk', + password='val1dPassw0rd!', + mobile_number='+441234123123', + name='valid', + created_at=datetime.now(), + role_id=1) + users_dao.insert_user(user) + for _ in range(10): + notifications_admin.test_client().post('/sign-in', + data={'email_address': 'valid@example.gov.uk', + 'password': 'whatIsMyPassword!'}) + + response = notifications_admin.test_client().post('/sign-in', + data={'email_address': 'valid@example.gov.uk', + 'password': 'val1dPassw0rd!'}) + + assert response.status_code == 401 + assert '"locked_out": true' in response.get_data(as_text=True) From edfc1d6efc637b27d9979d0306113a6578f9ee1c Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 16:44:59 +0000 Subject: [PATCH 42/44] 108536490: Implement User.is_active() If the state of the user is inactive the user.is_active() returns false. --- app/main/views/sign_in.py | 2 ++ app/models.py | 5 ++++- tests/app/main/dao/test_users_dao.py | 14 ++++++++++++++ tests/app/main/views/test_sign_in.py | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 492b3225c..e2d37a9c9 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -22,6 +22,8 @@ def process_sign_in(): user = users_dao.get_user_by_email(form.email_address.data) if user.is_locked(): return jsonify(locked_out=True), 401 + if not user.is_active(): + return jsonify(active_user=False), 401 if user is None: return jsonify(authorization=False), 401 if checkpw(form.password.data, user.password): diff --git a/app/models.py b/app/models.py index c7a388423..21d0aca52 100644 --- a/app/models.py +++ b/app/models.py @@ -47,7 +47,10 @@ class User(db.Model): return True def is_active(self): - return True + if self.state == 'inactive': + return False + else: + return True def is_anonymous(self): return False diff --git a/tests/app/main/dao/test_users_dao.py b/tests/app/main/dao/test_users_dao.py index 55b64e508..9fde6bd9b 100644 --- a/tests/app/main/dao/test_users_dao.py +++ b/tests/app/main/dao/test_users_dao.py @@ -105,3 +105,17 @@ def test_user_is_locked_if_failed_login_count_is_10_or_greater(notifications_adm saved_user = users_dao.get_user_by_id(user.id) assert saved_user.failed_login_count == 10 assert saved_user.is_locked() is True + + +def test_user_is_active_is_false_if_state_is_inactive(notifications_admin, notifications_admin_db): + user = User(name='inactive user', + password='somepassword', + email_address='test1@get_all.gov.uk', + mobile_number='+441234123412', + created_at=datetime.now(), + role_id=1, + state='inactive') + users_dao.insert_user(user) + + saved_user = users_dao.get_user_by_id(user.id) + assert saved_user.is_active() is False diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 1c84650c3..07d48c60a 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -55,3 +55,21 @@ def test_should_return_locked_out_true_when_user_is_locked(notifications_admin, assert response.status_code == 401 assert '"locked_out": true' in response.get_data(as_text=True) + + +def test_should_return_active_user_is_false_if_user_is_inactive(notifications_admin, notifications_admin_db): + user = User(email_address='inactive_user@example.gov.uk', + password='val1dPassw0rd!', + mobile_number='+441234123123', + name='inactive user', + created_at=datetime.now(), + role_id=1, + state='inactive') + users_dao.insert_user(user) + + response = notifications_admin.test_client().post('/sign-in', + data={'email_address': 'inactive_user@example.gov.uk', + 'password': 'val1dPassw0rd!'}) + + assert response.status_code == 401 + assert '"active_user": false' in response.get_data(as_text=True) From de0efcb5085c14eb84bd43b415b3ec9e9e4d5e95 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Mon, 30 Nov 2015 16:52:28 +0000 Subject: [PATCH 43/44] 108536490: Add test that post with bad password when account is locked results in 401 --- tests/app/main/views/test_sign_in.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 07d48c60a..2f55ba8cf 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -56,6 +56,12 @@ def test_should_return_locked_out_true_when_user_is_locked(notifications_admin, assert response.status_code == 401 assert '"locked_out": true' in response.get_data(as_text=True) + another_bad_attempt = notifications_admin.test_client().post('/sign-in', + data={'email_address': 'valid@example.gov.uk', + 'password': 'whatIsMyPassword!'}) + assert another_bad_attempt.status_code == 401 + assert '"locked_out": true' in response.get_data(as_text=True) + def test_should_return_active_user_is_false_if_user_is_inactive(notifications_admin, notifications_admin_db): user = User(email_address='inactive_user@example.gov.uk', From e8d2a81597cc726dca1cd60b86c22351e6385cf5 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Tue, 1 Dec 2015 10:35:49 +0000 Subject: [PATCH 44/44] 108536490: Fix bug when user does not exist and tries to sign in --- app/main/views/sign_in.py | 4 ++-- tests/app/main/views/test_sign_in.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index e2d37a9c9..b48487724 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -20,12 +20,12 @@ def process_sign_in(): form = LoginForm() if form.validate_on_submit(): user = users_dao.get_user_by_email(form.email_address.data) + if user is None: + return jsonify(authorization=False), 401 if user.is_locked(): return jsonify(locked_out=True), 401 if not user.is_active(): return jsonify(active_user=False), 401 - if user is None: - return jsonify(authorization=False), 401 if checkpw(form.password.data, user.password): login_user(user) else: diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 2f55ba8cf..da181cd8a 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -79,3 +79,11 @@ def test_should_return_active_user_is_false_if_user_is_inactive(notifications_ad assert response.status_code == 401 assert '"active_user": false' in response.get_data(as_text=True) + + +def test_should_return_401_when_user_does_not_exist(notifications_admin, notifications_admin_db): + response = notifications_admin.test_client().post('/sign-in', + data={'email_address': 'does_not_exist@gov.uk', + 'password': 'doesNotExist!'}) + + assert response.status_code == 401