diff --git a/app/assets/javascripts/apiKey.js b/app/assets/javascripts/apiKey.js index 30d87278d..215be5672 100644 --- a/app/assets/javascripts/apiKey.js +++ b/app/assets/javascripts/apiKey.js @@ -1,15 +1,11 @@ (function(Modules) { "use strict"; + if (!document.queryCommandSupported('copy')) return; + Modules.ApiKey = function() { const states = { - 'initial': ` - - `, - 'keyVisibleBasic': key => ` - ${key} - `, 'keyVisible': key => ` ${key} @@ -33,23 +29,22 @@ this.start = function(component) { - const $component = $(component).html(states.initial).attr('aria-live', 'polite'), + const $component = $(component), key = $component.data('key'); $component - .on( - 'click', '.api-key-button-show', () => - $component.html( - document.queryCommandSupported('copy') ? - states.keyVisible(key) : states.keyVisibleBasic(key) - ) - ) + .html(states.keyVisible(key)) + .attr('aria-live', 'polite') .on( 'click', '.api-key-button-copy', () => this.copyKey( $('.api-key-key', component)[0], () => $component.html(states.keyCopied) ) + ) + .on( + 'click', '.api-key-button-show', () => + $component.html(states.keyVisible(key)) ); }; diff --git a/app/assets/stylesheets/components/api-key.scss b/app/assets/stylesheets/components/api-key.scss index 22c787355..e17b45ef4 100644 --- a/app/assets/stylesheets/components/api-key.scss +++ b/app/assets/stylesheets/components/api-key.scss @@ -1,5 +1,10 @@ .api-key { + &-name { + @include bold-19; + margin-bottom: 5px; + } + &-key { font-family: monospace; display: block; diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index ed749b6e3..ce6930370 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -1,3 +1,7 @@ +.table { + margin-bottom: $gutter; +} + .table-heading { text-align: left; } @@ -16,8 +20,15 @@ } &-error { + color: $error-colour; font-weight: bold; + + a:link, + a:visited { + color: $error-colour; + } + } } diff --git a/app/main/forms.py b/app/main/forms.py index 70af82666..6b8a224a2 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -235,3 +235,7 @@ class ChangeMobileNumberForm(Form): class ConfirmMobileNumberForm(Form): sms_code = sms_code() + + +class CreateKeyForm(Form): + key_name = StringField(u'Description of key') diff --git a/app/main/views/api_keys.py b/app/main/views/api_keys.py index 0241346a8..74a5abab4 100644 --- a/app/main/views/api_keys.py +++ b/app/main/views/api_keys.py @@ -1,9 +1,57 @@ -from flask import render_template +from flask import request, render_template, redirect, url_for, flash from flask_login import login_required from app.main import main +from app.main.forms import CreateKeyForm + + +@main.route("/services//documentation") +@login_required +def documentation(service_id): + return render_template('views/documentation.html', service_id=service_id) @main.route("/services//api-keys") @login_required def api_keys(service_id): - return render_template('views/api-keys.html', service_id=service_id) + return render_template( + 'views/api-keys.html', + service_id=service_id, + keys=[ + {'name': 'Test key 1', 'last_used': '12 January 2016, 10:01AM', 'id': 1}, + {'name': 'Test key 2', 'last_used': '12 January 2016, 9:50AM', 'id': 1}, + {'name': 'Test key 3', 'last_used': '12 January 2016, 9:49AM', 'id': 1}, + { + 'name': 'My first key', 'last_used': '25 December 2015, 09:49AM', 'id': 1, + 'revoked': '4 January 2016, 6:00PM' + } + ] + ) + + +@main.route("/services//api-keys/create", methods=['GET', 'POST']) +@login_required +def create_api_key(service_id): + form = CreateKeyForm() + if form.validate_on_submit(): + return redirect(url_for('.show_api_key', service_id=service_id)) + return render_template( + 'views/api-keys/create.html', + service_id=service_id, + key_name=form.key_name + ) + + +@main.route("/services//api-keys/show") +@login_required +def show_api_key(service_id): + return render_template('views/api-keys/show.html', service_id=service_id) + + +@main.route("/services//api-keys/revoke/", methods=['GET', 'POST']) +@login_required +def revoke_api_key(service_id, key_id): + if request.method == 'GET': + return render_template('views/api-keys/revoke.html', service_id=service_id) + elif request.method == 'POST': + flash('‘Test key 1’ was revoked') + return redirect(url_for('.api_keys', service_id=service_id)) diff --git a/app/templates/components/api-key.html b/app/templates/components/api-key.html index 014136bc4..2a2eb2525 100644 --- a/app/templates/components/api-key.html +++ b/app/templates/components/api-key.html @@ -1,4 +1,7 @@ -{% macro api_key(key) %} +{% macro api_key(key, name) %} +

+ {{ name }} +

{{ key }}
diff --git a/app/templates/components/table.html b/app/templates/components/table.html index fb41a6aa7..b5fe9e9d2 100644 --- a/app/templates/components/table.html +++ b/app/templates/components/table.html @@ -55,3 +55,7 @@ {% macro right_aligned_field_heading(text) %} {{ text }} {%- endmacro %} + +{% macro hidden_field_heading(text) %} + {{ text }} +{%- endmacro %} diff --git a/app/templates/main_nav.html b/app/templates/main_nav.html index 65cc28c57..d78cdd3ca 100644 --- a/app/templates/main_nav.html +++ b/app/templates/main_nav.html @@ -9,7 +9,8 @@
  • Templates
    • Manage users
    • diff --git a/app/templates/views/api-keys.html b/app/templates/views/api-keys.html index 7cc837971..601deead0 100644 --- a/app/templates/views/api-keys.html +++ b/app/templates/views/api-keys.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} -{% from "components/page-footer.html" import page_footer %} -{% from "components/api-key.html" import api_key %} +{% from "components/table.html" import list_table, field, hidden_field_heading %} {% block page_title %} GOV.UK Notify | API keys and documentation @@ -8,49 +7,55 @@ {% block maincolumn_content %} -
      -
      +

      + API keys +

      -

      - API keys and documentation -

      +

      + To connect to the API you will need to send your service ID, encrypted with + an API key. The API key stays secret. +

      -

      - How to integrate GOV.UK Notify into your service -

      +

      + There are client libraries for several languages which manage this for + you. See + the + developer documentation for more information. +

      -

      - blah blah blah this is where we tell you how the API works -

      +

      + Service ID +

      +

      + {{ service_id }} +

      -

      Repositories

      + {% call(item) list_table( + keys, + empty_message="You haven’t created any API keys yet", + caption="API keys", + caption_visible=False, + field_headings=['Key name', 'Created at', hidden_field_heading('Action')] + ) %} + {% call field() %} + {{ item.name }} + {% endcall %} + {% call field() %} + {{ item.last_used }} + {% endcall %} + {% if item.revoked %} + {% call field(align='right', status='default') %} + Revoked {{ item.revoked }} + {% endcall %} + {% else %} + {% call field(align='right', status='error') %} + Revoke + {% endcall %} + {% endif %} + {% endcall %} - - -

      - GOV.UK Notify API -

      - -

      - GOV.UK Notify Python client -

      - -

      API key for [service name]

      - - {{ api_key('d30512af92e1386d63b90e5973b49a10') }} - -

      API endpoint

      - -

      - https://www.notify.works/api/endpoint -

      - -
      -
      - - {{ page_footer( - back_link=url_for('.service_dashboard', service_id=service_id), - back_link_text='Back to dashboard' - ) }} +

      + Create a new API key +

      {% endblock %} diff --git a/app/templates/views/api-keys/create.html b/app/templates/views/api-keys/create.html new file mode 100644 index 000000000..570a54194 --- /dev/null +++ b/app/templates/views/api-keys/create.html @@ -0,0 +1,24 @@ +{% extends "withnav_template.html" %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/textbox.html" import textbox %} + +{% block page_title %} + GOV.UK Notify | API keys and documentation +{% endblock %} + +{% block maincolumn_content %} + +

      + Add a new API key +

      + +
      + {{ textbox(key_name, hint='eg CRM application') }} + {{ page_footer( + 'Continue', + back_link=url_for('.api_keys', service_id=service_id), + back_link_text='Back to API keys' + ) }} +
      + +{% endblock %} diff --git a/app/templates/views/api-keys/revoke.html b/app/templates/views/api-keys/revoke.html new file mode 100644 index 000000000..3597a6270 --- /dev/null +++ b/app/templates/views/api-keys/revoke.html @@ -0,0 +1,37 @@ +{% extends "withnav_template.html" %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/api-key.html" import api_key %} + +{% block page_title %} + GOV.UK Notify | API keys and documentation +{% endblock %} + +{% block maincolumn_content %} + +
      +
      + +

      + Revoke API key +

      + +

      + ‘Test key 1’ will no longer let you connect to GOV.UK Notify. +

      +

      + You can’t undo this. +

      + +
      +
      + +
      + {{ page_footer( + 'Revoke this API key', + back_link=url_for('.api_keys', service_id=service_id), + back_link_text='Back to API keys', + destructive=True + ) }} +
      + +{% endblock %} diff --git a/app/templates/views/api-keys/show.html b/app/templates/views/api-keys/show.html new file mode 100644 index 000000000..fca59852d --- /dev/null +++ b/app/templates/views/api-keys/show.html @@ -0,0 +1,33 @@ +{% extends "withnav_template.html" %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/api-key.html" import api_key %} + +{% block page_title %} + GOV.UK Notify | API keys and documentation +{% endblock %} + +{% block maincolumn_content %} + +
      +
      + +

      + New API key +

      + +

      + Copy your key to somewhere safe. You won’t be able to see it again + once you leave this page. +

      + + {{ api_key('d30512af92e1386d63b90e5973b49a10', 'CRM application') }} + +
      +
      + + {{ page_footer( + back_link=url_for('.api_keys', service_id=service_id), + back_link_text='Back to API keys' + ) }} + +{% endblock %} diff --git a/app/templates/views/documentation.html b/app/templates/views/documentation.html new file mode 100644 index 000000000..944c84b5c --- /dev/null +++ b/app/templates/views/documentation.html @@ -0,0 +1,49 @@ +{% extends "withnav_template.html" %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/api-key.html" import api_key %} + +{% block page_title %} + GOV.UK Notify | API keys and documentation +{% endblock %} + +{% block maincolumn_content %} + +
      +
      + +

      + Developer documentation +

      + +

      + How to integrate GOV.UK Notify into your service +

      + +

      + blah blah blah this is where we tell you how the API works +

      + +

      Repositories

      + +

      + GOV.UK Notify API +

      + +

      + GOV.UK Notify Python client +

      + +

      API endpoint

      + +

      + https://www.notify.works/api/endpoint +

      + +

      + API keys for your service +

      + +
      +
      + +{% endblock %} diff --git a/tests/app/main/views/test_api_keys.py b/tests/app/main/views/test_api_keys.py index 4b863f889..1e274486d 100644 --- a/tests/app/main/views/test_api_keys.py +++ b/tests/app/main/views/test_api_keys.py @@ -1,13 +1,87 @@ from flask import url_for -def test_should_show_api_keys_and_documentation_page(app_, - db_, - db_session, - active_user): +def test_should_show_documentation_page(app_, + db_, + db_session, + active_user): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(active_user) + response = client.get(url_for('main.documentation', service_id=123)) + + assert response.status_code == 200 + + +def test_should_show_api_keys_page(app_, + db_, + db_session, + active_user): with app_.test_request_context(): with app_.test_client() as client: client.login(active_user) response = client.get(url_for('main.api_keys', service_id=123)) assert response.status_code == 200 + + +def test_should_show_name_api_key_page(app_, + db_, + db_session, + active_user): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(active_user) + response = client.get(url_for('main.create_api_key', service_id=123)) + + assert response.status_code == 200 + + +def test_should_redirect_to_new_api_key(app_, + db_, + db_session, + active_user): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(active_user) + response = client.post(url_for('main.create_api_key', service_id=123)) + + assert response.status_code == 302 + assert response.location == url_for('main.show_api_key', service_id=123, _external=True) + + +def test_should_show_new_api_key(app_, + db_, + db_session, + active_user): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(active_user) + response = client.get(url_for('main.show_api_key', service_id=123)) + + assert response.status_code == 200 + + +def test_should_show_confirm_revoke_api_key(app_, + db_, + db_session, + active_user): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(active_user) + response = client.get(url_for('main.revoke_api_key', service_id=123, key_id=321)) + + assert response.status_code == 200 + + +def test_should_redirect_after_revoking_api_key(app_, + db_, + db_session, + active_user): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(active_user) + response = client.post(url_for('main.revoke_api_key', service_id=123, key_id=321)) + + assert response.status_code == 302 + assert response.location == url_for('.api_keys', service_id=123, _external=True)