mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 02:42:26 -05:00
All tests passing and merged with master.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
%banner,
|
||||
.banner {
|
||||
.banner,
|
||||
.banner-default {
|
||||
|
||||
@include core-19;
|
||||
background: $turquoise;
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
.table-empty-message {
|
||||
@include core-16;
|
||||
color: $secondary-text-colour;
|
||||
border-top: 1px solid $border-colour;
|
||||
border-bottom: 1px solid $border-colour;
|
||||
padding: 5px 0 8px 0;
|
||||
padding: 0.75em 0 0.5625em 0;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,11 @@ def get_service_by_id(id_):
|
||||
return notifications_api_client.get_service(id_)
|
||||
|
||||
|
||||
def get_services():
|
||||
return notifications_api_client.get_services()
|
||||
def get_services(user_id=None):
|
||||
if user_id:
|
||||
return notifications_api_client.get_services({'user_id': str(user_id)})
|
||||
else:
|
||||
return notifications_api_client.get_services()
|
||||
|
||||
|
||||
def unrestrict_service(service_id):
|
||||
@@ -55,8 +58,8 @@ def activate_service(service_id):
|
||||
|
||||
|
||||
# TODO Fix when functionality is added to the api.
|
||||
def find_service_by_service_name(service_name):
|
||||
resp = notifications_api_client.get_services()
|
||||
def find_service_by_service_name(service_name, user_id=None):
|
||||
resp = notifications_api_client.get_services(user_id)
|
||||
retval = None
|
||||
for srv_json in resp['data']:
|
||||
if srv_json['name'] == service_name:
|
||||
@@ -69,8 +72,8 @@ def delete_service(id_):
|
||||
return notifications_api_client.delete_service(id_)
|
||||
|
||||
|
||||
def find_all_service_names():
|
||||
resp = notifications_api_client.get_services()
|
||||
def find_all_service_names(user_id=None):
|
||||
resp = notifications_api_client.get_services(user_id)
|
||||
return [x['name'] for x in resp['data']]
|
||||
|
||||
|
||||
@@ -90,4 +93,4 @@ class ServicesBrowsableItem(BrowsableItem):
|
||||
|
||||
@property
|
||||
def hint(self):
|
||||
return "Some service hint here"
|
||||
return None
|
||||
|
||||
@@ -38,6 +38,7 @@ def update_user(user):
|
||||
def increment_failed_login_count(id):
|
||||
user = get_user_by_id(id)
|
||||
user.failed_login_count += 1
|
||||
return user_api_client.update_user(user)
|
||||
|
||||
|
||||
def activate_user(user):
|
||||
@@ -45,38 +46,19 @@ def activate_user(user):
|
||||
return user_api_client.update_user(user)
|
||||
|
||||
|
||||
def update_email_address(id, email_address):
|
||||
user = get_user_by_id(id)
|
||||
user.email_address = email_address
|
||||
# TODO update user
|
||||
|
||||
|
||||
def is_email_unique(email_address):
|
||||
if user_api_client.get_user_by_email(email_address):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def update_mobile_number(id, mobile_number):
|
||||
user = get_user_by_id(id)
|
||||
user.mobile_number = mobile_number
|
||||
# TODO update user
|
||||
|
||||
|
||||
def update_password(user, password):
|
||||
user.password = hashpw(password)
|
||||
user.password_changed_at = datetime.now()
|
||||
user.state = 'active'
|
||||
# TODO update user
|
||||
|
||||
|
||||
def request_password_reset(email):
|
||||
user = get_user_by_email(email)
|
||||
user.state = 'request_password_reset'
|
||||
# TODO update user
|
||||
|
||||
|
||||
def send_verify_code(user_id, code_type):
|
||||
def send_verify_code(user_id, code_type, to=None):
|
||||
return user_api_client.send_verify_code(user_id, code_type)
|
||||
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@ class LoginForm(Form):
|
||||
|
||||
|
||||
class RegisterUserForm(Form):
|
||||
def __init__(self, existing_email_addresses, *args, **kwargs):
|
||||
self.existing_emails = existing_email_addresses
|
||||
def __init__(self, unique_email_func, *args, **kwargs):
|
||||
self.unique_email_func = unique_email_func
|
||||
super(RegisterUserForm, self).__init__(*args, **kwargs)
|
||||
|
||||
name = StringField('Full name',
|
||||
@@ -117,7 +117,7 @@ class RegisterUserForm(Form):
|
||||
|
||||
def validate_email_address(self, field):
|
||||
# Validate email address is unique.
|
||||
if self.existing_emails(field.data):
|
||||
if not self.unique_email_func(field.data):
|
||||
raise ValidationError('Email address already exists')
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
templates = [
|
||||
{
|
||||
'type': 'sms',
|
||||
'name': 'Confirmation with details Jan 2016',
|
||||
'body': '((name)), we’ve received your ((thing)). We’ll contact you again within 1 week.'
|
||||
},
|
||||
{
|
||||
'type': 'sms',
|
||||
'name': 'Confirmation Jan 2016',
|
||||
'body': 'We’ve received your payment. We’ll contact you again within 1 week.'
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
from flask import request, render_template, jsonify, redirect, session, url_for, abort
|
||||
from flask_login import login_required
|
||||
from flask import render_template, redirect, session, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from app.main import main
|
||||
from app.main.dao import services_dao, users_dao
|
||||
from app.main.forms import AddServiceForm
|
||||
@@ -9,7 +9,7 @@ from app.main.forms import AddServiceForm
|
||||
@login_required
|
||||
def add_service():
|
||||
form = AddServiceForm(services_dao.find_all_service_names)
|
||||
services = services_dao.get_services()
|
||||
services = services_dao.get_services(current_user.id)
|
||||
if len(services) > 0:
|
||||
heading = 'Set up notifications for your service'
|
||||
else:
|
||||
|
||||
@@ -51,5 +51,5 @@ def revoke_api_key(service_id, key_id):
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
api_key_api_client.revoke_api_key(service_id=service_id, key_id=key_id)
|
||||
flash('‘{}’ was revoked'.format(key_name))
|
||||
flash('‘{}’ was revoked'.format(key_name), 'default')
|
||||
return redirect(url_for('.api_keys', service_id=service_id))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from flask import (render_template, redirect, url_for)
|
||||
from flask_login import login_required
|
||||
from flask_login import login_required, current_user
|
||||
from app.main.dao import services_dao
|
||||
from app.main import main
|
||||
|
||||
@@ -7,7 +7,7 @@ from app.main import main
|
||||
@main.route("/services")
|
||||
@login_required
|
||||
def choose_service():
|
||||
services = services_dao.get_services()
|
||||
services = services_dao.get_services(current_user.id)
|
||||
# If there is only one service redirect
|
||||
# to the service dashboard.
|
||||
if len(services['data']) == 1:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from flask import (
|
||||
render_template, redirect, session, url_for)
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from app.main import main
|
||||
from app.main.dao import users_dao
|
||||
from app.main.forms import EmailNotReceivedForm, TextNotReceivedForm
|
||||
@@ -12,8 +14,9 @@ def check_and_resend_email_code():
|
||||
user = users_dao.get_user_by_email(session['user_details']['email'])
|
||||
form = EmailNotReceivedForm(email_address=user.email_address)
|
||||
if form.validate_on_submit():
|
||||
users_dao.update_email_address(id=user.id, email_address=form.email_address.data)
|
||||
users_dao.send_verify_code(user.id, 'email')
|
||||
users_dao.send_verify_code(user.id, 'email', to=form.email_address.data)
|
||||
user.email_address = form.email_address.data
|
||||
users_dao.update_user(user)
|
||||
return redirect(url_for('.verify'))
|
||||
return render_template('views/email-not-received.html', form=form)
|
||||
|
||||
@@ -24,8 +27,9 @@ def check_and_resend_text_code():
|
||||
user = users_dao.get_user_by_email(session['user_details']['email'])
|
||||
form = TextNotReceivedForm(mobile_number=user.mobile_number)
|
||||
if form.validate_on_submit():
|
||||
users_dao.update_mobile_number(id=user.id, mobile_number=form.mobile_number.data)
|
||||
users_dao.send_verify_code(user.id, 'sms')
|
||||
users_dao.send_verify_code(user.id, 'sms', to=form.mobile_number.data)
|
||||
user.mobile_number = form.mobile_number.data
|
||||
users_dao.update_user(user)
|
||||
return redirect(url_for('.verify'))
|
||||
return render_template('views/text-not-received.html', form=form)
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from flask import render_template
|
||||
from flask import render_template, url_for, redirect
|
||||
from app.main import main
|
||||
from flask_login import login_required
|
||||
|
||||
from flask.ext.login import current_user
|
||||
|
||||
|
||||
@main.route('/')
|
||||
def index():
|
||||
if current_user and current_user.is_authenticated():
|
||||
return redirect(url_for('main.choose_service'))
|
||||
return render_template('views/signedout.html')
|
||||
|
||||
|
||||
|
||||
@@ -21,8 +21,11 @@ def new_password(token):
|
||||
form = NewPasswordForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
users_dao.update_password(user, form.new_password.data)
|
||||
users_dao.send_verify_code(user.id, 'sms')
|
||||
session['user_details'] = {
|
||||
'id': user.id,
|
||||
'email': user.email_address,
|
||||
'password': form.new_password.data}
|
||||
return redirect(url_for('main.two_factor'))
|
||||
else:
|
||||
return render_template('views/new-password.html', token=token, form=form, user=user)
|
||||
|
||||
@@ -8,6 +8,8 @@ from flask import (
|
||||
url_for
|
||||
)
|
||||
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from client.errors import HTTPError
|
||||
|
||||
from app.main import main
|
||||
@@ -19,7 +21,10 @@ from app import user_api_client
|
||||
|
||||
@main.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
form = RegisterUserForm(users_dao.get_user_by_email)
|
||||
if current_user and current_user.is_authenticated():
|
||||
return redirect(url_for('main.choose_service'))
|
||||
|
||||
form = RegisterUserForm(users_dao.is_email_unique)
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,8 @@ from flask import (
|
||||
abort
|
||||
)
|
||||
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from app.main import main
|
||||
from app.main.dao import users_dao
|
||||
from app.main.forms import LoginForm
|
||||
@@ -13,18 +15,31 @@ from app.main.forms import LoginForm
|
||||
|
||||
@main.route('/sign-in', methods=(['GET', 'POST']))
|
||||
def sign_in():
|
||||
if current_user and current_user.is_authenticated():
|
||||
return redirect(url_for('main.choose_service'))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = users_dao.get_user_by_email(form.email_address.data)
|
||||
user = _get_and_verify_user(form.email_address.data, form.password.data)
|
||||
if user:
|
||||
if not user.is_locked() and user.is_active() and users_dao.verify_password(user, form.password.data):
|
||||
users_dao.send_verify_code(user.id, 'sms')
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
return redirect(url_for('.two_factor'))
|
||||
else:
|
||||
# TODO re wire this increment to api
|
||||
users_dao.increment_failed_login_count(user.id)
|
||||
# Vague error message for login
|
||||
form.password.errors.append('Username or password is incorrect')
|
||||
users_dao.send_verify_code(user.id, 'sms')
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
return redirect(url_for('.two_factor'))
|
||||
else:
|
||||
# Vague error message for login in case of user not known, locked, inactive or password not verified
|
||||
form.password.errors.append('Username or password is incorrect')
|
||||
|
||||
return render_template('views/signin.html', form=form)
|
||||
|
||||
|
||||
def _get_and_verify_user(email_address, password):
|
||||
user = users_dao.get_user_by_email(email_address)
|
||||
if not user:
|
||||
return None
|
||||
elif user.is_locked():
|
||||
return None
|
||||
elif not user.is_active():
|
||||
return None
|
||||
elif not users_dao.verify_password(user, password):
|
||||
return None
|
||||
else:
|
||||
return user
|
||||
|
||||
@@ -24,12 +24,7 @@ from app.main.uploader import (
|
||||
s3upload,
|
||||
s3download
|
||||
)
|
||||
|
||||
from ._templates import templates
|
||||
|
||||
sms_templates = [
|
||||
template for template in templates if template['type'] == 'sms'
|
||||
]
|
||||
from app.main.dao import templates_dao
|
||||
|
||||
|
||||
@main.route("/services/<int:service_id>/sms/send", methods=['GET', 'POST'])
|
||||
@@ -51,8 +46,16 @@ def send_sms(service_id):
|
||||
flash(str(e))
|
||||
return redirect(url_for('.send_sms', service_id=service_id))
|
||||
|
||||
try:
|
||||
templates = templates_dao.get_service_templates(service_id)['data']
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
abort(404)
|
||||
else:
|
||||
raise e
|
||||
|
||||
return render_template('views/send-sms.html',
|
||||
message_templates=sms_templates,
|
||||
templates=templates,
|
||||
form=form,
|
||||
service_id=service_id)
|
||||
|
||||
@@ -69,7 +72,9 @@ def check_sms(service_id, upload_id):
|
||||
'views/check-sms.html',
|
||||
upload_result=upload_result,
|
||||
filename='someupload_file_name.csv',
|
||||
message_template=sms_templates[0]['body'],
|
||||
message_template='''
|
||||
((name)), we’ve received your ((thing)). We’ll contact you again within 1 week.
|
||||
''',
|
||||
service_id=service_id
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
|
||||
@@ -20,8 +20,12 @@ def two_factor():
|
||||
form = TwoFactorForm(_check_code)
|
||||
|
||||
if form.validate_on_submit():
|
||||
del session['user_details']
|
||||
user = users_dao.get_user_by_id(user_id)
|
||||
# Check if coming from new password page
|
||||
if 'password' in session['user_details']:
|
||||
user.set_password(session['user_details']['password'])
|
||||
users_dao.update_user(user)
|
||||
del session['user_details']
|
||||
login_user(user)
|
||||
return redirect(url_for('.choose_service'))
|
||||
|
||||
|
||||
@@ -70,8 +70,10 @@ class UserApiClient(BaseAPIClient):
|
||||
return user[0]
|
||||
return None
|
||||
|
||||
def send_verify_code(self, user_id, code_type):
|
||||
def send_verify_code(self, user_id, code_type, to=None):
|
||||
data = {'code_type': code_type}
|
||||
if to:
|
||||
data['to'] = to
|
||||
endpoint = '/user/{}/code'.format(user_id)
|
||||
resp = self.post(endpoint, data=data)
|
||||
|
||||
@@ -92,41 +94,14 @@ class UserApiClient(BaseAPIClient):
|
||||
|
||||
class User(object):
|
||||
def __init__(self, fields, max_failed_login_count=3):
|
||||
self.fields = fields
|
||||
self.max_failed_login_count = max_failed_login_count
|
||||
self._id = fields.get('id')
|
||||
self._name = fields.get('name')
|
||||
self._email_address = fields.get('email_address')
|
||||
self._mobile_number = fields.get('mobile_number')
|
||||
self._password_changed_at = fields.get('password_changed_at')
|
||||
self._failed_login_count = 0
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.fields.get('id')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.fields.get('name')
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self.fields['name'] = name
|
||||
|
||||
@property
|
||||
def email_address(self):
|
||||
return self.fields.get('email_address')
|
||||
|
||||
@email_address.setter
|
||||
def email_address(self, email_address):
|
||||
self.fields['email_address'] = email_address
|
||||
|
||||
@property
|
||||
def mobile_number(self):
|
||||
return self.fields.get('mobile_number')
|
||||
|
||||
@mobile_number.setter
|
||||
def mobile_number(self, mobile_number):
|
||||
self.fields['mobile_number'] = mobile_number
|
||||
|
||||
@property
|
||||
def password_changed_at(self):
|
||||
return self.fields.get('password_changed_at')
|
||||
self._state = fields.get('state')
|
||||
self.max_failed_login_count = max_failed_login_count
|
||||
|
||||
def get_id(self):
|
||||
return self.id
|
||||
@@ -137,13 +112,53 @@ class User(object):
|
||||
def is_active(self):
|
||||
return self.state == 'active'
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@id.setter
|
||||
def id(self, id):
|
||||
self._id = id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def email_address(self):
|
||||
return self._email_address
|
||||
|
||||
@email_address.setter
|
||||
def email_address(self, email_address):
|
||||
self._email_address = email_address
|
||||
|
||||
@property
|
||||
def mobile_number(self):
|
||||
return self._mobile_number
|
||||
|
||||
@mobile_number.setter
|
||||
def mobile_number(self, mobile_number):
|
||||
self._mobile_number = mobile_number
|
||||
|
||||
@property
|
||||
def password_changed_at(self):
|
||||
return self._password_changed_at
|
||||
|
||||
@password_changed_at.setter
|
||||
def password_changed_at(self, password_changed_at):
|
||||
self._password_changed_at = password_changed_at
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.fields['state']
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self.fields['state'] = state
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def failed_login_count(self):
|
||||
@@ -157,10 +172,19 @@ class User(object):
|
||||
return False
|
||||
|
||||
def is_locked(self):
|
||||
return self.failed_login_count > self.max_failed_login_count
|
||||
return self.failed_login_count >= self.max_failed_login_count
|
||||
|
||||
def serialize(self):
|
||||
return self.fields
|
||||
dct = {"id": self.id,
|
||||
"name": self.name,
|
||||
"email_address": self.email_address,
|
||||
"mobile_number": self.mobile_number,
|
||||
"password_changed_at": self.password_changed_at,
|
||||
"state": self.state,
|
||||
"failed_login_count": self.failed_login_count}
|
||||
if getattr(self, '_password', None):
|
||||
dct['password'] = self._password
|
||||
return dct
|
||||
|
||||
def set_password(self, pwd):
|
||||
self.fields['password'] = pwd
|
||||
self._password = pwd
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
Current service
|
||||
</summary>
|
||||
<div>
|
||||
<a href="{{ url_for('main.choose_service') }}">Switchx§ service</a>
|
||||
<a href="{{ url_for('main.choose_service') }}">Switch service</a>
|
||||
<a href="{{ url_for('.add_service') }}">Add a new service to GOV.UK Notify</a>
|
||||
</div>
|
||||
</details>
|
||||
@@ -72,19 +72,13 @@
|
||||
<main id="content" role="main" class="page-container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<ul class="banner-dangerous">
|
||||
{% for category, message in messages %}
|
||||
<li class="flash-message">
|
||||
{{ message }}
|
||||
{% if 'delete' == category %}
|
||||
<form method='post'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" class="button" name="delete" value="Yes, delete this template" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for category, message in messages %}
|
||||
{{ banner(
|
||||
message,
|
||||
'default' if category == 'default' else 'dangerous',
|
||||
delete_button="Yes, delete this template" if 'delete' == category else None
|
||||
)}}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block fullwidth_content %}{% endblock %}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{% macro banner(body, type=None, with_tick=False) %}
|
||||
{% macro banner(body, type=None, with_tick=False, delete_button=None) %}
|
||||
<div class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'>
|
||||
{{ body }}
|
||||
{% if delete_button %}
|
||||
<form method='post'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" class="button" name="delete" value="{{ delete_button }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</h3>
|
||||
{% endif %}
|
||||
{% if input_name %}
|
||||
<input class="sms-message-picker" type="radio" id="{{ input_name }}-{{ input_index }}" name="{{ input_name }}" />
|
||||
<input class="sms-message-picker" type="radio" id="{{ input_name }}-{{ input_index }}" name="{{ input_name }}" value="{{ input_index }}" />
|
||||
{% endif %}
|
||||
<div class="sms-message-wrapper{% if input_name %}-with-radio{% endif %}">
|
||||
{{ body|placeholders }}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<li><a href="{{ url_for('.service_dashboard', service_id=service_id) }}">Dashboard</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="{{ url_for('.send_sms', service_id=service_id) }}">Send text messages</a></li>-->
|
||||
<!--<li><a href="{{ url_for('.send_email', service_id=service_id) }}">Send emails</a></li>-->
|
||||
<!--<li><a href="{{ url_for('.view_jobs', service_id=service_id) }}">Activity</a></li>-->
|
||||
<li><a href="{{ url_for('.send_sms', service_id=service_id) }}">Send text messages</a></li>
|
||||
<li><a href="{{ url_for('.send_email', service_id=service_id) }}">Send emails</a></li>
|
||||
<li><a href="{{ url_for('.view_jobs', service_id=service_id) }}">Activity</a></li>
|
||||
<li><a href="{{ url_for('.manage_service_templates', service_id=service_id) }}">Templates</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
@@ -13,7 +13,7 @@
|
||||
<li><a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage users</a></li>-->
|
||||
<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage users</a></li>
|
||||
<li><a href="{{ url_for('.service_settings', service_id=service_id) }}">Service settings</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
To connect to the API you will need to send your service ID, encrypted with
|
||||
an API key. The API key stays secret.
|
||||
To connect to the API you will need to create an API Key. Each service can have multiple API Keys to allow
|
||||
for test and live environments.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
There are client libraries available which can do this for you. See
|
||||
<a href="{{ url_for(".documentation", service_id=service_id) }}">the
|
||||
developer documentation</a> for more information.
|
||||
API usage is described in
|
||||
<a href="{{ url_for('.documentation', service_id=service_id) }}">the
|
||||
developer documentation</a>.
|
||||
</p>
|
||||
|
||||
<h2 class="api-key-name">
|
||||
|
||||
@@ -3,47 +3,162 @@
|
||||
{% from "components/api-key.html" import api_key %}
|
||||
|
||||
{% block page_title %}
|
||||
GOV.UK Notify | API keys and documentation
|
||||
GOV.UK Notify | API keys and documentation
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
|
||||
<h1 class="heading-xlarge">
|
||||
Developer documentation
|
||||
Developer documentation
|
||||
</h1>
|
||||
|
||||
<h2 class="heading-medium">
|
||||
How to integrate GOV.UK Notify into your service
|
||||
How to integrate GOV.UK Notify into your service
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
blah blah blah this is where we tell you how the API works
|
||||
Notify provides an API that allows the creation of text notifications and the ability to get the status of a
|
||||
sent notification.
|
||||
</p>
|
||||
|
||||
<h2 class="heading-medium">Repositories</h2>
|
||||
<h3 class="heading-medium">
|
||||
API authentication
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/alphagov/notifications-api">GOV.UK Notify API</a>
|
||||
Notify uses <a href="https://jwt.io/">JSON Web Tokens (JWT)</a> for authentication.
|
||||
JWT tokens have a series of claims, standard and application specific.
|
||||
</p>
|
||||
|
||||
<p>Notify standard claims:</p>
|
||||
|
||||
<pre>
|
||||
typ: JWT
|
||||
alg: HS256
|
||||
</pre>
|
||||
|
||||
<p>Notify application specific:</p>
|
||||
<pre>
|
||||
iss: service id
|
||||
iat: creation time in epoch seconds (UTC)
|
||||
req: signed request
|
||||
pay: signed payload (POST requests only)
|
||||
</pre>
|
||||
|
||||
<p>Notify API tokens sign both the request being made, and for POST requests, the payload.</p>
|
||||
|
||||
<p>
|
||||
The signing algorithm is HMAC signature, using provided key SHA256 hashing algorithm.
|
||||
</p>
|
||||
|
||||
<p>Request signing is of the form HTTP METHOD PATH.</p>
|
||||
|
||||
<code>GET /notification/1234</code>
|
||||
|
||||
<p></p>
|
||||
|
||||
<p>Payload signing requires the actual payload to be signed, NOT the JSON object. Serialize the object first
|
||||
then sign the serialized object.</p>
|
||||
|
||||
<h3 class="heading-medium">
|
||||
API endpoints
|
||||
</h3>
|
||||
|
||||
<p>To create a text notification</p>
|
||||
|
||||
<code>POST /notifications/sms</code>
|
||||
|
||||
<p>
|
||||
<pre>
|
||||
{
|
||||
'to': '+441234123123',
|
||||
'template': 1
|
||||
}
|
||||
</pre>
|
||||
Where 'to' is the phone number and 'template' is the template id to send.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/alphagov/notify-api-client">GOV.UK Notify Python client</a>
|
||||
Response:
|
||||
<pre>
|
||||
{
|
||||
'notification':
|
||||
{
|
||||
'to': '+441234123123',
|
||||
'createdAt': '2016-01-01T09:00:00.999999Z',
|
||||
'status': 'created',
|
||||
'id': 1,
|
||||
'message': '....',
|
||||
'method': 'sms',
|
||||
'jobId': 1
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>To get the status of a text notification</p>
|
||||
|
||||
<p>
|
||||
<code>
|
||||
GET /notifications/{id}
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<pre>
|
||||
{
|
||||
'notification':
|
||||
{
|
||||
'status': 'sent',
|
||||
'createdAt': '2016-01-01T09:00:00.999999Z',
|
||||
'to': '+447827992607',
|
||||
'method': 'sms',
|
||||
'sentAt': '2016-01-01T09:01:00.999999Z',
|
||||
'id': 1,
|
||||
'message': '...',
|
||||
'jobId': 1,
|
||||
'sender': 'sms-partner'
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<h2 class="heading-medium">
|
||||
API client libraries
|
||||
</h2>
|
||||
|
||||
<p>A python client library is support by the Notify team:</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/alphagov/notifications-python-client">GOV.UK Notify Python client</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This provides example code for calling the API and for constructing the API tokens.
|
||||
</p>
|
||||
|
||||
<h2 class="heading-medium">API code</h2>
|
||||
|
||||
<p>Notify API code is open sourced at:</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/alphagov/notifications-api">GOV.UK Notify API</a>
|
||||
</p>
|
||||
|
||||
<h2 class="heading-medium">API endpoint</h2>
|
||||
|
||||
<p>
|
||||
https://www.notify.works/api/endpoint
|
||||
https://www.notify.works/api/endpoint
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys for your service</a>
|
||||
<a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys for your service</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
<fieldset class='form-group'>
|
||||
<legend class="heading-medium">1. Choose text message template</legend>
|
||||
{% for template in message_templates %}
|
||||
{% for template in templates %}
|
||||
{{ sms_message(
|
||||
template.body, name=template.name, input_name='template', input_index=loop.index
|
||||
template.content, name=template.name, input_name='template', input_index=template.id
|
||||
) }}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
'title': 'Temporarily suspend API keys',
|
||||
'link': url_for('.service_status_change', service_id=service_id),
|
||||
'destructive': True
|
||||
} if service.active else {
|
||||
} if not service.active else {
|
||||
'title': 'Reactivate API keys',
|
||||
'link': url_for('.service_status_change', service_id=service_id)
|
||||
},
|
||||
|
||||
@@ -22,14 +22,25 @@
|
||||
<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 user’s action.</p>
|
||||
{{ banner("This is a banner", with_tick=True) }}
|
||||
<p>Used to show the status of a thing or action.</p>
|
||||
|
||||
{{ banner("You sent 1,234 text messages", with_tick=True) }}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-one-third">
|
||||
{{ banner("Delivered 10:20") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ banner(
|
||||
'Your service is in restricted mode. You can only send notifications to yourself.',
|
||||
'info'
|
||||
) }}
|
||||
|
||||
{{ banner('You’re not allowed to do this', 'dangerous')}}
|
||||
|
||||
{{ banner('Are you sure you want to delete?', 'dangerous', delete_button="Yes, delete this thing")}}
|
||||
|
||||
<h2 class="heading-large">Big number</h2>
|
||||
|
||||
<p>Used to show some important statistics.</p>
|
||||
|
||||
Reference in New Issue
Block a user