diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index d05a0d38c..89d64aeaa 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -175,7 +175,7 @@ angular.module('auth').factory('authenticationService', ['$injector', // If an old token existed, request that the token be revoked if (currentToken) { - service.logout().catch(angular.noop) + service.revokeToken(currentToken).catch(angular.noop); } // Notify of login and new token @@ -252,6 +252,24 @@ angular.module('auth').factory('authenticationService', ['$injector', }; + /** + * Makes a request to revoke an authentication token using the token REST + * API endpoint, returning a promise that succeeds only if the token was + * successfully revoked. + * + * @param {string} token + * The authentication token to revoke. + * + * @returns {Promise} + * A promise which succeeds only if the token was successfully revoked. + */ + service.revokeToken = function revokeToken(token) { + return requestService({ + method: 'DELETE', + url: 'api/tokens/' + token + }); + }; + /** * Makes a request to authenticate a user using the token REST API endpoint * with a username and password, ignoring any currently-stored token, @@ -276,8 +294,8 @@ angular.module('auth').factory('authenticationService', ['$injector', }; /** - * Makes a request to logout a user using the login REST API endpoint, - * returning a promise succeeds only if the logout operation was + * Makes a request to logout a user using the token REST API endpoint, + * returning a promise that succeeds only if the logout operation was * successful. * * @returns {Promise} @@ -294,10 +312,7 @@ angular.module('auth').factory('authenticationService', ['$injector', $rootScope.$broadcast('guacLogout', token); // Delete old token - return requestService({ - method: 'DELETE', - url: 'api/tokens/' + token - }); + return service.revokeToken(token); }; diff --git a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js index 782167614..1bc5dd6fa 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -167,10 +167,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams className : "logout button", callback : function logoutCallback() { authenticationService.logout() - ['catch'](requestService.IGNORE) - ['finally'](function logoutComplete() { - $location.url('/'); - }); + ['catch'](requestService.IGNORE); } }; diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js index 9b9078235..51f129a3f 100644 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js @@ -25,6 +25,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Required services var $document = $injector.get('$document'); + var $route = $injector.get('$route'); var $window = $injector.get('$window'); var clipboardService = $injector.get('clipboardService'); var guacNotification = $injector.get('guacNotification'); @@ -50,6 +51,13 @@ angular.module('index').controller('indexController', ['$scope', '$injector', */ $scope.loginHelpText = null; + /** + * Whether the user has selected to log back in after having logged out. + * + * @type boolean + */ + $scope.reAuthenticating = false; + /** * The credentials that the authentication service is has already accepted, * pending additional credentials, if any. If the user is logged in, or no @@ -69,6 +77,51 @@ angular.module('index').controller('indexController', ['$scope', '$injector', */ $scope.expectedCredentials = null; + /** + * Possible overall states of the client side of the web application. + * + * @enum {string} + */ + var ApplicationState = { + + /** + * The application has fully loaded but is awaiting credentials from + * the user before proceeding. + */ + AWAITING_CREDENTIALS : 'awaitingCredentials', + + /** + * A fatal error has occurred that will prevent the client side of the + * application from functioning properly. + */ + FATAL_ERROR : 'fatalError', + + /** + * The application has just started within the user's browser and has + * not yet settled into any specific state. + */ + LOADING : 'loading', + + /** + * The user has manually logged out. + */ + LOGGED_OUT : 'loggedOut', + + /** + * The application has fully loaded and the user has logged in + */ + READY : 'ready' + + }; + + /** + * The current overall state of the client side of the application. + * Possible values are defined by {@link ApplicationState}. + * + * @type string + */ + $scope.applicationState = ApplicationState.LOADING; + /** * Basic page-level information. */ @@ -103,7 +156,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Do not handle key events if not logged in or if a notification is // shown - if ($scope.expectedCredentials || guacNotification.getStatus()) + if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus()) return true; // Warn of pending keydown @@ -122,7 +175,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Do not handle key events if not logged in or if a notification is // shown - if ($scope.expectedCredentials || guacNotification.getStatus()) + if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus()) return; // Warn of pending keyup @@ -168,32 +221,59 @@ angular.module('index').controller('indexController', ['$scope', '$injector', }, true); + /** + * Reloads the current route and controller, effectively forcing + * reauthentication. If the user is not logged in, this will result in + * the login screen appearing. + */ + $scope.reAuthenticate = function reAuthenticate() { + $scope.reAuthenticating = true; + $route.reload(); + }; + // Display login screen if a whole new set of credentials is needed $scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) { + + $scope.applicationState = ApplicationState.AWAITING_CREDENTIALS; $scope.page.title = 'APP.NAME'; $scope.page.bodyClassName = ''; + $scope.loginHelpText = null; $scope.acceptedCredentials = {}; $scope.expectedCredentials = error.expected; - $scope.fatalError = null; + }); // Prompt for remaining credentials if provided credentials were not enough $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { + + $scope.applicationState = ApplicationState.AWAITING_CREDENTIALS; $scope.page.title = 'APP.NAME'; $scope.page.bodyClassName = ''; + $scope.loginHelpText = error.translatableMessage; $scope.acceptedCredentials = parameters; $scope.expectedCredentials = error.expected; - $scope.fatalError = null; + }); // Replace absolutely all content with an error message if the page itself // cannot be displayed due to an error $scope.$on('guacFatalPageError', function fatalPageError(error) { + + $scope.applicationState = ApplicationState.FATAL_ERROR; $scope.page.title = 'APP.NAME'; $scope.page.bodyClassName = ''; + $scope.fatalError = error; + + }); + + // Replace the overall user interface with an informational message if the + // user has manually logged out + $scope.$on('guacLogout', function loggedOut() { + $scope.applicationState = ApplicationState.LOGGED_OUT; + $scope.reAuthenticating = false; }); // Ensure new pages always start with clear keyboard state @@ -209,10 +289,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Clear login screen if route change was successful (and thus // login was either successful or not required) - $scope.loginHelpText = null; - $scope.acceptedCredentials = null; - $scope.expectedCredentials = null; - $scope.fatalError = null; + $scope.applicationState = ApplicationState.READY; // Set title var title = current.$$route.title; diff --git a/guacamole/src/main/frontend/src/app/index/styles/cloak.css b/guacamole/src/main/frontend/src/app/index/styles/cloak.css new file mode 100644 index 000000000..701107bdf --- /dev/null +++ b/guacamole/src/main/frontend/src/app/index/styles/cloak.css @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * Hide portions of DOM by default until Angular has finished loading, + * compiling, etc. + */ + +*[ng-cloak], .translate-cloak { + display: none !important; +} diff --git a/guacamole/src/main/frontend/src/app/index/styles/fatal-page-error.css b/guacamole/src/main/frontend/src/app/index/styles/fatal-page-error.css index 9a50e9c9b..bbfe40615 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/fatal-page-error.css +++ b/guacamole/src/main/frontend/src/app/index/styles/fatal-page-error.css @@ -17,23 +17,10 @@ * under the License. */ -.fatal-page-error-outer { - display: table; - height: 100%; - width: 100%; - position: fixed; - left: 0; - top: 0; +.fatal-page-error-modal guac-modal { z-index: 30; } -.fatal-page-error-middle { - width: 100%; - text-align: center; - display: table-cell; - vertical-align: middle; -} - .fatal-page-error { display: inline-block; width: 100%; diff --git a/guacamole/src/main/frontend/src/app/index/styles/logged-out.css b/guacamole/src/main/frontend/src/app/index/styles/logged-out.css new file mode 100644 index 000000000..531ef00cd --- /dev/null +++ b/guacamole/src/main/frontend/src/app/index/styles/logged-out.css @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.logged-out-modal guac-modal { + background: white; + z-index: 20; +} + +.logged-out-modal .notification { + display: inline-block; + max-width: 3in; + width: 100%; +} diff --git a/guacamole/src/main/frontend/src/app/index/styles/status.css b/guacamole/src/main/frontend/src/app/index/styles/status.css index a24c71a56..7fd63672d 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/status.css +++ b/guacamole/src/main/frontend/src/app/index/styles/status.css @@ -17,25 +17,11 @@ * under the License. */ -.status-outer { - display: table; - height: 100%; - width: 100%; - position: fixed; - left: 0; - top: 0; +.global-status-modal guac-modal { background: rgba(0, 0, 0, 0.5); - z-index: 10; } -.status-middle { - width: 100%; - text-align: center; - display: table-cell; - vertical-align: middle; -} - -.status-middle .notification { +.global-status-modal .notification { width: 75%; max-width: 5in; @@ -47,34 +33,10 @@ } -.status-middle .notification .body { +.global-status-modal .notification .body { margin: 1.25em; } -.status-middle .notification .buttons { +.global-status-modal .notification .buttons { margin: 1em; } - -/* Fade entire status area in/out based on shown status */ - -.status-outer { - visibility: hidden; - opacity: 0; - transition: opacity, visibility; - transition-duration: 0.25s; -} - -.shown.status-outer { - visibility: visible; - opacity: 1; -} - -/* Hide dialog immediately based on status */ - -.status-middle .notification { - visibility: hidden; -} - -.shown .status-middle .notification { - visibility: visible; -} diff --git a/guacamole/src/main/frontend/src/app/login/styles/dialog.css b/guacamole/src/main/frontend/src/app/login/styles/dialog.css index e83353331..48c583ef0 100644 --- a/guacamole/src/main/frontend/src/app/login/styles/dialog.css +++ b/guacamole/src/main/frontend/src/app/login/styles/dialog.css @@ -36,8 +36,6 @@ max-width: 3in; text-align: left; padding: 1em; - border: 1px solid rgba(0, 0, 0, 0.25); - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); font-size: 1.25em; display: inline-block; diff --git a/guacamole/src/main/frontend/src/app/login/templates/login.html b/guacamole/src/main/frontend/src/app/login/templates/login.html index 41bc0cd10..9a354163b 100644 --- a/guacamole/src/main/frontend/src/app/login/templates/login.html +++ b/guacamole/src/main/frontend/src/app/login/templates/login.html @@ -6,7 +6,7 @@