mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-23 15:57:23 -04:00
Allow service to set callback url for notifications
This commit is contained in:
@@ -717,15 +717,38 @@ class ServiceInboundNumberForm(Form):
|
||||
)
|
||||
|
||||
|
||||
class ServiceInboundApiForm(Form):
|
||||
url = StringField("Callback URL",
|
||||
validators=[DataRequired(message='Can’t be empty'),
|
||||
Regexp(regex="^https.*",
|
||||
message='Must be a valid https URL')]
|
||||
)
|
||||
bearer_token = PasswordFieldShowHasContent("Bearer token",
|
||||
validators=[DataRequired(message='Can’t be empty'),
|
||||
Length(min=10, message='Must be at least 10 characters')])
|
||||
class ServiceCallbacksForm(Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.can_receive_inbound = kwargs['can_receive_inbound']
|
||||
|
||||
inbound_url = StringField("Inbound URL")
|
||||
inbound_bearer_token = PasswordFieldShowHasContent("Bearer token")
|
||||
outbound_url = StringField(
|
||||
"Outbound URL",
|
||||
validators=[DataRequired(message='Can’t be empty'),
|
||||
Regexp(regex="^https.*", message='Must be a valid https URL')]
|
||||
)
|
||||
outbound_bearer_token = PasswordFieldShowHasContent(
|
||||
"Bearer token",
|
||||
validators=[DataRequired(message='Can’t be empty'),
|
||||
Length(min=10, message='Must be at least 10 characters')]
|
||||
)
|
||||
|
||||
def validate_inbound_url(self, field):
|
||||
pattern = re.compile("^https.*")
|
||||
if self.can_receive_inbound:
|
||||
if not field.data:
|
||||
raise ValidationError('Can’t be empty')
|
||||
elif not pattern.match(field.data):
|
||||
raise ValidationError('Must be a valid https URL')
|
||||
|
||||
def validate_inbound_bearer_token(self, field):
|
||||
if self.can_receive_inbound:
|
||||
if not field.data:
|
||||
raise ValidationError('Can’t be empty')
|
||||
elif len(field.data) < 10:
|
||||
raise ValidationError('Must be at least 10 characters')
|
||||
|
||||
|
||||
class InternationalSMSForm(Form):
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from flask import request, render_template, redirect, url_for, flash, Markup, abort
|
||||
from flask_login import login_required
|
||||
from flask_login import login_required, current_user
|
||||
from app.main import main
|
||||
from app.main.forms import CreateKeyForm, Whitelist
|
||||
from app.main.forms import CreateKeyForm, Whitelist, ServiceCallbacksForm
|
||||
from app import api_key_api_client, service_api_client, notification_api_client, current_service
|
||||
from app.utils import user_has_permissions, email_safe
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_NORMAL, KEY_TYPE_TEST, KEY_TYPE_TEAM
|
||||
|
||||
dummy_bearer_token = 'bearer_token_set'
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/api")
|
||||
@login_required
|
||||
@@ -113,3 +115,87 @@ def revoke_api_key(service_id, key_id):
|
||||
api_key_api_client.revoke_api_key(service_id=service_id, key_id=key_id)
|
||||
flash('‘{}’ was revoked'.format(key_name), 'default_with_tick')
|
||||
return redirect(url_for('.api_keys', service_id=service_id))
|
||||
|
||||
|
||||
def get_apis():
|
||||
callback_api = None
|
||||
inbound_api = None
|
||||
if current_service['service_callback_api']:
|
||||
callback_api = service_api_client.get_service_callback_api(
|
||||
current_service['id'],
|
||||
current_service.get('service_callback_api')[0]
|
||||
)
|
||||
if current_service['inbound_api']:
|
||||
inbound_api = service_api_client.get_service_inbound_api(
|
||||
current_service['id'],
|
||||
current_service.get('inbound_api')[0]
|
||||
)
|
||||
|
||||
return (callback_api, inbound_api)
|
||||
|
||||
|
||||
def check_token_against_dummy_bearer(token):
|
||||
if token != dummy_bearer_token:
|
||||
return token
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/api/callbacks", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def api_callbacks(service_id):
|
||||
callback_api, inbound_api = get_apis()
|
||||
can_receive_inbound = 'inbound_sms' in current_service['permissions']
|
||||
|
||||
form = ServiceCallbacksForm(
|
||||
inbound_url=inbound_api.get('url') if inbound_api else '',
|
||||
inbound_bearer_token=dummy_bearer_token if inbound_api else '',
|
||||
outbound_url=callback_api.get('url') if callback_api else '',
|
||||
outbound_bearer_token=dummy_bearer_token if callback_api else '',
|
||||
can_receive_inbound=can_receive_inbound,
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if callback_api:
|
||||
if (callback_api.get('url') != form.outbound_url.data
|
||||
or form.outbound_bearer_token.data != dummy_bearer_token):
|
||||
service_api_client.update_service_callback_api(
|
||||
service_id,
|
||||
url=form.outbound_url.data,
|
||||
bearer_token=check_token_against_dummy_bearer(form.outbound_bearer_token.data),
|
||||
user_id=current_user.id,
|
||||
callback_api_id=callback_api.get('id')
|
||||
)
|
||||
else:
|
||||
service_api_client.create_service_callback_api(
|
||||
service_id,
|
||||
url=form.outbound_url.data,
|
||||
bearer_token=form.outbound_bearer_token.data,
|
||||
user_id=current_user.id
|
||||
)
|
||||
if can_receive_inbound:
|
||||
if inbound_api:
|
||||
if (inbound_api.get('url') != form.inbound_url.data
|
||||
or form.inbound_bearer_token.data != dummy_bearer_token):
|
||||
service_api_client.update_service_inbound_api(
|
||||
service_id,
|
||||
url=form.inbound_url.data,
|
||||
bearer_token=check_token_against_dummy_bearer(form.inbound_bearer_token.data),
|
||||
user_id=current_user.id,
|
||||
inbound_api_id=inbound_api.get('id')
|
||||
)
|
||||
else:
|
||||
service_api_client.create_service_inbound_api(
|
||||
service_id,
|
||||
url=form.inbound_url.data,
|
||||
bearer_token=form.inbound_bearer_token.data,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
return redirect(url_for('.api_integration', service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/api/callbacks.html',
|
||||
form=form,
|
||||
can_receive_inbound=can_receive_inbound,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from flask import (
|
||||
render_template,
|
||||
@@ -33,7 +31,6 @@ from app.main.forms import (
|
||||
ServiceLetterContactBlockForm,
|
||||
ServiceBrandingOrg,
|
||||
LetterBranding,
|
||||
ServiceInboundApiForm,
|
||||
InternationalSMSForm,
|
||||
OrganisationTypeForm,
|
||||
FreeSMSAllowance,
|
||||
@@ -44,17 +41,6 @@ from app import user_api_client, current_service, organisations_client, inbound_
|
||||
from notifications_utils.formatters import formatted_list
|
||||
|
||||
|
||||
dummy_bearer_token = 'bearer_token_set'
|
||||
|
||||
|
||||
def get_inbound_api():
|
||||
if current_service['inbound_api']:
|
||||
return service_api_client.get_service_inbound_api(
|
||||
current_service['id'],
|
||||
current_service.get('inbound_api')[0]
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings")
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
@@ -65,14 +51,6 @@ def service_settings(service_id):
|
||||
else:
|
||||
organisation = None
|
||||
|
||||
inbound_api = get_inbound_api()
|
||||
if inbound_api:
|
||||
parsed_url = urlparse(inbound_api.get('url')) if inbound_api else ''
|
||||
inbound_api_url = '{uri.scheme}://{uri.netloc}{elide_token}'.format(
|
||||
uri=parsed_url, elide_token='...' if parsed_url.path else '')
|
||||
else:
|
||||
inbound_api_url = ''
|
||||
|
||||
inbound_number = inbound_number_client.get_inbound_sms_number_for_service(service_id)
|
||||
disp_inbound_number = inbound_number['data'].get('number', '')
|
||||
reply_to_email_addresses = service_api_client.get_reply_to_email_addresses(service_id)
|
||||
@@ -100,7 +78,6 @@ def service_settings(service_id):
|
||||
current_service.get('dvla_organisation', '001')
|
||||
),
|
||||
can_receive_inbound=('inbound_sms' in current_service['permissions']),
|
||||
inbound_api_url=inbound_api_url,
|
||||
inbound_number=disp_inbound_number,
|
||||
default_reply_to_email_address=default_reply_to_email_address,
|
||||
reply_to_email_address_count=reply_to_email_address_count,
|
||||
@@ -807,41 +784,3 @@ def get_branding_as_dict(organisations):
|
||||
'colour': organisation['colour']
|
||||
} for organisation in organisations
|
||||
}
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/set-inbound-api", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def service_set_inbound_api(service_id):
|
||||
if 'inbound_sms' not in current_service['permissions']:
|
||||
abort(403)
|
||||
|
||||
inbound_api = get_inbound_api()
|
||||
form = ServiceInboundApiForm(
|
||||
url=inbound_api.get('url') if inbound_api else '',
|
||||
bearer_token=dummy_bearer_token if inbound_api else ''
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if inbound_api:
|
||||
if inbound_api.get('url') != form.url.data or form.bearer_token.data != dummy_bearer_token:
|
||||
service_api_client.update_service_inbound_api(
|
||||
service_id,
|
||||
url=form.url.data,
|
||||
bearer_token=form.bearer_token.data if form.bearer_token.data != dummy_bearer_token else '',
|
||||
user_id=current_user.id,
|
||||
inbound_api_id=inbound_api.get('id')
|
||||
)
|
||||
else:
|
||||
service_api_client.create_service_inbound_api(
|
||||
service_id,
|
||||
url=form.url.data,
|
||||
bearer_token=form.bearer_token.data,
|
||||
user_id=current_user.id
|
||||
)
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/service-settings/set-inbound-api.html',
|
||||
form=form,
|
||||
)
|
||||
|
||||
@@ -387,6 +387,30 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
}
|
||||
)
|
||||
|
||||
def get_service_callback_api(self, service_id, callback_api_id):
|
||||
return self.get(
|
||||
"/service/{}/delivery-receipt-api/{}".format(
|
||||
service_id, callback_api_id
|
||||
)
|
||||
)['data']
|
||||
|
||||
def update_service_callback_api(self, service_id, url, bearer_token, user_id, callback_api_id):
|
||||
data = {
|
||||
"url": url,
|
||||
"updated_by_id": user_id
|
||||
}
|
||||
if bearer_token:
|
||||
data['bearer_token'] = bearer_token
|
||||
return self.post("/service/{}/delivery-receipt-api/{}".format(service_id, callback_api_id), data)
|
||||
|
||||
def create_service_callback_api(self, service_id, url, bearer_token, user_id):
|
||||
data = {
|
||||
"url": url,
|
||||
"bearer_token": bearer_token,
|
||||
"updated_by_id": user_id
|
||||
}
|
||||
return self.post("/service/{}/delivery-receipt-api".format(service_id), data)
|
||||
|
||||
|
||||
class ServicesBrowsableItem(BrowsableItem):
|
||||
@property
|
||||
|
||||
@@ -3,33 +3,44 @@
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Callback URL
|
||||
Callbacks
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
<div class="grid-row">
|
||||
<div class="column-five-sixths">
|
||||
<h1 class="heading-large">Callback URL for received text messages</h1>
|
||||
<h1 class="heading-large">Callbacks</h1>
|
||||
<p>
|
||||
Text messages you receive can be forwarded to your systems with our callback feature.
|
||||
See our <a href="{{ url_for('.callbacks') }}">documentation on the format of the callback</a>.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% if can_receive_inbound %}
|
||||
{{ textbox(
|
||||
form.inbound_url,
|
||||
width='1-1',
|
||||
hint='Valid https URL'
|
||||
) }}
|
||||
{{ textbox(
|
||||
form.inbound_bearer_token,
|
||||
width='2-3',
|
||||
hint='At least 10 characters'
|
||||
) }}
|
||||
{% endif %}
|
||||
{{ textbox(
|
||||
form.url,
|
||||
width='2-3',
|
||||
form.outbound_url,
|
||||
width='1-1',
|
||||
hint='Valid https URL'
|
||||
) }}
|
||||
{{ textbox(
|
||||
form.bearer_token,
|
||||
form.outbound_bearer_token,
|
||||
width='2-3',
|
||||
hint='At least 10 characters'
|
||||
) }}
|
||||
{{ page_footer(
|
||||
'Save',
|
||||
back_link=url_for('.service_settings', service_id=current_service.id),
|
||||
back_link_text='Back to settings'
|
||||
back_link=url_for('.api_integration', service_id=current_service.id),
|
||||
back_link_text='Back to API integration'
|
||||
) }}
|
||||
</form>
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@
|
||||
<a class="pill-separate-item" href="{{ url_for('.whitelist', service_id=current_service.id) }}">Whitelist</a>
|
||||
</div>
|
||||
<div class="column-one-third">
|
||||
<a class="pill-separate-item" href="{{ url_for('.api_documentation', service_id=current_service.id) }}">Documentation</a>
|
||||
<a class="pill-separate-item" href="{{ url_for('.api_callbacks', service_id=current_service.id) }}">Callback Url</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
If you don’t have ‘receive text messages’ enabled for your service, <a href="{{ url_for('.support') }}">get in touch</a> and we can turn it on for you.
|
||||
</p>
|
||||
|
||||
<h2 class="heading-medium">Format of the callback</h2>
|
||||
<h2 class="heading-medium">Format of the callbacks</h2>
|
||||
|
||||
<p>
|
||||
The format of the callback message you receive is JSON.
|
||||
</p>
|
||||
|
||||
<h3 class="heading-small"> Received text messages callback </h3>
|
||||
|
||||
<div class="bottom-gutter-3-2">
|
||||
{% call mapping_table(
|
||||
caption='Callback message format',
|
||||
@@ -60,4 +62,32 @@
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<h3 class="heading-small"> Status callback </h3>
|
||||
|
||||
<div class="bottom-gutter-3-2">
|
||||
{% call mapping_table(
|
||||
caption='Callback message format',
|
||||
field_headings=['Key', 'Description', 'Format'],
|
||||
field_headings_visible=True,
|
||||
caption_visible=False
|
||||
) %}
|
||||
{% for key, description, format in [
|
||||
('id', 'Notify’s id for the status receipts', 'UUID'),
|
||||
('reference', 'The reference sent by the service', '12345678'),
|
||||
('to', 'The email address of the recipient', 'hello@gov.uk'),
|
||||
('status', 'The status of the notification', 'delivered | permanent-failure | temporary-failure | technical-failure'),
|
||||
('created_at', 'The time the service sent the request', '2017-05-14T12:15:30.000000Z'),
|
||||
('updated_at', 'The last time the status was updated', '2017-05-14T12:15:30.000000Z'),
|
||||
('sent_at', 'The time the notification was sent', '2017-05-14T12:15:30.000000Z or nil'),
|
||||
('notification_type', 'The notification type', 'email | sms | letter')
|
||||
] %}
|
||||
{% call row() %}
|
||||
{% call row_heading() %} {{ key }} {% endcall %}
|
||||
{{ text_field(description) }}
|
||||
{{ text_field(format) }}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -120,14 +120,6 @@
|
||||
{{ edit_field('Change', url_for('.service_set_inbound_sms', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
|
||||
{% if can_receive_inbound %}
|
||||
{% call row() %}
|
||||
{{ text_field('Callback URL for received text messages') }}
|
||||
{{ optional_text_field(inbound_api_url) }}
|
||||
{{ edit_field('Change', url_for('.service_set_inbound_api', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
Reference in New Issue
Block a user