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 <[email protected]> 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: { /*
