diff --git a/README.md b/README.md index 984842c8e..0f963d9c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Build Status](https://api.travis-ci.org/alphagov/notifications-admin.svg?branch=master)](https://api.travis-ci.org/alphagov/notifications-admin.svg?branch=master) +[![Build Status](https://travis-ci.org/alphagov/notifications-admin.svg)](https://travis-ci.org/alphagov/notifications-admin) +[![Requirements Status](https://requires.io/github/alphagov/notifications-admin/requirements.svg?branch=master)](https://requires.io/github/alphagov/notifications-admin/requirements/?branch=master) # notifications-admin diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss index a801b2ec7..70555d2d1 100644 --- a/app/assets/stylesheets/components/banner.scss +++ b/app/assets/stylesheets/components/banner.scss @@ -1,14 +1,22 @@ +%banner, .banner { @include core-19; background: $turquoise; color: $white; display: block; - padding: $gutter-half $gutter; + padding: $gutter-half; margin: 0 0 $gutter 0; text-align: center; position: relative; +} + +.banner-with-tick { + + @extend %banner; + padding: $gutter-half $gutter; + &:before { @include core-24; content: '✔'; diff --git a/app/assets/stylesheets/components/sms-message.scss b/app/assets/stylesheets/components/sms-message.scss index 4fa727388..abdd5f9bf 100644 --- a/app/assets/stylesheets/components/sms-message.scss +++ b/app/assets/stylesheets/components/sms-message.scss @@ -45,27 +45,4 @@ margin: -$gutter-half 0 $gutter 0; } - &-history { - - background: $turquoise; - color: $white; - padding: $gutter-half; - @include bold-19; - margin: 0 0 $gutter 0; - - &-heading { - - @include bold-19; - margin: 0; - - &-time { - @include inline-block; - margin-left: 10px; - font-weight: normal; - } - - } - - } - } diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 7a0a95feb..bcd47c542 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -22,6 +22,7 @@ @import '../govuk_elements/public/sass/elements/forms'; @import '../govuk_elements/public/sass/elements/forms/form-validation'; @import '../govuk_elements/public/sass/elements/forms/form-block-labels'; +@import '../govuk_elements/public/sass/elements/forms/form-validation'; @import '../govuk_elements/public/sass/elements/icons'; @import '../govuk_elements/public/sass/elements/layout'; @import '../govuk_elements/public/sass/elements/lists'; diff --git a/app/main/__init__.py b/app/main/__init__.py index 3d3ab5cde..578ea5e92 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -4,5 +4,5 @@ main = Blueprint('main', __name__) from app.main.views import ( index, sign_in, sign_out, register, two_factor, verify, sms, add_service, - code_not_received, jobs, dashboard, templates, service_settings, forgot_password, new_password + code_not_received, jobs, dashboard, templates, service_settings, forgot_password, new_password, styleguide ) diff --git a/app/main/forms.py b/app/main/forms.py index b0438e942..e4559a164 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1,5 +1,12 @@ from flask_wtf import Form -from wtforms import StringField, PasswordField, ValidationError, FileField + +from wtforms import ( + StringField, + PasswordField, + ValidationError, + TextAreaField, + FileField +) from wtforms.validators import DataRequired, Email, Length, Regexp from app.main.validators import Blacklist, ValidateUserCodes, CsvFileValidator @@ -124,6 +131,19 @@ class AddServiceForm(Form): raise ValidationError('Service name already exists') +class ServiceNameForm(Form): + service_name = StringField(u'New name') + + +class ConfirmPasswordForm(Form): + password = PasswordField(u'Enter password') + + +class TemplateForm(Form): + template_name = StringField(u'Template name') + template_body = TextAreaField(u'Message') + + class ForgotPasswordForm(Form): email_address = email_address() @@ -133,6 +153,5 @@ class NewPasswordForm(Form): class CsvUploadForm(Form): - file = FileField('File to upload', - validators=[DataRequired(message='Please pick a file'), - CsvFileValidator()]) + file = FileField('File to upload', validators=[DataRequired( + message='Please pick a file'), CsvFileValidator()]) diff --git a/app/main/notifications/__init__.py b/app/main/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/main/views/__init__.py b/app/main/views/__init__.py index a76b4feb4..e69de29bb 100644 --- a/app/main/views/__init__.py +++ b/app/main/views/__init__.py @@ -1,54 +0,0 @@ -from random import randint - -from flask import url_for, current_app - -from app import admin_api_client -from app.main.dao import verify_codes_dao - - -def create_verify_code(): - return ''.join(["%s" % randint(0, 9) for _ in range(0, 5)]) - - -def send_sms_code(user_id, mobile_number): - sms_code = create_verify_code() - verify_codes_dao.add_code(user_id=user_id, code=sms_code, code_type='sms') - admin_api_client.send_sms(mobile_number=mobile_number, message=sms_code, token=admin_api_client.auth_token) - - return sms_code - - -def send_email_code(user_id, email): - email_code = create_verify_code() - verify_codes_dao.add_code(user_id=user_id, code=email_code, code_type='email') - admin_api_client.send_email(email_address=email, - from_str='notify@digital.cabinet-office.gov.uk', - message=email_code, - subject='Verification code', - token=admin_api_client.auth_token) - return email_code - - -def send_change_password_email(email): - link_to_change_password = url_for('.new_password', token=generate_token(email), _external=True) - admin_api_client.send_email(email_address=email, - from_str='notify@digital.cabinet-office.gov.uk', - message=link_to_change_password, - subject='Reset password for GOV.UK Notify', - token=admin_api_client.auth_token) - - -def generate_token(email): - from itsdangerous import TimestampSigner - signer = TimestampSigner(current_app.config['SECRET_KEY']) - return signer.sign(email).decode('utf8') - - -def check_token(token): - from itsdangerous import TimestampSigner, SignatureExpired - signer = TimestampSigner(current_app.config['SECRET_KEY']) - try: - email = signer.unsign(token, max_age=current_app.config['TOKEN_MAX_AGE_SECONDS']) - return email - except SignatureExpired as e: - current_app.logger.info('token expired %s' % e) diff --git a/app/main/views/code_not_received.py b/app/main/views/code_not_received.py index 4f0962653..56d68025e 100644 --- a/app/main/views/code_not_received.py +++ b/app/main/views/code_not_received.py @@ -1,10 +1,10 @@ from flask import ( - render_template, redirect, jsonify, session, url_for) + render_template, redirect, session, url_for) from app.main import main from app.main.dao import users_dao from app.main.forms import EmailNotReceivedForm, TextNotReceivedForm -from app.main.views import send_sms_code, send_email_code +from app.notify_client.sender import send_sms_code, send_email_code @main.route('/email-not-received', methods=['GET', 'POST']) diff --git a/app/main/views/forgot_password.py b/app/main/views/forgot_password.py index 0e5c81bda..21d7d8f1c 100644 --- a/app/main/views/forgot_password.py +++ b/app/main/views/forgot_password.py @@ -1,8 +1,8 @@ -from flask import render_template, flash, current_app +from flask import render_template, current_app from app.main import main from app.main.dao import users_dao from app.main.forms import ForgotPasswordForm -from app.main.views import send_change_password_email +from app.notify_client.sender import send_change_password_email @main.route('/forgot-password', methods=['GET', 'POST']) diff --git a/app/main/views/new_password.py b/app/main/views/new_password.py index c1b0e1e04..7a46f5c18 100644 --- a/app/main/views/new_password.py +++ b/app/main/views/new_password.py @@ -3,7 +3,7 @@ from flask import (render_template, url_for, redirect, flash) from app.main import main from app.main.dao import users_dao from app.main.forms import NewPasswordForm -from app.main.views import send_sms_code, check_token +from app.notify_client.sender import check_token, send_sms_code @main.route('/new-password/', methods=['GET', 'POST']) @@ -13,7 +13,7 @@ def new_password(token): flash('The link in the email we sent you has expired. Enter your email address to resend.') return redirect(url_for('.forgot_password')) - user = users_dao.get_user_by_email(email_address=email_address.decode('utf-8')) + user = users_dao.get_user_by_email(email_address=email_address) if user and user.state != 'request_password_reset': flash('The link in the email we sent you has already been used.') return redirect(url_for('.index')) diff --git a/app/main/views/register.py b/app/main/views/register.py index 9eef37a4a..2ca7d8faa 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -5,12 +5,14 @@ from flask import render_template, redirect, session from app.main import main from app.main.dao import users_dao from app.main.forms import RegisterUserForm -from app.main.views import send_sms_code, send_email_code from app.models import User # TODO how do we handle duplicate unverifed email addresses? # malicious or otherwise. +from app.notify_client.sender import send_sms_code, send_email_code + + @main.route('/register', methods=['GET', 'POST']) def register(): form = RegisterUserForm(users_dao.get_user_by_email) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 6553a876e..b031fd447 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -2,6 +2,7 @@ from flask import render_template, redirect, request, url_for, abort from flask_login import login_required from app.main import main +from app.main.forms import ConfirmPasswordForm, ServiceNameForm service = { 'name': 'Service name', @@ -20,10 +21,15 @@ def service_settings(): @main.route("/service-settings/name", methods=['GET', 'POST']) def name(): + + form = ServiceNameForm() + form.service_name.data = 'Service name' + if request.method == 'GET': return render_template( 'views/service-settings/name.html', - service=service + service=service, + form=form ) elif request.method == 'POST': return redirect(url_for('.confirm_name_change')) @@ -31,10 +37,14 @@ def name(): @main.route("/service-settings/name/confirm", methods=['GET', 'POST']) def confirm_name_change(): + + form = ConfirmPasswordForm() + if request.method == 'GET': return render_template( 'views/service-settings/confirm.html', - heading='Change your service name' + heading='Change your service name', + form=form ) elif request.method == 'POST': return redirect(url_for('.service_settings')) @@ -64,11 +74,15 @@ def status(): @main.route("/service-settings/status/confirm", methods=['GET', 'POST']) def confirm_status_change(): + + form = ConfirmPasswordForm() + if request.method == 'GET': return render_template( 'views/service-settings/confirm.html', heading='Turn off all outgoing notifications', - destructive=True + destructive=True, + form=form ) elif request.method == 'POST': return redirect(url_for('.service_settings')) @@ -87,11 +101,15 @@ def delete(): @main.route("/service-settings/delete/confirm", methods=['GET', 'POST']) def confirm_delete(): + + form = ConfirmPasswordForm() + if request.method == 'GET': return render_template( 'views/service-settings/confirm.html', heading='Delete this service from Notify', - destructive=True + destructive=True, + form=form ) elif request.method == 'POST': return redirect(url_for('.dashboard')) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 0f3be0c2d..7c0e28a26 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -1,12 +1,12 @@ from flask import ( - render_template, redirect, jsonify, url_for) + render_template, redirect, url_for) from flask import session from app.main import main from app.main.dao import users_dao from app.main.encryption import check_hash from app.main.forms import LoginForm -from app.main.views import send_sms_code +from app.notify_client.sender import send_sms_code @main.route('/sign-in', methods=(['GET', 'POST'])) diff --git a/app/main/views/styleguide.py b/app/main/views/styleguide.py new file mode 100644 index 000000000..f778f9dcf --- /dev/null +++ b/app/main/views/styleguide.py @@ -0,0 +1,24 @@ +from flask import render_template +from flask_wtf import Form +from wtforms import StringField, PasswordField, TextAreaField, validators +from app.main import main + + +@main.route('/_styleguide') +def styleguide(): + + class FormExamples(Form): + username = StringField(u'Username') + password = PasswordField(u'Password', [validators.required()]) + message = TextAreaField(u'Message') + + form = FormExamples() + + form.message.data = "Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax" # noqa + + form.validate() + + return render_template( + 'views/styleguide.html', + form=form + ) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index ff80cb353..2d7d38e85 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -1,6 +1,7 @@ from flask import request, render_template, redirect, url_for from app.main import main +from app.main.forms import TemplateForm @main.route("/templates") @@ -10,12 +11,17 @@ def manage_templates(): @main.route("/templates/template", methods=['GET', 'POST']) def add_template(): + + form = TemplateForm() + + form.template_name.data = 'Reminder' + form.template_body.data = 'Vehicle tax: Your vehicle tax for ((registration number)) expires on ((date)). Tax your vehicle at www.gov.uk/vehicle-tax' # noqa + if request.method == 'GET': return render_template( 'views/edit-template.html', - template_name='Reminder', - template_body='Vehicle tax: Your vehicle tax for ((registration number)) expires on ((date)). Tax your vehicle at www.gov.uk/vehicle-tax', # noqa - h1='Edit template' + h1='Edit template', + form=form ) elif request.method == 'POST': return redirect(url_for('.manage_templates')) @@ -23,10 +29,14 @@ def add_template(): @main.route("/templates/template/add", methods=['GET', 'POST']) def edit_template(): + + form = TemplateForm() + if request.method == 'GET': return render_template( 'views/edit-template.html', - h1='Add template' + h1='Add template', + form=form ) elif request.method == 'POST': return redirect(url_for('.manage_templates')) diff --git a/app/notify_client/sender.py b/app/notify_client/sender.py new file mode 100644 index 000000000..1a509a820 --- /dev/null +++ b/app/notify_client/sender.py @@ -0,0 +1,52 @@ +from random import randint +from flask import url_for, current_app +from itsdangerous import URLSafeTimedSerializer, SignatureExpired +from app import admin_api_client +from app.main.dao import verify_codes_dao + + +def create_verify_code(): + return ''.join(["%s" % randint(0, 9) for _ in range(0, 5)]) + + +def send_sms_code(user_id, mobile_number): + sms_code = create_verify_code() + verify_codes_dao.add_code(user_id=user_id, code=sms_code, code_type='sms') + admin_api_client.send_sms(mobile_number=mobile_number, message=sms_code, token=admin_api_client.auth_token) + + return sms_code + + +def send_email_code(user_id, email): + email_code = create_verify_code() + verify_codes_dao.add_code(user_id=user_id, code=email_code, code_type='email') + admin_api_client.send_email(email_address=email, + from_str='notify@digital.cabinet-office.gov.uk', + message=email_code, + subject='Verification code', + token=admin_api_client.auth_token) + return email_code + + +def send_change_password_email(email): + link_to_change_password = url_for('.new_password', token=generate_token(email), _external=True) + admin_api_client.send_email(email_address=email, + from_str='notify@digital.cabinet-office.gov.uk', + message=link_to_change_password, + subject='Reset password for GOV.UK Notify', + token=admin_api_client.auth_token) + + +def generate_token(email): + ser = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) + return ser.dumps(email, current_app.config.get('DANGEROUS_SALT')) + + +def check_token(token): + ser = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) + try: + email = ser.loads(token, max_age=current_app.config['TOKEN_MAX_AGE_SECONDS'], + salt=current_app.config.get('DANGEROUS_SALT')) + return email + except SignatureExpired as e: + current_app.logger.info('token expired %s' % e) diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index a03e47612..c127da3f7 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -1,4 +1,3 @@ -{%- from "components/form-field.html" import render_field %} {% extends "govuk_template.html" %} {% block head %} diff --git a/app/templates/components/banner.html b/app/templates/components/banner.html index 2dab12c5d..a03d2cb0a 100644 --- a/app/templates/components/banner.html +++ b/app/templates/components/banner.html @@ -1,3 +1,5 @@ -{% macro banner(body) %} - +{% macro banner(body, with_tick=False) %} +
+ {{ body }} +
{% endmacro %} diff --git a/app/templates/components/form-field.html b/app/templates/components/form-field.html deleted file mode 100644 index 691b4c579..000000000 --- a/app/templates/components/form-field.html +++ /dev/null @@ -1,12 +0,0 @@ -{% macro render_field(field) %} -
{{ field.label }} -
{{ field(**kwargs)|safe }} - {% if field.errors %} - - {% endif %} -
-{% endmacro %} diff --git a/app/templates/components/sms-message.html b/app/templates/components/sms-message.html index 38d30b877..d0d0f92ce 100644 --- a/app/templates/components/sms-message.html +++ b/app/templates/components/sms-message.html @@ -10,11 +10,3 @@

{% endif %} {% endmacro %} - -{% macro message_status(status, time) %} -
-

- {{ status }} {{ time }} -

-
-{% endmacro %} diff --git a/app/templates/components/textbox.html b/app/templates/components/textbox.html index 7a74f0985..918fc33d7 100644 --- a/app/templates/components/textbox.html +++ b/app/templates/components/textbox.html @@ -1,15 +1,21 @@ -{% macro textbox(name, label, value='', small=True, highlight_tags=False, password=False) %} -
- - {% if small %} - - {% else %} - - {% endif %} +{% macro textbox(field, hint=False, highlight_tags=False) %} +
+ + {{ field(**{ + 'class': 'form-control textbox-highlight-textbox' if highlight_tags else 'form-control', + 'data-module': 'highlight-tags' if highlight_tags else '' + }) }}
{% endmacro %} diff --git a/app/templates/views/add-service.html b/app/templates/views/add-service.html index 2dff9d9a2..1e5784b58 100644 --- a/app/templates/views/add-service.html +++ b/app/templates/views/add-service.html @@ -1,5 +1,5 @@ {% extends "admin_template.html" %} -{% from "components/form-field.html" import render_field %} +{% from "components/textbox.html" import textbox %} {% block page_title %} GOV.UK Notify | Set up service @@ -17,9 +17,8 @@ GOV.UK Notify | Set up service
  • as your email sender name
  • -
    - {{ form.hidden_tag() }} - {{ render_field(form.service_name, class='form-control-2-3') }} + + {{ textbox(form.service_name) }}

    diff --git a/app/templates/views/edit-template.html b/app/templates/views/edit-template.html index 46615c78e..30ab1ba95 100644 --- a/app/templates/views/edit-template.html +++ b/app/templates/views/edit-template.html @@ -11,8 +11,8 @@ GOV.UK Notify | Edit template

    {{ h1 }}

    - {{ textbox(name='template_name', label='Template name', value=template_name) }} - {{ textbox(name='template_body', label='Message', small=False, value=template_body, highlight_tags=True) }} + {{ textbox(form.template_name) }} + {{ textbox(form.template_body, highlight_tags=True) }} {{ page_footer( 'Save and continue', back_link=url_for('.dashboard'), diff --git a/app/templates/views/email-not-received.html b/app/templates/views/email-not-received.html index 9f7960383..0ef40ffce 100644 --- a/app/templates/views/email-not-received.html +++ b/app/templates/views/email-not-received.html @@ -1,4 +1,5 @@ {% extends "admin_template.html" %} +{% from "components/textbox.html" import textbox %} {% from "components/page-footer.html" import page_footer %} {% block page_title %} @@ -14,13 +15,10 @@ GOV.UK Notify

    Check your email address is correct and then resend the confirmation code.

    - - {{ form.hidden_tag() }} - {{ render_field(form.email_address, class='form-control-2-3') }} - Your email address must end in .gov.uk -

    -

    - {{ page_footer('Resend confirmation code') }} + + {{ textbox(form.email_address) }} + Your email address must end in .gov.uk + {{ page_footer('Resend confirmation code') }}
    diff --git a/app/templates/views/forgot-password.html b/app/templates/views/forgot-password.html index 4d36f2a86..c4062e6b9 100644 --- a/app/templates/views/forgot-password.html +++ b/app/templates/views/forgot-password.html @@ -1,4 +1,6 @@ {% extends "admin_template.html" %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} {% block page_title %} GOV.UK Notify @@ -12,13 +14,11 @@ GOV.UK Notify

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

    -
    - {{ form.hidden_tag() }} - {{ render_field(form.email_address, class='form-control-2-3') }} -

    - -

    + + {{ textbox(form.email_address) }} + {{ page_footer("Send email") }}
    + diff --git a/app/templates/views/job.html b/app/templates/views/job.html index 29ae57ba7..b733a5e61 100644 --- a/app/templates/views/job.html +++ b/app/templates/views/job.html @@ -14,7 +14,7 @@ GOV.UK Notify | Notifications activity

    - {{ banner(flash_message) }} + {{ banner(flash_message, with_tick=True) }}