GUAC-1161: Convert login page to directive which accepts a dynamic form. Display login directive when credentials are needed.

This commit is contained in:
Michael Jumper
2015-04-21 12:02:01 -07:00
parent 872aa38498
commit f29a24ad68
12 changed files with 214 additions and 221 deletions

View File

@@ -1,31 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* The config block for setting up the authentication interceptor.
*/
angular.module('index').config(['$httpProvider',
function indexInterceptorConfig($httpProvider) {
$httpProvider.interceptors.push('authenticationInterceptor');
}]);

View File

@@ -161,15 +161,6 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
resolve : { updateCurrentToken: updateCurrentToken }
})
// Login screen
.when('/login/', {
title : 'APP.NAME',
bodyClassName : 'login',
templateUrl : 'app/login/templates/login.html',
controller : 'loginController'
// No need to update token here - the login screen ignores all auth
})
// Client view
.when('/client/:type/:id/:params?', {
bodyClassName : 'client',

View File

@@ -36,6 +36,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/
$scope.guacNotification = guacNotification;
/**
* The credentials that the authentication service is currently expecting,
* if any. If the user is logged in, this will be null.
*
* @type Form[]|Form|Field[]|Field
*/
$scope.expectedCredentials = null;
/**
* Basic page-level information.
*/
@@ -92,6 +100,21 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
keyboard.reset();
};
// Display login screen if a whole new set of credentials is needed
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, expected) {
$scope.expectedCredentials = expected;
});
// Prompt for remaining credentials if provided credentials were not enough
$scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, expected) {
// TODO: Implement insufficient credential prompting
});
// Clear login screen if login was successful
$scope.$on('guacLogin', function loginSuccessful() {
$scope.expectedCredentials = null;
});
// Update title and CSS class upon navigation
$scope.$on('$routeChangeSuccess', function(event, current, previous) {

View File

@@ -1,63 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
angular.module('index').factory('authenticationInterceptor', ['$injector',
function authenticationInterceptor($injector) {
// Required services
var $location = $injector.get('$location');
var $q = $injector.get('$q');
var service = {};
/**
* Redirect users to login if authorization fails. This is called
* automatically when this service is registered as an interceptor, as
* documented at:
*
* https://docs.angularjs.org/api/ng/service/$http#interceptors
*
* @param {HttpPromise} rejection
* The promise associated with the HTTP request that failed.
*
* @returns {Promise}
* A rejected promise containing the originally-rejected HttpPromise.
*/
service.responseError = function responseError(rejection) {
// Only redirect failed authentication requests
if ((rejection.status === 401 || rejection.status === 403)
&& rejection.config.url === 'api/tokens') {
// Only redirect if not already on login page
if ($location.path() !== '/login/')
$location.path('/login/');
}
return $q.reject(rejection);
};
return service;
}]);

View File

@@ -1,71 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
angular.module('login').controller('loginController', ['$scope', '$injector',
function loginController($scope, $injector) {
// Required services
var $location = $injector.get('$location');
var authenticationService = $injector.get('authenticationService');
var userPageService = $injector.get('userPageService');
/**
* Whether an error occurred during login.
*
* @type Boolean
*/
$scope.loginError = false;
/**
* Whether the password field has focus.
*
* @type Boolean
*/
$scope.passwordFocused = false;
/**
* Submits the currently-specified username and password to the
* authentication service, redirecting to the main view if successful.
*/
$scope.login = function login() {
// Attempt login once existing session is destroyed
authenticationService.login($scope.username, $scope.password)
// Redirect to main view upon success
.then(function loginSuccessful() {
userPageService.getHomePage()
.then(function homePageRetrieved(homePage) {
$location.url(homePage.url);
});
})
// Reset and focus password upon failure
['catch'](function loginFailed() {
$scope.loginError = true;
$scope.passwordFocused = true;
$scope.password = '';
});
};
}]);

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive for displaying an arbitrary login form.
*/
angular.module('login').directive('guacLogin', [function guacLogin() {
// Login directive
var directive = {
restrict : 'E',
replace : true,
templateUrl : 'app/login/templates/login.html'
};
// Login directive scope
directive.scope = {
/**
* The login form or set of fields. This will be displayed to the user
* to capture their credentials.
*
* @type Form[]|Form|Field[]|Field
*/
form : '='
};
// Controller for login directive
directive.controller = ['$scope', '$injector',
function loginController($scope, $injector) {
// Required services
var $route = $injector.get('$route');
var authenticationService = $injector.get('authenticationService');
/**
* Whether an error occurred during login.
*
* @type Boolean
*/
$scope.loginError = false;
/**
* All form values entered by the user.
*/
$scope.values = {};
/**
* Submits the currently-specified username and password to the
* authentication service, redirecting to the main view if successful.
*/
$scope.login = function login() {
// Attempt login once existing session is destroyed
authenticationService.authenticate($scope.values)
// Clear and reload upon success
.then(function loginSuccessful() {
$scope.loginError = false;
$scope.values = {};
$route.reload();
})
// Reset upon failure
['catch'](function loginFailed() {
$scope.loginError = true;
$scope.values.password = '';
});
};
}];
return directive;
}]);

View File

@@ -23,4 +23,8 @@
/**
* The module for the login functionality.
*/
angular.module('login', ['element', 'navigation']);
angular.module('login', [
'element',
'form',
'navigation'
]);

View File

@@ -27,6 +27,8 @@ div.login-ui {
left: 0;
top: 0;
display: table;
background: white;
z-index: 20;
}
p.login-error {
@@ -52,3 +54,35 @@ p.login-error {
text-align: center;
color: #964040;
}
.login-fields .form-field .password-field .toggle-password {
display: none;
}
.login-fields .labeled-field {
display: block;
position: relative;
}
.login-fields .field-header {
display: block;
position: absolute;
left: 0;
right: 0;
overflow: hidden;
z-index: -1;
margin: 0.5em;
font-size: 0.9em;
opacity: 0.5;
}
.login-fields .form-field.empty input {
background: transparent;
}
.login-fields .form-field input:focus {
background: white;
}

View File

@@ -1,26 +1,25 @@
<!--
Copyright 2014 Glyptodon LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<div class="login-ui" ng-class="{error: loginError}" >
<!--
Copyright 2014 Glyptodon LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<!-- Login error message -->
<p class="login-error">{{'LOGIN.ERROR_INVALID_LOGIN' | translate}}</p>
@@ -35,10 +34,9 @@ THE SOFTWARE.
<img class="logo" src="images/guac-tricolor.png" alt=""/>
<div class="version">{{'APP.NAME' | translate}}</div>
<!-- Login fields (username + password) -->
<!-- Login fields -->
<div class="login-fields">
<input ng-model="username" placeholder="{{'LOGIN.FIELD_PLACEHOLDER_USERNAME' | translate}}" type="text" name="username" autofocus="autofocus" autocorrect="off" autocapitalize="off" class="username"/>
<input ng-model="password" placeholder="{{'LOGIN.FIELD_PLACEHOLDER_PASSWORD' | translate}}" type="password" name="password" class="password" guac-focus="passwordFocused"/>
<guac-form namespace="'LOGIN'" content="form" model="values"></guac-form>
</div>
<!-- Submit button -->

View File

@@ -48,6 +48,7 @@ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu()
// Get required services
var $document = $injector.get('$document');
var $location = $injector.get('$location');
var $route = $injector.get('$route');
var authenticationService = $injector.get('authenticationService');
var userPageService = $injector.get('userPageService');
@@ -100,12 +101,15 @@ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu()
};
/**
* Logs out the current user, redirecting them to back to the login
* screen after logout completes.
* Logs out the current user, redirecting them to back to the root
* after logout completes.
*/
$scope.logout = function logout() {
authenticationService.logout()['finally'](function logoutComplete() {
$location.path('/login/');
if ($location.path() !== '/')
$location.url('/');
else
$route.reload();
});
};

View File

@@ -34,22 +34,30 @@ THE SOFTWARE.
</head>
<body ng-class="page.bodyClassName">
<!-- Global status/error dialog -->
<div ng-class="{shown: guacNotification.status}" class="status-outer">
<div class="status-middle">
<guac-notification notification="guacNotification.status"></guac-notification>
<!-- Content for logged-in users -->
<div ng-show="!expectedCredentials">
<!-- Global status/error dialog -->
<div ng-class="{shown: guacNotification.status}" class="status-outer">
<div class="status-middle">
<guac-notification notification="guacNotification.status"></guac-notification>
</div>
</div>
<div id="content" ng-view>
</div>
<!-- Notification area -->
<div id="notificationArea">
<div ng-repeat="wrapper in guacNotification.notifications">
<guac-notification notification="wrapper.notification"></guac-notification>
</div>
</div>
</div>
<div id="content" ng-view>
</div>
<!-- Notification area -->
<div id="notificationArea">
<div ng-repeat="wrapper in guacNotification.notifications">
<guac-notification notification="wrapper.notification"></guac-notification>
</div>
</div>
<!-- Login screen for logged-out users -->
<guac-login ng-show="expectedCredentials" form="expectedCredentials"></guac-login>
<script type="text/javascript" src="guacamole.min.js"></script>
</body>

View File

@@ -141,12 +141,12 @@
"LOGIN": {
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"ERROR_INVALID_LOGIN" : "Invalid Login",
"ERROR_INVALID_LOGIN" : "Invalid Login",
"FIELD_PLACEHOLDER_USERNAME" : "Username",
"FIELD_PLACEHOLDER_PASSWORD" : "Password"
"FIELD_HEADER_USERNAME" : "Username",
"FIELD_HEADER_PASSWORD" : "Password"
},