You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2022/10/20 22:27:00 UTC

[airavata-django-portal] 01/04: AIRAVATA-3647 Special display for unauthenticated errors, directing users to re-authenticate

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit faeb92a062f0e3b881f5ce7a8be23109d48f6539
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Oct 7 12:20:14 2022 -0400

    AIRAVATA-3647 Special display for unauthenticated errors, directing users to re-authenticate
---
 django_airavata/apps/api/exceptions.py             |  6 +++
 .../django_airavata_api/js/errors/ErrorUtils.js    | 19 +++++++++
 .../js/errors/UnhandledError.js                    |  6 +++
 .../js/errors/UnhandledErrorDispatcher.js          | 14 ++++++-
 .../django_airavata_api/js/utils/FetchUtils.js     |  4 +-
 .../common/js/components/NotificationsDisplay.vue  | 49 +++++++++++++++++-----
 6 files changed, 86 insertions(+), 12 deletions(-)

diff --git a/django_airavata/apps/api/exceptions.py b/django_airavata/apps/api/exceptions.py
index 7caf17c7..0676c126 100644
--- a/django_airavata/apps/api/exceptions.py
+++ b/django_airavata/apps/api/exceptions.py
@@ -4,6 +4,7 @@ from airavata.api.error.ttypes import AuthorizationException
 from django.core.exceptions import ObjectDoesNotExist
 from django.http import JsonResponse
 from rest_framework import status
+from rest_framework.exceptions import NotAuthenticated
 from rest_framework.response import Response
 from rest_framework.views import exception_handler
 from thrift.Thrift import TException
@@ -43,6 +44,11 @@ def custom_exception_handler(exc, context):
             {'detail': str(exc)},
             status=status.HTTP_404_NOT_FOUND)
 
+    if isinstance(exc, NotAuthenticated):
+        log.debug("NotAuthenticated", exc_info=exc)
+        if response is not None:
+            response.data['is_authenticated'] = False
+
     # Generic handler
     if response is None:
         log.error("API exception", exc_info=exc)
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js
index 78329930..b1213e42 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/errors/ErrorUtils.js
@@ -22,4 +22,23 @@ export default {
   isNotFoundError(error) {
     return this.isAPIException(error) && error.details.status === 404;
   },
+  isUnauthenticatedError(error) {
+    return (
+      this.isAPIException(error) &&
+      [401, 403].includes(error.details.status) &&
+      "is_authenticated" in error.details.response &&
+      error.details.response.is_authenticated === false
+    );
+  },
+  buildLoginUrl(includeNextParameter = true) {
+    let loginUrl = "/auth/login";
+    if (includeNextParameter) {
+      let currentURL = window.location.pathname;
+      if (window.location.search) {
+        currentURL += window.location.search;
+      }
+      loginUrl += `?next=${encodeURIComponent(currentURL)}`;
+    }
+    return loginUrl;
+  },
 };
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledError.js b/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledError.js
index fbde9a25..41ece6ae 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledError.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledError.js
@@ -1,3 +1,5 @@
+import ErrorUtils from "./ErrorUtils";
+
 let idSequence = 0;
 class UnhandledError {
   constructor({
@@ -15,6 +17,10 @@ class UnhandledError {
     this.suppressLogging = suppressLogging;
     this.createdDate = new Date();
   }
+
+  get isUnauthenticatedError() {
+    return ErrorUtils.isUnauthenticatedError(this.error);
+  }
 }
 
 export default UnhandledError;
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js b/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
index 35074fd1..10d75b1b 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
@@ -21,10 +21,22 @@ class UnhandledErrorDispatcher {
   }
 
   reportUnhandledError(unhandledError) {
+    // Ignore unauthenticated errors that have already been displayed
+    if (
+      unhandledError.isUnauthenticatedError &&
+      UnhandledErrorList.list.some((e) => e.isUnauthenticatedError)
+    ) {
+      return;
+    }
+
     if (!unhandledError.suppressDisplay) {
       UnhandledErrorList.add(unhandledError);
     }
-    if (!unhandledError.suppressLogging) {
+    if (
+      !unhandledError.suppressLogging &&
+      // Don't log unauthenticated errors
+      !unhandledError.isUnauthenticatedError
+    ) {
       ErrorReporter.reportUnhandledError(unhandledError);
     }
   }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
index 8a52802b..f11e7d12 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
@@ -1,3 +1,4 @@
+import ErrorUtils from "../errors/ErrorUtils";
 import UnhandledErrorDispatcher from "../errors/UnhandledErrorDispatcher";
 import Cache from "./Cache";
 
@@ -266,7 +267,8 @@ export default {
         if (showSpinner) {
           decrementCount();
         }
-        if (!ignoreErrors) {
+        // Always report unauthenticated errors so user knows they need to re-authenticate
+        if (!ignoreErrors || ErrorUtils.isUnauthenticatedError(error)) {
           this.reportError(error);
         }
         throw error;
diff --git a/django_airavata/static/common/js/components/NotificationsDisplay.vue b/django_airavata/static/common/js/components/NotificationsDisplay.vue
index 19542187..5d7a8f6d 100644
--- a/django_airavata/static/common/js/components/NotificationsDisplay.vue
+++ b/django_airavata/static/common/js/components/NotificationsDisplay.vue
@@ -1,16 +1,36 @@
 <template>
   <div id="notifications-display">
     <transition-group name="fade" tag="div">
-      <b-alert
-        v-for="unhandledError in unhandledErrors"
-        variant="danger"
-        :key="unhandledError.id"
-        show
-        dismissible
-        @dismissed="dismissUnhandledError(unhandledError)"
-      >
-        {{ unhandledError.message }}
-      </b-alert>
+      <template v-for="unhandledError in unhandledErrors">
+        <b-alert
+          v-if="isUnauthenticatedError(unhandledError.error)"
+          variant="danger"
+          :key="unhandledError.id"
+          show
+          dismissible
+          @dismissed="dismissUnhandledError(unhandledError)"
+        >
+          Your login session has expired. Please
+          <b-link class="alert-link" :href="loginLinkWithNext"
+            >log in again</b-link
+          >. You can also
+          <b-link class="alert-link" :href="loginLink" target="_blank"
+            >login in a separate tab
+            <i class="fa fa-external-link-alt" aria-hidden="true"></i
+          ></b-link>
+          and then return to this tab and try again.
+        </b-alert>
+        <b-alert
+          v-else
+          variant="danger"
+          :key="unhandledError.id"
+          show
+          dismissible
+          @dismissed="dismissUnhandledError(unhandledError)"
+        >
+          {{ unhandledError.message }}
+        </b-alert>
+      </template>
       <b-alert
         v-for="notification in notifications"
         :variant="variant(notification)"
@@ -86,6 +106,9 @@ export default {
       }.bind(this);
       setTimeout(pollAPIServerStatus.bind(this), this.pollingDelay);
     },
+    isUnauthenticatedError(error) {
+      return errors.ErrorUtils.isUnauthenticatedError(error);
+    },
   },
   computed: {
     apiServerDown() {
@@ -130,6 +153,12 @@ export default {
         : false;
       return notificationsApiServerDown || unhandledErrorsApiServerDown;
     },
+    loginLinkWithNext() {
+      return errors.ErrorUtils.buildLoginUrl();
+    },
+    loginLink() {
+      return errors.ErrorUtils.buildLoginUrl(false);
+    },
   },
   watch: {
     /*