diff --git a/api/tests.py b/api/tests.py
index 483cf28300763493a739c594f28f991616e4f38a..9803a6cd92dd434e449a9bb736ba0c2a0067a11c 100644
--- a/api/tests.py
+++ b/api/tests.py
@@ -1,5 +1,7 @@
 import typing
 
+import unittest
+
 from django.contrib.auth import get_user_model
 from django.test import Client, TestCase
 
@@ -554,6 +556,8 @@ class DataSourceApiHyperCatTest(TestCase):
         self.assertLessEqual(1, len(data['data']))
         # TODO test contents of 'data' list
 
+    # CityVerve API is discontinued
+    @unittest.expectedFailure
     def test_api_datasource_get_dataset_metadata(self):
         """
         Test the :class:`DataSource` API functionality to retrieve dataset metadata.
@@ -569,6 +573,8 @@ class DataSourceApiHyperCatTest(TestCase):
         self.assertLessEqual(1, len(data['data']))
         # TODO test contents of 'data' list
 
+    # CityVerve API is discontinued
+    @unittest.expectedFailure
     def test_api_datasource_get_dataset_data(self):
         """
         Test the :class:`DataSource` API functionality to retrieve dataset data.
diff --git a/api/views/datasources.py b/api/views/datasources.py
index 0061d50c7344ccdc9d266e68bb47023ebc58bf7f..1804a6da47d915b5b9184f678b49a45b961f300a 100644
--- a/api/views/datasources.py
+++ b/api/views/datasources.py
@@ -8,7 +8,9 @@ import typing
 
 from django.db.models import ObjectDoesNotExist
 from django.http import HttpResponse, JsonResponse
+
 from rest_framework import decorators, request, response, viewsets
+from requests.exceptions import HTTPError
 
 from .. import permissions
 from datasources import models, serializers
@@ -20,28 +22,28 @@ class DataSourceApiViewset(viewsets.ReadOnlyModelViewSet):
     Provides views for:
 
     /api/datasources/
-      List all :class:`DataSource`s
+      List all :class:`datasources.models.DataSource`\ s.
 
     /api/datasources/<int>/
-      Retrieve a single :class:`DataSource`
+      Retrieve a single :class:`datasources.models.DataSource`.
 
     /api/datasources/<int>/prov/
-      Retrieve PROV records related to a :class:`DataSource`
+      Retrieve PROV records related to a :class:`datasources.models.DataSource`.
 
     /api/datasources/<int>/metadata/
-      Retrieve :class:`DataSource` metadata via API call to data source URL
+      Retrieve :class:`datasources.models.DataSource` metadata via API call to data source URL.
 
     /api/datasources/<int>/data/
-      Retrieve :class:`DataSource` data via API call to data source URL
+      Retrieve :class:`datasources.models.DataSource` data via API call to data source URL.
 
     /api/datasources/<int>/datasets/
-      Retrieve :class:`DataSource` list of data sets via API call to data source URL
+      Retrieve :class:`datasources.models.DataSource` list of data sets via API call to data source URL.
 
     /api/datasources/<int>/datasets/<href>/metadata/
-      Retrieve :class:`DataSource` metadata for a single dataset via API call to data source URL
+      Retrieve :class:`datasources.models.DataSource` metadata for a single dataset via API call to data source URL.
 
     /api/datasources/<int>/datasets/<href>/metadata/
-      Retrieve :class:`DataSource` data for a single dataset via API call to data source URL
+      Retrieve :class:`datasources.models.DataSource` data for a single dataset via API call to data source URL.
     """
     queryset = models.DataSource.objects.all()
     serializer_class = serializers.DataSourceSerializer
@@ -134,6 +136,10 @@ class DataSourceApiViewset(viewsets.ReadOnlyModelViewSet):
                 }
                 return response.Response(data, status=400)
 
+            except HTTPError as e:
+                # Pass upstream errors through
+                return response.Response(e.response.text, status=e.response.status_code)
+
     def list(self, request, *args, **kwargs):
         """
         List the queryset after filtering by request query parameters for data source metadata.
diff --git a/applications/admin.py b/applications/admin.py
index 9f0ea3ec04cf84fbc0d8355c69300cada23996a3..107e3f2c4e8ff4af5d2d66f4a1b7e0e4b620366b 100644
--- a/applications/admin.py
+++ b/applications/admin.py
@@ -14,7 +14,7 @@ class ApplicationAdmin(admin.ModelAdmin):
         permission = super().has_change_permission(request, obj)
 
         if obj is not None:
-            permission &= obj.owner == request.user
+            permission &= (obj.owner == request.user) or request.user.is_superuser
 
         return permission
 
@@ -25,7 +25,7 @@ class ApplicationAdmin(admin.ModelAdmin):
         permission = super().has_delete_permission(request, obj)
 
         if obj is not None:
-            permission &= obj.owner == request.user
+            permission &= (obj.owner == request.user) or request.user.is_superuser
 
         return permission
 
diff --git a/applications/models.py b/applications/models.py
index 33fc2eca58c5c78c0dce1483ae5f9d81a5526f34..fa32ec28188fb954d46b70325d62bfbb3d8c6d37 100644
--- a/applications/models.py
+++ b/applications/models.py
@@ -1,3 +1,5 @@
+from uuid import uuid4
+
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from django.conf import settings
@@ -127,7 +129,8 @@ class Application(ProvAbleModel, ProvApplicationModel, BaseAppDataModel):
         if self.proxy_user:
             return self.proxy_user
 
-        proxy_username = 'application-proxy-' + slugify(self.name)
+        # Add random UUID to username to allow multiple applications with the same name
+        proxy_username = 'application-proxy-' + slugify(self.name) + str(uuid4())
         proxy_user = get_user_model().objects.create_user(proxy_username)
 
         # Create an API access token for the proxy user
diff --git a/applications/templates/applications/application/detail.html b/applications/templates/applications/application/detail.html
index cdeb12b3c087f3c5f279c11293ddb22e935c3093..37a95c90d09d0ad111a93e0909f5fa4c8beb7651 100644
--- a/applications/templates/applications/application/detail.html
+++ b/applications/templates/applications/application/detail.html
@@ -1,6 +1,10 @@
 {% extends "base.html" %}
 {% load bootstrap4 %}
 
+{% block extra_head %}
+    <script src="https://cdn.jsdelivr.net/npm/js-cookie@2.2.0/src/js.cookie.min.js"></script>
+{% endblock %}
+
 {% block content %}
     <nav aria-label="breadcrumb">
         <ol class="breadcrumb">
@@ -62,28 +66,51 @@
                 <tr>
                     <td>API Key</td>
                     <td>
-                        <span id="spanApiToken">
-                            {% if application.proxy_user.auth_token %}
-                                {{ application.proxy_user.auth_token }}
+                        <script type="application/javascript">
+                            function getToken() {
+                                $.ajax({
+                                    dataType: "json",
+                                    url: "{% url 'applications:token' pk=application.pk %}",
+                                    data: null,
+                                    success: function (data) {
+                                        $('#spanApiToken').text(data.data.token.key);
+
+                                        document.getElementById("spanApiTokenPresent").style.display = "inline";
+                                        document.getElementById("spanApiTokenAbsent").style.display = "none";
+                                    }
+                                });
+                            }
 
-                            {% else %}
-                                <script type="application/javascript">
-                                    function getToken() {
-                                        $.ajax({
-                                            dataType: "json",
-                                            url: "{% url 'applications:token' pk=application.pk %}",
-                                            data: null,
-                                            success: function (data) {
-                                                $('#spanApiToken').text(data.data.token.key);
-                                            }
-                                        });
+                            function revokeToken() {
+                                $.ajax({
+                                    dataType: "json",
+                                    url: "{% url 'applications:token' pk=application.pk %}",
+                                    method: "DELETE",
+                                    headers: {
+                                        "X-CSRFToken": Cookies.get("csrftoken")
+                                    },
+                                    data: null,
+                                    success: function (data) {
+                                        $('#spanApiToken').text("");
+
+                                        document.getElementById("spanApiTokenPresent").style.display = "none";
+                                        document.getElementById("spanApiTokenAbsent").style.display = "inline";
                                     }
-                                </script>
+                                });
+                            }
+                        </script>
+
+                        <span id="spanApiTokenPresent" {% if not application.proxy_user.auth_token %}style="display: none;"{% endif %}>
+                            <span id="spanApiToken">
+                                {{ application.proxy_user.auth_token }}
+                            </span>
 
-                                <button onclick="getToken();" class="btn btn-default" role="button">Generate an API Token</button>
-                            {% endif %}
+                            <button onclick="revokeToken();" class="btn btn-danger" role="button">Revoke API Token</button>
                         </span>
 
+                        <span id="spanApiTokenAbsent" {% if application.proxy_user.auth_token %}style="display: none;"{% endif %}>
+                                <button onclick="getToken();" class="btn btn-default" role="button">Generate API Token</button>
+                        </span>
                     </td>
                 </tr>
             {% endif %}
diff --git a/applications/urls.py b/applications/urls.py
index ae74341444ed0fe0599bced8c8622946e25c3bb7..731701cb8c9d654fd7bbc4b44d75644b800e095a 100644
--- a/applications/urls.py
+++ b/applications/urls.py
@@ -26,7 +26,7 @@ urlpatterns = [
          name='application.delete'),
 
     path('<int:pk>/token',
-         views.ApplicationGetTokenView.as_view(),
+         views.ApplicationManageTokenView.as_view(),
          name='token'),
 
     path('<int:pk>/manage-access',
diff --git a/applications/views.py b/applications/views.py
index 82565962e6a28573605c547ee5bdbbcaf3515d44..046a64136ede982dc98da345540d286688af30ee 100644
--- a/applications/views.py
+++ b/applications/views.py
@@ -1,3 +1,7 @@
+"""
+Views to manage and access :class:`Application`s.
+"""
+
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.http import JsonResponse
 from django.urls import reverse_lazy
@@ -42,6 +46,7 @@ class ApplicationUpdateView(OwnerPermissionMixin, UpdateView):
     context_object_name = 'application'
 
     fields = '__all__'
+    permission_required = 'applications.change_application'
 
 
 class ApplicationDeleteView(OwnerPermissionMixin, DeleteView):
@@ -49,6 +54,7 @@ class ApplicationDeleteView(OwnerPermissionMixin, DeleteView):
     template_name = 'applications/application/delete.html'
     context_object_name = 'application'
 
+    permission_required = 'applications.delete_application'
     success_url = reverse_lazy('applications:application.list')
 
 
@@ -90,15 +96,15 @@ class ApplicationManageAccessView(OwnerPermissionMixin, ManageAccessView):
     context_object_name = 'application'
 
 
-class ApplicationGetTokenView(OwnerPermissionMixin, DetailView):
+class ApplicationManageTokenView(OwnerPermissionMixin, DetailView):
     """
-    Get an API Token for an application.
+    Manage an API Token for an application.
     """
     model = models.Application
 
     def render_to_response(self, context, **response_kwargs):
         """
-        Get an existing API Token or create a new one for the currently authenticated user.
+        Get an existing API Token or create a new one for the requested :class:`Application`.
 
         :return: JSON containing Token key
         """
@@ -112,3 +118,17 @@ class ApplicationGetTokenView(OwnerPermissionMixin, DetailView):
                 }
             }
         })
+
+    def delete(self, request, *args, **kwargs):
+        """
+        Revoke an API Token for the requested :class:`Application`.
+        """
+        self.object = self.get_object()
+        self.object.proxy_user.revoke_auth_token()
+
+        return JsonResponse({
+            'status': 'success',
+            'data': {
+                'token': None,
+            }
+        })
diff --git a/datasources/admin.py b/datasources/admin.py
index f98162f754fafea55bc3d7fccae6ec20095a71e6..c2419c0629b2ba98784c3ecfee0fcd8309da7548 100644
--- a/datasources/admin.py
+++ b/datasources/admin.py
@@ -20,7 +20,7 @@ class DataSourceAdmin(admin.ModelAdmin):
         permission = super().has_change_permission(request, obj)
 
         if obj is not None:
-            permission &= obj.owner == request.user
+            permission &= (obj.owner == request.user) or request.user.is_superuser
 
         return permission
 
@@ -31,7 +31,7 @@ class DataSourceAdmin(admin.ModelAdmin):
         permission = super().has_delete_permission(request, obj)
 
         if obj is not None:
-            permission &= obj.owner == request.user
+            permission &= (obj.owner == request.user) or request.user.is_superuser
 
         return permission
 
diff --git a/datasources/templates/datasources/datasource/detail.html b/datasources/templates/datasources/datasource/detail.html
index 67152ed7bf0d97367a3d78e7d2d64636e39c01e5..d2a4c823b24511ab37f10a27af890603d72f5564 100644
--- a/datasources/templates/datasources/datasource/detail.html
+++ b/datasources/templates/datasources/datasource/detail.html
@@ -104,12 +104,16 @@
                 </td>
             </tr>
             <tr>
-                <td>URL</td>
+                <td>Licence</td>
+                <td>{{ datasource.licence.name }}</td>
+            </tr>
+            <tr>
+                <td>External API URL</td>
                 <td>{{ datasource.url }}</td>
             </tr>
             <tr>
-                <td>Licence</td>
-                <td>{{ datasource.licence.name }}</td>
+                <td>Pedasi API URL</td>
+                <td>{{ api_url }}</td>
             </tr>
         </tbody>
     </table>
diff --git a/datasources/templates/datasources/datasource/explorer.html b/datasources/templates/datasources/datasource/explorer.html
index dff92cde41b694e4db184265fc5f24fffcc0bde9..1f215b0da83fefd809a818e7f179afba92cc1bf2 100644
--- a/datasources/templates/datasources/datasource/explorer.html
+++ b/datasources/templates/datasources/datasource/explorer.html
@@ -156,7 +156,7 @@
             </form>
 
             <div class="alert alert-info w-100">
-                Query URL: /api/datasources/{{ datasource.pk }}/<span id="datasetUrlSpan"></span>data/?<span id="queryParamSpan"></span>
+                Query URL: {{ api_url }}<span id="datasetUrlSpan"></span>data/?<span id="queryParamSpan"></span>
             </div>
 
             <table class="table" id="tableParams">
diff --git a/datasources/templates/datasources/datasource/manage_access.html b/datasources/templates/datasources/datasource/manage_access.html
index 11ce9bdce97f4cf53ac318d7aa07e6c5467234ee..014e49572c01d74888b46ab1054a46c84e5ae9cf 100644
--- a/datasources/templates/datasources/datasource/manage_access.html
+++ b/datasources/templates/datasources/datasource/manage_access.html
@@ -47,7 +47,13 @@
                 <tr id="requested-user-{{ permission.user.pk }}">
                     <td>
                         <p>
-                            {{ permission.user.username }}
+                            {% if permission.user.application_proxy %}
+                                {{ permission.user.application_proxy.name }}
+                                <a href="{% url 'applications:application.detail' pk=permission.user.application_proxy.pk %}"
+                                   role="button" class="badge badge-info">App</a>
+                            {% else %}
+                                {{ permission.user.username }}
+                            {% endif %}
                         </p>
                         {% if permission.reason %}
                             <div class="alert alert-secondary" role="note">
@@ -105,7 +111,15 @@
         <tbody>
         {% for permission in permissions_granted %}
             <tr id="approved-user-{{ permission.user.pk }}">
-                <td>{{ permission.user.username }}</td>
+                <td>
+                    {% if permission.user.application_proxy %}
+                        {{ permission.user.application_proxy.name }}
+                        <a href="{% url 'applications:application.detail' pk=permission.user.application_proxy.pk %}"
+                           role="button" class="badge badge-info">App</a>
+                    {% else %}
+                        {{ permission.user.username }}
+                    {% endif %}
+                </td>
                 <td>{{ permission.get_requested_display }}</td>
                 <td>{{ permission.get_granted_display }}</td>
                 <td>
diff --git a/datasources/tests/test_connectors_hypercat.py b/datasources/tests/test_connectors_hypercat.py
index 8c25b63c3c20f540a2cb1075f30c5d0d81113d7d..67825675da044d6449648cfaf89e3578e3bb5939 100644
--- a/datasources/tests/test_connectors_hypercat.py
+++ b/datasources/tests/test_connectors_hypercat.py
@@ -1,5 +1,6 @@
 import itertools
 import typing
+import unittest
 
 from django.test import TestCase
 
@@ -157,6 +158,8 @@ class ConnectorHyperCatTest(TestCase):
                 self.assertEqual(k,
                                  v.location)
 
+    # CityVerve API is discontinued
+    @unittest.expectedFailure
     def test_plugin_get_subcatalogue_metadata(self):
         connection = self._get_connection()
 
@@ -179,6 +182,8 @@ class ConnectorHyperCatTest(TestCase):
         self.assertEqual('https://developer.cityverve.org.uk',
                          _get_item_by_key_value(result, 'rel', 'urn:X-hypercat:rels:hasHomepage')['val'])
 
+    # CityVerve API is discontinued
+    @unittest.expectedFailure
     def test_plugin_get_subcatalogue_datasets(self):
         connection = self._get_connection()
 
@@ -200,6 +205,8 @@ class ConnectorHyperCatTest(TestCase):
         for exp in expected:
             self.assertIn(exp, datasets)
 
+    # CityVerve API is discontinued
+    @unittest.expectedFailure
     def test_plugin_get_subcatalogue_dataset_metadata(self):
         connection = self._get_connection()
 
@@ -228,6 +235,8 @@ class ConnectorHyperCatTest(TestCase):
         self.assertEqual('polling-station',
                          _get_item_by_key_value(result, 'rel', 'urn:X-cityverve:rels:type')['val'])
 
+    # CityVerve API is discontinued
+    @unittest.expectedFailure
     def test_plugin_get_subcatalogue_dataset_data(self):
         connection = self._get_connection()
 
diff --git a/datasources/views/datasource.py b/datasources/views/datasource.py
index dbc90b468aa9ef7953e76ac2fd5dd1ecd261dc01..1ea6417e764043f439af276d276805c6458c6426 100644
--- a/datasources/views/datasource.py
+++ b/datasources/views/datasource.py
@@ -44,6 +44,12 @@ class DataSourceDetailView(DetailView):
         except (KeyError, ValueError):
             messages.error(self.request, 'This data source is not configured correctly.  Please notify the owner.')
 
+        context['api_url'] = (
+            'https://' if self.request.is_secure() else 'http://' +
+            self.request.get_host() +
+            '/api/datasources/{0}/'.format(self.object.pk)
+        )
+
         return context
 
 
@@ -71,6 +77,7 @@ class DataSourceUpdateView(OwnerPermissionMixin, UpdateView):
     context_object_name = 'datasource'
 
     form_class = forms.DataSourceForm
+    permission_required = 'datasources.change_datasource'
 
 
 class DataSourceDeleteView(OwnerPermissionMixin, DeleteView):
@@ -78,6 +85,7 @@ class DataSourceDeleteView(OwnerPermissionMixin, DeleteView):
     template_name = 'datasources/datasource/delete.html'
     context_object_name = 'datasource'
 
+    permission_required = 'datasources.delete_datasource'
     success_url = reverse_lazy('datasources:datasource.list')
 
 
@@ -198,4 +206,10 @@ class DataSourceExplorerView(HasPermissionLevelMixin, DetailView):
             field__short_name='data_query_param'
         ).values_list('value', flat=True)
 
+        context['api_url'] = (
+            'https://' if self.request.is_secure() else 'http://' +
+                                                        self.request.get_host() +
+                                                        '/api/datasources/{0}/'.format(self.object.pk)
+        )
+
         return context
diff --git a/deploy/nginx/sites-available/pedasi b/deploy/nginx/sites-available/pedasi
index 6d405e53bf3b4e4866a20db25afd339d73de162f..399490107c8da3a36966a93c8b0439bab0f91bf1 100644
--- a/deploy/nginx/sites-available/pedasi
+++ b/deploy/nginx/sites-available/pedasi
@@ -1,8 +1,9 @@
 server {
-    listen 80;
-    server_name localhost pedasi.* pedasi-dev.* *.iotobservatory.io;
+    listen 80 default_server;
+    server_name _;
 
     merge_slashes off;
+    client_max_body_size 100m;
 
     location /favicon.ico {
         alias /var/www/pedasi/static/img/favicon.ico;
@@ -13,11 +14,15 @@ server {
     }
 
     location = /report.html {
-        file /var/www/pedasi/report.html;
+        alias /var/www/pedasi/report.html;
         auth_basic "Restricted Content";
         auth_basic_user_file /etc/nginx/.htpasswd;
     }
 
+    location = /robots.txt {
+        alias /var/www/pedasi/deploy/robots.txt;
+    }
+
     location / {
         include     uwsgi_params;
         uwsgi_pass  unix:/run/uwsgi/pedasi.sock;
diff --git a/deploy/nginx/sites-available/pedasi.dev b/deploy/nginx/sites-available/pedasi.dev
deleted file mode 100644
index e7a65daa508ee3282fc157c7a3bd830886fecf74..0000000000000000000000000000000000000000
--- a/deploy/nginx/sites-available/pedasi.dev
+++ /dev/null
@@ -1,24 +0,0 @@
-server {
-    listen 80;
-
-    merge_slashes off;
-
-    location = /favicon.ico {
-        alias /var/www/pedasi/static/img/favicon.ico;
-    }
-
-    location /static/ {
-        alias /var/www/pedasi/static/;
-    }
-
-    location = /report.html {
-        alias /var/www/pedasi/report.html;
-        auth_basic "Restricted Content";
-        auth_basic_user_file /etc/nginx/.htpasswd;
-    }
-
-    location / {
-        include     uwsgi_params;
-        uwsgi_pass  unix:/run/uwsgi/pedasi.sock;
-    }
-}
diff --git a/deploy/robots.txt b/deploy/robots.txt
new file mode 100644
index 0000000000000000000000000000000000000000..adc3595e0ed36ab274e1ea13c3a835751103faad
--- /dev/null
+++ b/deploy/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow: /admin/
+Disallow: /api/
diff --git a/pedasi/common/__init__.py b/pedasi/common/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/pedasi/settings.py b/pedasi/settings.py
index abac017ef80c43ed47d11f89bc128dbf3f38add2..c72f67d4d77dda75ca8191f42f61704e8cdb9d15 100644
--- a/pedasi/settings.py
+++ b/pedasi/settings.py
@@ -27,6 +27,9 @@ DEBUG
   Run the server in debug mode?
   Default is 'false'.
 
+ALLOWED_HOSTS - required if not in debug mode
+  List of hostnames on which the server is permitted to run
+
 DATABASE_URL
   URL to default SQL database - in `dj-database-url <https://github.com/kennethreitz/dj-database-url>`_ format.
   Default is SQLite3 'db.sqlite3' in project root directory.
@@ -42,7 +45,7 @@ import os
 
 from django.urls import reverse_lazy
 
-from decouple import config
+from decouple import config, Csv
 import dj_database_url
 import mongoengine
 
@@ -55,17 +58,9 @@ SECRET_KEY = config('SECRET_KEY')
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = config('DEBUG', default=False, cast=bool)
 
-if DEBUG:
-    ALLOWED_HOSTS = [
-        '*',
-    ]
-
-else:
-    ALLOWED_HOSTS = [
-        'localhost',
-        'pedasi-dev.eastus.cloudapp.azure.com',
-    ]
-
+ALLOWED_HOSTS = config('ALLOWED_HOSTS',
+                       cast=Csv(),
+                       default='*' if DEBUG else '127.0.0.1,localhost.localdomain,localhost')
 
 # Application definition
 
@@ -237,8 +232,8 @@ SOCIAL_AUTH_PIPELINE = [
     # 'social_core.pipeline.social_auth.associate_by_email',
 
     # Create a user account if we haven't found one yet.
-    'social_core.pipeline.user.create_user',
-    # 'profiles.social_auth.create_user_disabled',
+    # 'social_core.pipeline.user.create_user',
+    'profiles.social_auth.create_user_disabled',
 
     # Create the record that associates the social account with the user.
     'social_core.pipeline.social_auth.associate_user',
@@ -249,6 +244,9 @@ SOCIAL_AUTH_PIPELINE = [
 
     # Update the user record with any changed info from the auth service.
     'social_core.pipeline.user.user_details',
+
+    # Email admins to activate the account
+    'profiles.social_auth.email_admins',
 ]
 
 SOCIAL_AUTH_LOGIN_REDIRECT_URL = reverse_lazy('index')
@@ -316,3 +314,23 @@ STATICFILES_DIRS = [
     os.path.join(BASE_DIR, 'pedasi', 'static'),
     os.path.join(BASE_DIR, 'docs', 'build'),
 ]
+
+
+# Email provider for notification emails
+EMAIL_HOST = config('EMAIL_HOST', default=None)
+EMAIL_PORT = config('EMAIL_PORT', cast=int, default=25)
+
+EMAIL_HOST_USER = config('EMAIL_HOST_USER', default=None)
+EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default=None)
+DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default=EMAIL_HOST_USER)
+
+EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool, default=False)
+EMAIL_USE_SSL = config('EMAIL_USE_SSL', cast=bool, default=False)
+
+EMAIL_SUBJECT_PREFIX = '[PEDASI]'
+
+if DEBUG and not EMAIL_HOST:
+    EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
+    EMAIL_FILE_PATH = os.path.join(BASE_DIR, 'mail')
+
+ADMINS = [val.split('|') for val in config('ADMINS', cast=Csv(), default='')]
diff --git a/playbook.yml b/playbook.yml
index 74a342fcdc54335d494c5526a6c1e7196fea0f9f..b3345535366f688603832879bc15d23a88d4becd 100644
--- a/playbook.yml
+++ b/playbook.yml
@@ -46,7 +46,7 @@
           - python3
           - python3-dev
           - python3-pip
-          - python3-venv
+          - python-virtualenv
           - git
           - mysql-server
           - libmysqlclient-dev
@@ -57,83 +57,43 @@
           # Required for Ansible to setup DB
           - python3-mysqldb
 
-    - name: Setup dev deployment
-      block:
-      - name: Check if running under Vagrant
-        stat:
-          path: /vagrant
-        register: vagrant_dir
-
-      - name: Clone / update from local repo
-        git:
-          repo: '/vagrant'
-          dest: '{{ project_dir }}'
-        when: vagrant_dir.stat.exists == True
-
-      - name: Copy deploy key
-        copy:
-          src: 'deploy/.deployment-key'
-          dest: '~/.deployment-key'
-          mode: 0600
-        when: vagrant_dir.stat.exists == False
-
-      - name: Clone / update dev branch from main repo
-        git:
-          repo: 'ssh://git@github.com/PEDASI/PEDASI.git'
-          dest: '{{ project_dir }}'
-          accept_hostkey: yes
-          key_file: '~/.deployment-key'
-          version: dev
-        when: vagrant_dir.stat.exists == False
-
-      - name: Copy dev settings
-        copy:
-          src: 'deploy/.env.dev'
-          dest: '{{ project_dir }}/.env'
-          owner: www-data
-          group: www-data
-          mode: 0600
-
-      when: production is not defined
-
-    - name: Setup production deployment
-      block:
-      - name: Copy deploy key
-        copy:
-          src: 'deploy/.deployment-key'
-          dest: '~/.deployment-key'
-          mode: 0600
-
-      - name: Clone / update master branch
-        git:
-          repo: 'ssh://git@github.com/PEDASI/PEDASI.git'
-          dest: '{{ project_dir }}'
-          accept_hostkey: yes
-          key_file: '~/.deployment-key'
-          version: master
-
-      - name: Copy production settings
-        copy:
-          src: 'deploy/.env.prod'
-          dest: '{{ project_dir }}/.env'
-          owner: www-data
-          group: www-data
-          mode: 0600
-
-      when: production is defined
+    - name: Check if running under Vagrant
+      stat:
+        path: /vagrant
+      register: vagrant_dir
+
+    - name: Clone / update from local repo
+      git:
+        repo: '/vagrant'
+        dest: '{{ project_dir }}'
+      when: vagrant_dir.stat.exists == True
+
+    - name: Clone / update branch from main repo
+      git:
+        repo: 'https://github.com/PEDASI/PEDASI.git'
+        dest: '{{ project_dir }}'
+        accept_hostkey: yes
+        version: '{{ branch | default("dev") }}'
+      when: vagrant_dir.stat.exists == False
+
+    - name: Copy settings
+      copy:
+        src: '{{ env_file | default("deploy/.env") }}'
+        dest: '{{ project_dir }}/.env'
+        owner: www-data
+        group: www-data
+        mode: 0600
 
     - name: Set permissions on manage.py
       file:
         path: '{{ project_dir }}/manage.py'
         mode: 0755
 
-    - name: Create virtualenv
-      command: python3 -m venv '{{ venv_dir }}' creates='{{ venv_dir }}'
-
     - name: Install pip requirements
       pip:
         requirements: '{{ project_dir}}/requirements.txt'
         virtualenv: '{{ venv_dir }}'
+        virtualenv_python: python3
 
     - name: Restart and enable MariaDB
       systemd:
@@ -195,23 +155,18 @@
         - { src: '{{ project_dir }}/deploy/uwsgi/sites/pedasi.ini', dest: /etc/uwsgi/sites/pedasi.ini }
         - { src: '{{ project_dir }}/deploy/systemd/system/uwsgi.service', dest: /etc/systemd/system/uwsgi.service }
 
-    - name: Copy web config files - dev
-      copy:
-        src: '{{ project_dir }}/deploy/nginx/sites-available/pedasi.dev'
-        dest: /etc/nginx/sites-available/pedasi
-        remote_src: yes
-        # WARNING: this will not update an existing file
-        force: no
-      when: production is not defined
+    - name: Deactivate default Nginx site
+      file:
+        path: /etc/nginx/sites-enabled/default
+        state: absent
 
-    - name: Copy web config files - production
+    - name: Copy web config files
       copy:
         src: '{{ project_dir }}/deploy/nginx/sites-available/pedasi'
         dest: /etc/nginx/sites-available/pedasi
         remote_src: yes
         # WARNING: this will not update an existing file
         force: no
-      when: production is defined
 
     - name: Activate Nginx site
       file:
@@ -279,7 +234,6 @@
           SPHINXAPIDOC: '{{ venv_dir }}/bin/sphinx-apidoc'
       loop:
       - clean
-      - apidoc
       - html
 
     - name: Django collect static files
diff --git a/profiles/models.py b/profiles/models.py
index 1ef55900baa1e8ce943b830059f4d71c76841dc7..738fa71aaca9294d9327cb4476253935c4d89972 100644
--- a/profiles/models.py
+++ b/profiles/models.py
@@ -28,3 +28,10 @@ class User(AbstractUser):
         """
         token, created = Token.objects.get_or_create(user=self)
         return token
+
+    def revoke_auth_token(self):
+        """
+        Revoke and API auth token for this user.
+        """
+        self.auth_token.delete()
+
diff --git a/profiles/permissions.py b/profiles/permissions.py
index 5c5d5a4e82df6dd8131f0751a2338f4929be9b4e..ae136d39f06e519565322a5ebad7734ceb0b49fe 100644
--- a/profiles/permissions.py
+++ b/profiles/permissions.py
@@ -1,6 +1,21 @@
+"""
+Mixins for views to require certain permissions or ownership.
+"""
+
 from django.contrib.auth.mixins import UserPassesTestMixin, PermissionRequiredMixin
 
 
+class SelfOrAdminPermissionMixin(UserPassesTestMixin):
+    """
+    Mixin to require that a user is the linked object or an admin.
+
+    To be used e.g. for edit permission on user profiles
+    """
+    def test_func(self) -> bool:
+        user = self.get_object()
+        return user == self.request.user or self.request.user.is_superuser
+
+
 class OwnerPermissionMixin(PermissionRequiredMixin):
     """
     Mixin to require that a user has the relevant global permission and is the owner of the relevant object.
diff --git a/profiles/social_auth.py b/profiles/social_auth.py
index b4d320fc414d66500d14aa5320443f2a3174c39c..453f75f90a4d408059b6f123550622842d68cb99 100644
--- a/profiles/social_auth.py
+++ b/profiles/social_auth.py
@@ -1,7 +1,21 @@
+"""
+Module containing customisations to the Python Social Auth pipeline.
+
+See https://python-social-auth.readthedocs.io/en/latest/
+"""
+
+from django.core.mail import mail_admins
+
+
 from social_core.pipeline.user import create_user
 
 
 def create_user_disabled(strategy, details, backend, user=None, *args, **kwargs):
+    """
+    Create a user account for the user being authenticated - but mark it as disabled.
+
+    A new user must have their account enabled by an admin before they are able to log in.
+    """
     # Returns dict containing 'is_new' and 'user'
     result = create_user(strategy, details, backend, user, *args, **kwargs)
 
@@ -11,3 +25,21 @@ def create_user_disabled(strategy, details, backend, user=None, *args, **kwargs)
         django_user.save()
 
     return result
+
+
+def email_admins(strategy, details, backend, user=None, *args, **kwargs):
+    """
+    Email the PEDASI admins if a new account has been created and requires approval
+    """
+    if kwargs['is_new']:
+        mail_admins(
+            subject='PEDASI Account Created',
+            message=(
+                'New PEDASI user account: {0}\n\n'
+                'A new user account has been created by {1} - {2} and requires admin approval'
+            ).format(
+                user.username,
+                user.get_full_name(),
+                user.email
+            )
+        )
diff --git a/profiles/templates/profiles/user/profile.html b/profiles/templates/profiles/user/profile.html
index 52af26156d30256b035cd272c4b05353c51917e5..76999de7163e1ae0c17c3b120d8a43a44274cd5a 100644
--- a/profiles/templates/profiles/user/profile.html
+++ b/profiles/templates/profiles/user/profile.html
@@ -1,6 +1,10 @@
 {% extends "base.html" %}
 {% load bootstrap4 %}
 
+{% block extra_head %}
+    <script src="https://cdn.jsdelivr.net/npm/js-cookie@2.2.0/src/js.cookie.min.js"></script>
+{% endblock %}
+
 {% block content %}
     <nav aria-label="breadcrumb">
         <ol class="breadcrumb">
@@ -22,32 +26,58 @@
         </thead>
 
         <tbody>
-        <tr>
-            <td>API Token</td>
-            <td>
-                <span id="spanApiToken">
-                    {% if user == request.user and user.auth_token %}
-                        {{ user.auth_token }}
-
-                    {% else %}
+            {% if user == request.user or request.user.is_superuser %}
+                <tr>
+                    <td>API Token</td>
+                    <td>
                         <script type="application/javascript">
                             function getToken() {
                                 $.ajax({
                                     dataType: "json",
-                                    url: "{% url 'profiles:token' %}",
+                                    url: "{% url 'profiles:token' pk=user.pk %}",
                                     data: null,
                                     success: function (data) {
                                         $('#spanApiToken').text(data.data.token.key);
+
+                                        document.getElementById("spanApiTokenPresent").style.display = "inline";
+                                        document.getElementById("spanApiTokenAbsent").style.display = "none";
+                                    }
+                                });
+                            }
+
+                            function revokeToken() {
+                                $.ajax({
+                                    dataType: "json",
+                                    url: "{% url 'profiles:token' pk=user.pk %}",
+                                    method: "DELETE",
+                                    headers: {
+                                        "X-CSRFToken": Cookies.get("csrftoken")
+                                    },
+                                    data: null,
+                                    success: function (data) {
+                                        $('#spanApiToken').text("");
+
+                                        document.getElementById("spanApiTokenPresent").style.display = "none";
+                                        document.getElementById("spanApiTokenAbsent").style.display = "inline";
                                     }
                                 });
                             }
                         </script>
 
-                        <button onclick="getToken();" class="btn btn-default" role="button">Generate an API Token</button>
-                    {% endif %}
-                </span>
-            </td>
-        </tr>
+                        <span id="spanApiTokenPresent" {% if not user.auth_token %}style="display: none;"{% endif %}>
+                                    <span id="spanApiToken">
+                                        {{ user.auth_token }}
+                                    </span>
+
+                                    <button onclick="revokeToken();" class="btn btn-danger" role="button">Revoke API Token</button>
+                                </span>
+
+                        <span id="spanApiTokenAbsent" {% if user.auth_token %}style="display: none;"{% endif %}>
+                                        <button onclick="getToken();" class="btn btn-default" role="button">Generate API Token</button>
+                                </span>
+                    </td>
+                </tr>
+            {% endif %}
         </tbody>
     </table>
 
diff --git a/profiles/tests.py b/profiles/tests.py
index 75e3d2c60ce4d5b15446147a96409ce94083fc6a..1cc396ed84364e154c2c7126cfda4068db4545d7 100644
--- a/profiles/tests.py
+++ b/profiles/tests.py
@@ -44,7 +44,7 @@ class UserTest(TestCase):
             # User should not already have token
             token = Token.objects.get(user=user)
 
-        response = client.get(reverse('profiles:token'))
+        response = client.get(reverse('profiles:token', kwargs={'pk': user.pk}))
 
         self.assertEqual(200, response.status_code)
 
diff --git a/profiles/urls.py b/profiles/urls.py
index 1c71dc1268c0de04e2ea8bc075da22834d6a701f..d57bf5b141a9ef2eace08b2dffb7591b12e52b20 100644
--- a/profiles/urls.py
+++ b/profiles/urls.py
@@ -19,7 +19,7 @@ urlpatterns = [
          views.UserUriView.as_view(),
          name='uri'),
 
-    path('token',
-         views.UserGetTokenView.as_view(),
+    path('<int:pk>/token',
+         views.UserManageTokenView.as_view(),
          name='token'),
 ]
diff --git a/profiles/views.py b/profiles/views.py
index d6cca5d14ec6f69bedf3d8bdf925e267c71e425f..7546d381dbc9b421cd438eb5e9b693cde5668833 100644
--- a/profiles/views.py
+++ b/profiles/views.py
@@ -1,5 +1,8 @@
+"""
+Views to manage user profiles.
+"""
+
 from django.contrib.auth import get_user_model
-from django.contrib.auth.mixins import LoginRequiredMixin
 from django.http import JsonResponse
 from django.views.generic import TemplateView
 from django.views.generic.detail import DetailView
@@ -8,6 +11,7 @@ from rest_framework.authtoken.models import Token
 
 from applications.models import Application
 from datasources.models import DataSource
+from profiles.permissions import SelfOrAdminPermissionMixin
 
 
 class IndexView(TemplateView):
@@ -61,16 +65,15 @@ class UserInactiveView(TemplateView):
     template_name = 'profiles/user/inactive.html'
 
 
-class UserGetTokenView(LoginRequiredMixin, DetailView):
+class UserManageTokenView(SelfOrAdminPermissionMixin, DetailView):
     """
-    Get an API Token for the currently authenticated user.
+    Manage an API Token for the requested user.
     """
-    def get_object(self, queryset=None):
-        return self.request.user
+    model = get_user_model()
 
     def render_to_response(self, context, **response_kwargs):
         """
-        Get an existing API Token or create a new one for the currently authenticated user.
+        Get an existing API Token or create a new one for the requested user.
 
         :return: JSON containing Token key
         """
@@ -84,3 +87,17 @@ class UserGetTokenView(LoginRequiredMixin, DetailView):
                 }
             }
         })
+
+    def delete(self, request, *args, **kwargs):
+        """
+        Revoke an API Token for the requested user.
+        """
+        self.object = self.get_object()
+        self.object.revoke_auth_token()
+
+        return JsonResponse({
+            'status': 'success',
+            'data': {
+                'token': None,
+            }
+        })
diff --git a/provenance/models.py b/provenance/models.py
index e94d04867341e9af939cb6b1ac773bccb24be8ec..df9bd295f11f361b817c8da83f5502f4b19bae7a 100644
--- a/provenance/models.py
+++ b/provenance/models.py
@@ -203,8 +203,8 @@ class ProvWrapper(mongoengine.Document):
         """
         Get all :class:`ProvEntry` documents related to a particular Django model instance.
 
-        :param instance: Model instance for which to get all :class:`ProvEntry`s
-        :return: List of :class:`ProvEntry`s
+        :param instance: Model instance for which to get all :class:`ProvEntry`\ s
+        :return: List of :class:`ProvEntry`\ s
         """
         instance_type = ContentType.objects.get_for_model(instance)
 
diff --git a/requirements.txt b/requirements.txt
index baab955031f0d1fb0acb9a90179b7db8a63dfccd..4edb81020e50bcc916a03754f8089bb582e121e1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,7 @@ chardet==3.0.4
 decorator==4.3.0
 defusedxml==0.5.0
 dj-database-url==0.5.0
-Django==2.0.8
+Django==2.0.13
 django-bootstrap4==0.0.6
 django-cors-headers==2.4.0
 django-haystack==2.8.1
@@ -44,12 +44,12 @@ python-decouple==3.1
 python3-openid==3.1.0
 pytz==2018.5
 rdflib==4.2.2
-requests==2.19.1
+requests==2.21.0
 requests-oauthlib==1.0.0
 six==1.11.0
 snowballstemmer==1.2.1
-social-auth-app-django==3.0.0
-social-auth-core==2.0.0
+social-auth-app-django==3.1.0
+social-auth-core==3.1.0
 Sphinx==1.7.6
 sphinx-rtd-theme==0.4.2
 sphinxcontrib-apidoc==0.3.0