resolve merge conflicts

This commit is contained in:
Adam Shimali
2016-01-12 10:43:23 +00:00
45 changed files with 448 additions and 231 deletions

View File

@@ -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 # notifications-admin

View File

@@ -1,14 +1,22 @@
%banner,
.banner { .banner {
@include core-19; @include core-19;
background: $turquoise; background: $turquoise;
color: $white; color: $white;
display: block; display: block;
padding: $gutter-half $gutter; padding: $gutter-half;
margin: 0 0 $gutter 0; margin: 0 0 $gutter 0;
text-align: center; text-align: center;
position: relative; position: relative;
}
.banner-with-tick {
@extend %banner;
padding: $gutter-half $gutter;
&:before { &:before {
@include core-24; @include core-24;
content: ''; content: '';

View File

@@ -45,27 +45,4 @@
margin: -$gutter-half 0 $gutter 0; 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;
}
}
}
} }

View File

@@ -22,6 +22,7 @@
@import '../govuk_elements/public/sass/elements/forms'; @import '../govuk_elements/public/sass/elements/forms';
@import '../govuk_elements/public/sass/elements/forms/form-validation'; @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-block-labels';
@import '../govuk_elements/public/sass/elements/forms/form-validation';
@import '../govuk_elements/public/sass/elements/icons'; @import '../govuk_elements/public/sass/elements/icons';
@import '../govuk_elements/public/sass/elements/layout'; @import '../govuk_elements/public/sass/elements/layout';
@import '../govuk_elements/public/sass/elements/lists'; @import '../govuk_elements/public/sass/elements/lists';

View File

@@ -4,5 +4,5 @@ main = Blueprint('main', __name__)
from app.main.views import ( from app.main.views import (
index, sign_in, sign_out, register, two_factor, verify, sms, add_service, 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
) )

View File

@@ -1,5 +1,12 @@
from flask_wtf import Form 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 wtforms.validators import DataRequired, Email, Length, Regexp
from app.main.validators import Blacklist, ValidateUserCodes, CsvFileValidator from app.main.validators import Blacklist, ValidateUserCodes, CsvFileValidator
@@ -124,6 +131,19 @@ class AddServiceForm(Form):
raise ValidationError('Service name already exists') 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): class ForgotPasswordForm(Form):
email_address = email_address() email_address = email_address()
@@ -133,6 +153,5 @@ class NewPasswordForm(Form):
class CsvUploadForm(Form): class CsvUploadForm(Form):
file = FileField('File to upload', file = FileField('File to upload', validators=[DataRequired(
validators=[DataRequired(message='Please pick a file'), message='Please pick a file'), CsvFileValidator()])
CsvFileValidator()])

View File

View File

@@ -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)

View File

@@ -1,10 +1,10 @@
from flask import ( from flask import (
render_template, redirect, jsonify, session, url_for) render_template, redirect, session, url_for)
from app.main import main from app.main import main
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.forms import EmailNotReceivedForm, TextNotReceivedForm 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']) @main.route('/email-not-received', methods=['GET', 'POST'])

View File

@@ -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 import main
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.forms import ForgotPasswordForm 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']) @main.route('/forgot-password', methods=['GET', 'POST'])

View File

@@ -3,7 +3,7 @@ from flask import (render_template, url_for, redirect, flash)
from app.main import main from app.main import main
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.forms import NewPasswordForm 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/<path:token>', methods=['GET', 'POST']) @main.route('/new-password/<path:token>', 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.') flash('The link in the email we sent you has expired. Enter your email address to resend.')
return redirect(url_for('.forgot_password')) 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': if user and user.state != 'request_password_reset':
flash('The link in the email we sent you has already been used.') flash('The link in the email we sent you has already been used.')
return redirect(url_for('.index')) return redirect(url_for('.index'))

View File

@@ -5,12 +5,14 @@ from flask import render_template, redirect, session
from app.main import main from app.main import main
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.forms import RegisterUserForm from app.main.forms import RegisterUserForm
from app.main.views import send_sms_code, send_email_code
from app.models import User from app.models import User
# TODO how do we handle duplicate unverifed email addresses? # TODO how do we handle duplicate unverifed email addresses?
# malicious or otherwise. # malicious or otherwise.
from app.notify_client.sender import send_sms_code, send_email_code
@main.route('/register', methods=['GET', 'POST']) @main.route('/register', methods=['GET', 'POST'])
def register(): def register():
form = RegisterUserForm(users_dao.get_user_by_email) form = RegisterUserForm(users_dao.get_user_by_email)

View File

@@ -2,6 +2,7 @@ from flask import render_template, redirect, request, url_for, abort
from flask_login import login_required from flask_login import login_required
from app.main import main from app.main import main
from app.main.forms import ConfirmPasswordForm, ServiceNameForm
service = { service = {
'name': 'Service name', 'name': 'Service name',
@@ -20,10 +21,15 @@ def service_settings():
@main.route("/service-settings/name", methods=['GET', 'POST']) @main.route("/service-settings/name", methods=['GET', 'POST'])
def name(): def name():
form = ServiceNameForm()
form.service_name.data = 'Service name'
if request.method == 'GET': if request.method == 'GET':
return render_template( return render_template(
'views/service-settings/name.html', 'views/service-settings/name.html',
service=service service=service,
form=form
) )
elif request.method == 'POST': elif request.method == 'POST':
return redirect(url_for('.confirm_name_change')) return redirect(url_for('.confirm_name_change'))
@@ -31,10 +37,14 @@ def name():
@main.route("/service-settings/name/confirm", methods=['GET', 'POST']) @main.route("/service-settings/name/confirm", methods=['GET', 'POST'])
def confirm_name_change(): def confirm_name_change():
form = ConfirmPasswordForm()
if request.method == 'GET': if request.method == 'GET':
return render_template( return render_template(
'views/service-settings/confirm.html', 'views/service-settings/confirm.html',
heading='Change your service name' heading='Change your service name',
form=form
) )
elif request.method == 'POST': elif request.method == 'POST':
return redirect(url_for('.service_settings')) return redirect(url_for('.service_settings'))
@@ -64,11 +74,15 @@ def status():
@main.route("/service-settings/status/confirm", methods=['GET', 'POST']) @main.route("/service-settings/status/confirm", methods=['GET', 'POST'])
def confirm_status_change(): def confirm_status_change():
form = ConfirmPasswordForm()
if request.method == 'GET': if request.method == 'GET':
return render_template( return render_template(
'views/service-settings/confirm.html', 'views/service-settings/confirm.html',
heading='Turn off all outgoing notifications', heading='Turn off all outgoing notifications',
destructive=True destructive=True,
form=form
) )
elif request.method == 'POST': elif request.method == 'POST':
return redirect(url_for('.service_settings')) return redirect(url_for('.service_settings'))
@@ -87,11 +101,15 @@ def delete():
@main.route("/service-settings/delete/confirm", methods=['GET', 'POST']) @main.route("/service-settings/delete/confirm", methods=['GET', 'POST'])
def confirm_delete(): def confirm_delete():
form = ConfirmPasswordForm()
if request.method == 'GET': if request.method == 'GET':
return render_template( return render_template(
'views/service-settings/confirm.html', 'views/service-settings/confirm.html',
heading='Delete this service from Notify', heading='Delete this service from Notify',
destructive=True destructive=True,
form=form
) )
elif request.method == 'POST': elif request.method == 'POST':
return redirect(url_for('.dashboard')) return redirect(url_for('.dashboard'))

View File

@@ -1,12 +1,12 @@
from flask import ( from flask import (
render_template, redirect, jsonify, url_for) render_template, redirect, url_for)
from flask import session from flask import session
from app.main import main from app.main import main
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.encryption import check_hash from app.main.encryption import check_hash
from app.main.forms import LoginForm 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'])) @main.route('/sign-in', methods=(['GET', 'POST']))

View File

@@ -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
)

View File

@@ -1,6 +1,7 @@
from flask import request, render_template, redirect, url_for from flask import request, render_template, redirect, url_for
from app.main import main from app.main import main
from app.main.forms import TemplateForm
@main.route("/templates") @main.route("/templates")
@@ -10,12 +11,17 @@ def manage_templates():
@main.route("/templates/template", methods=['GET', 'POST']) @main.route("/templates/template", methods=['GET', 'POST'])
def add_template(): 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': if request.method == 'GET':
return render_template( return render_template(
'views/edit-template.html', 'views/edit-template.html',
template_name='Reminder', h1='Edit template',
template_body='Vehicle tax: Your vehicle tax for ((registration number)) expires on ((date)). Tax your vehicle at www.gov.uk/vehicle-tax', # noqa form=form
h1='Edit template'
) )
elif request.method == 'POST': elif request.method == 'POST':
return redirect(url_for('.manage_templates')) return redirect(url_for('.manage_templates'))
@@ -23,10 +29,14 @@ def add_template():
@main.route("/templates/template/add", methods=['GET', 'POST']) @main.route("/templates/template/add", methods=['GET', 'POST'])
def edit_template(): def edit_template():
form = TemplateForm()
if request.method == 'GET': if request.method == 'GET':
return render_template( return render_template(
'views/edit-template.html', 'views/edit-template.html',
h1='Add template' h1='Add template',
form=form
) )
elif request.method == 'POST': elif request.method == 'POST':
return redirect(url_for('.manage_templates')) return redirect(url_for('.manage_templates'))

View File

@@ -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)

View File

@@ -1,4 +1,3 @@
{%- from "components/form-field.html" import render_field %}
{% extends "govuk_template.html" %} {% extends "govuk_template.html" %}
{% block head %} {% block head %}

View File

@@ -1,3 +1,5 @@
{% macro banner(body) %} {% macro banner(body, with_tick=False) %}
<div class='banner'>{{ body }}</div> <div class='banner{% if with_tick %}-with-tick{% endif %}'>
{{ body }}
</div>
{% endmacro %} {% endmacro %}

View File

@@ -1,12 +0,0 @@
{% macro render_field(field) %}
<dt>{{ field.label }}
<dd>{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=error>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</dd>
{% endmacro %}

View File

@@ -10,11 +10,3 @@
</p> </p>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro message_status(status, time) %}
<div class="sms-message-history">
<p class="sms-message-history-heading">
{{ status }} <span class="sms-message-history-heading-time">{{ time }}</span>
</p>
</div>
{% endmacro %}

View File

@@ -1,15 +1,21 @@
{% macro textbox(name, label, value='', small=True, highlight_tags=False, password=False) %} {% macro textbox(field, hint=False, highlight_tags=False) %}
<div class="form-group"> <div class="form-group{% if field.errors %} error{% endif %}">
<label class="form-label" for="{{ name }}">{{ label }}</label> <label class="form-label" for="{{ field.name }}">
{% if small %} {{ field.label }}
<input class="form-control" id="{{ name }}" name="{{ name }}" type="{{ 'password' if password else 'text' }}" value="{{ value }}"> {% if hint %}
{% else %} <span class="form-hint">
<textarea {{ hint }}
class="form-control {% if highlight_tags %}textbox-highlight-textbox{% endif %}" </span>
id="{{ name }}" name="{{ name }}" {% endif %}
cols="30" rows="10" {% if field.errors %}
{% if highlight_tags %}data-module='highlight-tags'{% endif %} <span class="error-message">
>{{ value }}</textarea> {{ field.errors[0] }}
{% endif %} </span>
{% endif %}
</label>
{{ field(**{
'class': 'form-control textbox-highlight-textbox' if highlight_tags else 'form-control',
'data-module': 'highlight-tags' if highlight_tags else ''
}) }}
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -1,5 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/form-field.html" import render_field %} {% from "components/textbox.html" import textbox %}
{% block page_title %} {% block page_title %}
GOV.UK Notify | Set up service GOV.UK Notify | Set up service
@@ -17,9 +17,8 @@ GOV.UK Notify | Set up service
<li>as your email sender name</li> <li>as your email sender name</li>
</ul> </ul>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.service_name) }}
{{ render_field(form.service_name, class='form-control-2-3') }}
<p> <p>
<button class="button" href="dashboard" role="button">Continue</button> <button class="button" href="dashboard" role="button">Continue</button>

View File

@@ -11,8 +11,8 @@ GOV.UK Notify | Edit template
<h1 class="heading-xlarge">{{ h1 }}</h1> <h1 class="heading-xlarge">{{ h1 }}</h1>
<form method="post"> <form method="post">
{{ textbox(name='template_name', label='Template name', value=template_name) }} {{ textbox(form.template_name) }}
{{ textbox(name='template_body', label='Message', small=False, value=template_body, highlight_tags=True) }} {{ textbox(form.template_body, highlight_tags=True) }}
{{ page_footer( {{ page_footer(
'Save and continue', 'Save and continue',
back_link=url_for('.dashboard'), back_link=url_for('.dashboard'),

View File

@@ -1,4 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -14,13 +15,10 @@ GOV.UK Notify
<p>Check your email address is correct and then resend the confirmation code.</p> <p>Check your email address is correct and then resend the confirmation code.</p>
<p> <p>
</p> </p>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.email_address) }}
{{ render_field(form.email_address, class='form-control-2-3') }} <span class="font-xsmall">Your email address must end in .gov.uk</span>
<span class="font-xsmall">Your email address must end in .gov.uk</span> {{ page_footer('Resend confirmation code') }}
<p>
</p>
{{ page_footer('Resend confirmation code') }}
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,6 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
GOV.UK Notify GOV.UK Notify
@@ -12,13 +14,11 @@ GOV.UK Notify
<p>If you have forgotten your password, we can send you an email to create a new password.</p> <p>If you have forgotten your password, we can send you an email to create a new password.</p>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.email_address) }}
{{ render_field(form.email_address, class='form-control-2-3') }} {{ page_footer("Send email") }}
<p>
<button class="button" role="button">Send email</button>
</p>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@ GOV.UK Notify | Notifications activity
</h1> </h1>
<p> <p>
{{ banner(flash_message) }} {{ banner(flash_message, with_tick=True) }}
</p> </p>
<ul class="grid-row job-totals"> <ul class="grid-row job-totals">

View File

@@ -1,4 +1,6 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
GOV.UK Notify GOV.UK Notify
@@ -13,17 +15,9 @@ GOV.UK Notify
<p> You can now create a new password for your account.</p> <p> You can now create a new password for your account.</p>
<form action="" autocomplete="off" method="post"> <form method="post" autocomplete="off">
{{ form.hidden_tag() }} {{ textbox(form.new_password, hint="Your password must have at least 10 characters") }}
<p> {{ page_footer("Continue") }}
{{ render_field(form.new_password, class="form-control-1-4", type="password") }}
<span class="font-xsmall">Your password must have at least 10 characters</span>
</p>
<p>
<button class="button" role="button">Continue</button>
</p>
</form> </form>
{% else %} {% else %}
Message about email address does not exist. Some one needs to figure out the words here. Message about email address does not exist. Some one needs to figure out the words here.

View File

@@ -1,5 +1,6 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/sms-message.html" import sms_message, message_status %} {% from "components/sms-message.html" import sms_message, message_status %}
{% from "components/banner.html" import banner %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -18,7 +19,9 @@ GOV.UK Notify | Notifications activity
{{ sms_message(message.message, message.phone) }} {{ sms_message(message.message, message.phone) }}
</div> </div>
<div class="column-one-third"> <div class="column-one-third">
{{ message_status(message.status, delivered_at) }} {{ banner(
"{} {}".format(message.status, delivered_at)
) }}
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -16,12 +17,10 @@ GOV.UK Notify | Create an account
<form autocomplete="off" action="" method="post"> <form autocomplete="off" action="" method="post">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ render_field(form.name, class='form-control-2-3') }} {{ textbox(form.name) }}
{{ render_field(form.email_address, class='form-control-2-3') }} {{ textbox(form.email_address, hint="Your email address must end in .gov.uk") }}
<span class="font-xsmall">Your email address must end in .gov.uk</span> {{ textbox(form.mobile_number) }}
{{ render_field(form.mobile_number, class='form-control-2-3') }} {{ textbox(form.password, hint="Your password must have at least 10 characters") }}
{{ render_field(form.password, class='form-control-2-3') }}
<span class="font-xsmall">Your password must have at least 10 characters</span></label>
{{ page_footer("Continue") }} {{ page_footer("Continue") }}
</form> </form>
</div> </div>

View File

@@ -1,7 +1,7 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/sms-message.html" import sms_message %} {% from "components/sms-message.html" import sms_message %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form-field.html" import render_field %} {% from "components/textbox.html" import textbox %}
{% block page_title %} {% block page_title %}
GOV.UK Notify | Send text messages GOV.UK Notify | Send text messages
@@ -38,7 +38,7 @@
You can also <a href="#">download an example CSV</a>. You can also <a href="#">download an example CSV</a>.
</p> </p>
<p> <p>
{{render_field(form.file)}} {{textbox(form.file)}}
</p> </p>
{{ page_footer("Continue") }} {{ page_footer("Continue") }}

View File

@@ -14,7 +14,7 @@ GOV.UK Notify | Service settings
<div class="column-three-quarters"> <div class="column-three-quarters">
<form method="post"> <form method="post">
{{ textbox('new_name', 'Enter your password', password=True) }} {{ textbox(form.password) }}
{{ page_footer( {{ page_footer(
'Confirm', 'Confirm',
destructive=destructive, destructive=destructive,

View File

@@ -13,12 +13,15 @@ GOV.UK Notify | Service settings
<div class="grid-row"> <div class="grid-row">
<div class="column-three-quarters"> <div class="column-three-quarters">
<p> <p>Users will see your service name:</p>
Your service name ({{ service.name }}) is included in every sent notification
</p> <ul class="list-bullet">
<li>at the start of every text message, eg Vehicle tax: we received your payment, thank you</li>
<li>as your email sender name</li>
</ul>
<form method="post"> <form method="post">
{{ textbox('new_name', 'New name', value=service.name) }} {{ textbox(form.service_name) }}
{{ page_footer( {{ page_footer(
'Save', 'Save',
back_link=url_for('.service_settings') back_link=url_for('.service_settings')

View File

@@ -1,4 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -13,14 +14,10 @@ Sign in
<p>If you do not have an account, you can <a href="register">register for one now</a>.</p> <p>If you do not have an account, you can <a href="register">register for one now</a>.</p>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.email_address) }}
{{ render_field(form.email_address, class='form-control-2-3') }} {{ textbox(form.password) }}
{{ render_field(form.password, class='form-control-2-3') }} {{ page_footer("Continue", back_link="#", back_link_text="Forgotten password?") }}
<p>
<span class="font-xsmall"><a href="{{url_for('main.forgot_password')}}">Forgotten password?</a></span>
</p>
{{ page_footer("Continue") }}
</form> </form>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,155 @@
{% extends "admin_template.html" %}
{% from "components/banner.html" import banner %}
{% from "components/big-number.html" import big_number %}
{% from "components/browse-list.html" import browse_list %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/sms-message.html" import sms_message %}
{% from "components/table.html" import mapping_table, list_table, row, field %}
{% from "components/textbox.html" import textbox %}
{% block page_title %}
Styleguide GOV.UK Notify
{% endblock %}
{% block fullwidth_content %}
<h1 class="heading-xlarge">
Styleguide
</h1>
<p><a href="https://github.com/alphagov/notifications-admin/blob/master/app/templates/views/styleguide.html">View source</a></p>
<h2 class="heading-large">Banner</h2>
<p>Used to show the result of a users action.</p>
{{ banner("This is a banner", with_tick=True) }}
<div class="grid-row">
<div class="column-one-third">
{{ banner("Delivered 10:20") }}
</div>
</div>
<h2 class="heading-large">Big number</h2>
<p>Used to show some important statistics.</p>
<div class="grid-row">
<div class="column-one-third">
{{ big_number("567") }}
</div>
<div class="column-one-third">
{{ big_number("2", "Messages delivered") }}
</div>
</div>
<h2 class="heading-large">Browse list</h2>
<p>Used to navigate to child pages.</p>
{{ browse_list([
{
'title': 'Change your username',
'link': 'http://example.com',
},
{
'title': 'Change your password',
'link': 'http://example.com',
'hint': 'Your password is used to log in'
},
{
'title': 'Delete everything',
'link': 'http://example.com',
'hint': 'You cant undo this',
'destructive': True
}
]) }}
<h2 class="heading-large">Page footer</h2>
<p>
Used to submit forms and optionally provide a link to go back to the
previous page.
</p>
<p>
Must be used inside a form.
</p>
<p>
Adds a hidden CSRF token to the page.
</p>
{{ page_footer(
button_text='Save and continue'
) }}
{{ page_footer(
button_text='Delete', destructive=True
) }}
{{ page_footer(
button_text='Send', back_link='http://example.com', back_link_text="Back to dashboard"
) }}
<h2 class="heading-large">SMS message</h2>
<p>Used to show or preview an SMS message.</p>
<div class="grid-row">
<div class="column-half">
{{ sms_message("Your vehicle tax for LC12 BFL is due on 1 March 2016. Renew online at www.gov.uk/vehicle-tax") }}
{{ sms_message("Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax") }}
{{ sms_message(
"Your vehicle tax for registration number is due on date. Renew online at www.gov.uk/vehicle-tax",
"+44 7700 900 306"
) }}
</div>
</div>
<h2 class="heading-large">Tables</h2>
{% call mapping_table(
caption='Account settings',
field_headings=['Label', 'Value'],
field_headings_visible=True,
caption_visible=True
) %}
{% call row() %}
{% call field() %}
Username
{% endcall %}
{% call field() %}
admin
{% endcall %}
{% endcall %}
{% endcall %}
{% call(item) list_table(
[
{
'file': 'dispatch_20151114.csv', 'status': 'Queued'
},
{
'file': 'dispatch_20151117.csv', 'status': 'Delivered'
},
{
'file': 'remdinder_monday.csv', 'status': 'Delivered'
}
],
caption='Messages',
field_headings=['File', 'Status'],
field_headings_visible=False,
caption_visible=True
) %}
{% call field() %}
{{ item.file }}
{% endcall %}
{% call field() %}
{{ item.status }}
{% endcall %}
{% endcall %}
<h2 class="heading-large">Textbox</h2>
{{ textbox(form.username) }}
{{ textbox(form.password) }}
{{ textbox(form.message, highlight_tags=True) }}
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -13,9 +14,8 @@ GOV.UK Notify
<p>Check your mobile phone number is correct and then resend the confirmation code.</p> <p>Check your mobile phone number is correct and then resend the confirmation code.</p>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.mobile_number) }}
{{ render_field(form.mobile_number, class='form-control-2-3') }}
{{ page_footer("Resend confirmation code") }} {{ page_footer("Resend confirmation code") }}
</form> </form>
</div> </div>

View File

@@ -1,4 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -14,9 +15,8 @@ GOV.UK Notify | Text verification
<p>We've sent you a text message with a verification code.</p> <p>We've sent you a text message with a verification code.</p>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.sms_code) }}
{{ render_field(form.sms_code, class='form-control-1-4') }}
<span class="font-xsmall"><a href="{{ url_for('.verification_code_not_received') }}">I haven't received a text</a></span> <span class="font-xsmall"><a href="{{ url_for('.verification_code_not_received') }}">I haven't received a text</a></span>
{{ page_footer("Continue") }} {{ page_footer("Continue") }}
</form> </form>

View File

@@ -1,4 +1,5 @@
{% extends "admin_template.html" %} {% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -13,11 +14,10 @@ GOV.UK Notify | Confirm email address and mobile number
<p>We've sent you confirmation codes by email and text message. You need to enter both codes here.</p> <p>We've sent you confirmation codes by email and text message. You need to enter both codes here.</p>
<form autocomplete="off" action="" method="post"> <form autocomplete="off" method="post">
{{ form.hidden_tag() }} {{ textbox(form.email_code) }}
{{ render_field(form.email_code, class='form-control-1-4') }}
<span class="font-xsmall"><a href="{{ url_for('.check_and_resend_email_code')}}">I haven't received an email</a></span> <span class="font-xsmall"><a href="{{ url_for('.check_and_resend_email_code')}}">I haven't received an email</a></span>
{{ render_field(form.sms_code, class='form-control-1-4') }} {{ textbox(form.sms_code) }}
<span class="font-xsmall"><a href="{{ url_for('.check_and_resend_text_code') }}">I haven't received a text</a></span> <span class="font-xsmall"><a href="{{ url_for('.check_and_resend_text_code') }}">I haven't received a text</a></span>
{{ page_footer("Continue") }} {{ page_footer("Continue") }}
</form> </form>

View File

@@ -13,7 +13,7 @@ hooks:
location: scripts/aws_change_ownership.sh location: scripts/aws_change_ownership.sh
runas: root runas: root
timeout: 300 timeout: 300
ApplicationStart: ApplicationStart:
- -
location: scripts/aws_start_app.sh location: scripts/aws_start_app.sh
runas: root runas: root

View File

@@ -29,7 +29,7 @@ class Config(object):
SECRET_KEY = 'secret-key' SECRET_KEY = 'secret-key'
HTTP_PROTOCOL = 'http' HTTP_PROTOCOL = 'http'
DANGEROUS_SALT = 'itsdangeroussalt' DANGEROUS_SALT = 'itsdangeroussalt'
TOKEN_MAX_AGE_SECONDS = 120000 TOKEN_MAX_AGE_SECONDS = 3600
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10mb MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10mb

View File

View File

@@ -0,0 +1,35 @@
from itsdangerous import BadSignature
from pytest import fail
from app.notify_client.sender import generate_token, check_token
def test_should_return_email_from_signed_token(notifications_admin,
notifications_admin_db,
notify_db_session):
email = 'email@something.com'
token = generate_token(email)
assert email == check_token(token)
def test_should_throw_exception_when_token_is_tampered_with(notifications_admin,
notifications_admin_db,
notify_db_session):
email = 'email@something.com'
token = generate_token(email)
try:
check_token(token + 'qerqwer')
fail()
except BadSignature:
pass
def test_return_none_when_token_is_expired(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context():
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = -1000
email = 'email@something.com'
token = generate_token(email)
assert check_token(token) is None
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 120000

View File

@@ -1,15 +1,14 @@
from flask import url_for from flask import url_for
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.views import generate_token
from tests.app.main import create_test_user from tests.app.main import create_test_user
def test_should_render_forgot_password(notifications_admin, notifications_admin_db, notify_db_session): def test_should_render_forgot_password(notifications_admin, notifications_admin_db, notify_db_session):
response = notifications_admin.test_client().get('/forgot-password') with notifications_admin.test_request_context():
assert response.status_code == 200 response = notifications_admin.test_client().get(url_for('.forgot_password'))
assert 'If you have forgotten your password, we can send you an email to create a new password.' \ assert response.status_code == 200
in response.get_data(as_text=True) assert 'If you have forgotten your password, we can send you an email to create a new password.' \
in response.get_data(as_text=True)
def test_should_redirect_to_password_reset_sent_and_state_updated(notifications_admin, def test_should_redirect_to_password_reset_sent_and_state_updated(notifications_admin,
@@ -17,25 +16,11 @@ def test_should_redirect_to_password_reset_sent_and_state_updated(notifications_
mocker, mocker,
notify_db_session): notify_db_session):
mocker.patch("app.admin_api_client.send_email") mocker.patch("app.admin_api_client.send_email")
user = create_test_user('active')
response = notifications_admin.test_client().post('/forgot-password',
data={'email_address': user.email_address})
assert response.status_code == 200
assert 'You have been sent an email containing a link to reset your password.' in response.get_data(
as_text=True)
assert users_dao.get_user_by_id(user.id).state == 'request_password_reset'
def test_should_redirect_to_forgot_password_with_flash_message_when_token_is_expired(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context(): with notifications_admin.test_request_context():
with notifications_admin.test_client() as client: user = create_test_user('active')
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = -1000 response = notifications_admin.test_client().post(url_for('.forgot_password'),
user = create_test_user('active') data={'email_address': user.email_address})
token = generate_token(user.email_address) assert response.status_code == 200
response = client.post('/new-password/{}'.format(token), assert 'You have been sent an email containing a link to reset your password.' in response.get_data(
data={'new_password': 'a-new_password'}) as_text=True)
assert response.status_code == 302 assert users_dao.get_user_by_id(user.id).state == 'request_password_reset'
assert response.location == url_for('.forgot_password', _external=True)
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 86400

View File

@@ -2,7 +2,7 @@ from flask import url_for
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.encryption import check_hash from app.main.encryption import check_hash
from app.main.views import generate_token from app.notify_client.sender import generate_token
from tests.app.main import create_test_user from tests.app.main import create_test_user
@@ -56,7 +56,7 @@ def test_should_redirect_to_forgot_password_with_flash_message_when_token_is_exp
response = client.post(url_for('.new_password', token=token), data={'new_password': 'a-new_password'}) response = client.post(url_for('.new_password', token=token), data={'new_password': 'a-new_password'})
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for('.forgot_password', _external=True) assert response.location == url_for('.forgot_password', _external=True)
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 86400 notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 3600
def test_should_redirect_to_forgot_password_when_user_is_active_should_be_request_password_reset(notifications_admin, def test_should_redirect_to_forgot_password_when_user_is_active_should_be_request_password_reset(notifications_admin,

View File

@@ -0,0 +1,4 @@
def test_styleguide_can_render(notifications_admin):
response = notifications_admin.test_client().get('/_styleguide')
assert response.status_code == 200