Auth support

- Created StringUtil class with some useful random string methods.
- Create UrlUtil class with useful URL manipulation and builder methods.
- Cleaned up some unused libraries (cookies, mocks) from index.html
- Added LocalStorage dependency.
- Added advanced routing to auth module for OAuth response routing.
- Added state resolver methods so we can enforce UI states that require
certain session states.
- Removed AuthProvider resolver and resource, as they're no longer necessary.
- Updated header to point to correct routes.
- Updated header to correctly represent state.
- Added busy template for "pending" activity. This shouldn't actually show up
because the javascript will resolve the view logic too quickly, but it's
included for the sake of completion.
- Added error state in case we get an error response from the server. It's
very basic.
- Added request interceptor that attaches an access token to every request
if a valid access token exists.
- Added OpenId service to handle our redirection and token resolution.
- Added Deauthorization (logout) controller.
- Added session management controller.
- Added search param provider to inject non-hashbang query parameters.

Change-Id: Id9b1e7fe9ed98ad4be0a80f1acd4a9e125ec57c9
This commit is contained in:
Nikita Konovalov 2014-02-13 13:39:31 +04:00 committed by Michael Krotscheck
parent a91c4e7d4d
commit 9e9ee48918
28 changed files with 1067 additions and 209 deletions

View File

@ -6,11 +6,11 @@
"font-awesome": "4.0", "font-awesome": "4.0",
"angular": "1.2.13", "angular": "1.2.13",
"angular-resource": "1.2.13", "angular-resource": "1.2.13",
"angular-cookies": "1.2.13",
"angular-sanitize": "1.2.13", "angular-sanitize": "1.2.13",
"bootstrap": "3.1.0", "bootstrap": "3.1.0",
"angular-ui-router": "0.2.8-bowratic-tedium", "angular-ui-router": "0.2.8-bowratic-tedium",
"angular-bootstrap": "0.10.0" "angular-bootstrap": "0.10.0",
"angular-local-storage": "0.0.1"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "1.2.13", "angular-mocks": "1.2.13",

View File

@ -35,7 +35,10 @@ module.exports = function (config) {
], ],
files: [ files: [
'./dist/js/*.js', './dist/js/libs.js',
'./bower_components/angular-mocks/angular-mocks.js',
'./dist/js/storyboard.js',
'./dist/js/templates.js',
'./test/unit/**/*.js' './test/unit/**/*.js'
], ],

View File

@ -35,7 +35,10 @@ module.exports = function (config) {
], ],
files: [ files: [
'./dist/js/*.js', './dist/js/libs.js',
'./bower_components/angular-mocks/angular-mocks.js',
'./dist/js/storyboard.js',
'./dist/js/templates.js',
'./test/unit/**/*.js' './test/unit/**/*.js'
], ],

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/*
* This controller is responsible for getting an authorization code
* having a state and an openid.
*
* @author Nikita Konovalov
*/
angular.module('sb.auth').controller('AuthAuthorizeController',
function ($stateParams, $state, $log, OpenId) {
'use strict';
// First, check for the edge case where the API returns an error code
// back to us. This should only happen when it fails to properly parse
// our redirect_uri and thus just sends the error back to referrer, but
// we should still catch it.
if (!!$stateParams.error) {
$log.debug('Error received, redirecting to auth.error.');
$state.go('auth.error', $stateParams);
return;
}
// We're not an error, let's fire the authorization.
OpenId.authorize();
});

View File

@ -15,14 +15,14 @@
*/ */
/** /**
* This controller handles the logic for the authorization provider list page. * This controller deauthorizes the session and destroys all tokens.
*
* @author Michael Krotscheck
*/ */
angular.module('sb.auth').controller('AuthLoginController',
function ($scope, authProvider) { angular.module('sb.auth').controller('AuthDeauthorizeController',
function (Session, $state, $log) {
'use strict'; 'use strict';
$scope.authProvider = authProvider; $log.debug('Logging out');
Session.destroySession();
$state.go('index');
}); });

View File

@ -15,19 +15,16 @@
*/ */
/** /**
* This resource exposes authorization providers to our angularjs environment, * View controller for authorization error conditions.
* allowing us to manage & control them. It's also used during the
* authorization/login process to determine how we're going to allow users to
* log in to storyboard.
*
* @author Michael Krotscheck
*/ */
angular.module('sb.auth').controller('AuthErrorController',
angular.module('sb.services').factory('AuthProvider', function ($scope, $stateParams) {
function ($resource, storyboardApiBase, storyboardApiSignature) {
'use strict'; 'use strict';
return $resource(storyboardApiBase + '/auth/provider/:id', console.warn('AuthErrorController');
{id: '@id'},
storyboardApiSignature);
$scope.error = $stateParams.error || 'Unknown';
$scope.errorDescription = $stateParams.error_description ||
'No description received from server.';
}); });

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2014 Mirantis Inc.
*
* Licensed 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.
*/
/*
* This controller is responsible for getting an access_token and
* a refresh token having an authorization_code.
*
* @author Nikita Konovalov
*/
angular.module('sb.auth').controller('AuthTokenController',
function ($state, $log, OpenId, Session, $searchParams) {
'use strict';
// First, check for the edge case where the API returns an error code
// back to us. This should only happen when it fails to properly parse
// our redirect_uri and thus just sends the error back to referrer, but
// we should still catch it.
if (!!$searchParams.error) {
$log.debug('Error received, redirecting to auth.error.');
$state.go('auth.error', $searchParams);
return;
}
// Looks like there's no error, so let's see if we can resolve a token.
// TODO: Finish implementing.
OpenId.token($searchParams)
.then(
function (token) {
Session.updateSession(token)
.then(function () {
$state.go('index');
});
},
function (error) {
Session.destroySession();
$state.go('auth.error', error);
}
);
});

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* An HTTP request interceptor that attaches an authorization to every HTTP
* request, assuming it exists and isn't expired.
*/
angular.module('sb.auth').factory('httpAuthorizationHeader',
function (AccessToken) {
'use strict';
return {
request: function (request) {
// TODO(krotscheck): Only apply the token to requests to
// storyboardApiBase.
var token = AccessToken.getAccessToken();
var type = AccessToken.getTokenType();
if (!!token && !AccessToken.isExpired()) {
request.headers.Authorization = type + ' ' + token;
}
return request;
}
};
})
// Attach the HTTP interceptor.
.config(function ($httpProvider) {
'use strict';
$httpProvider.interceptors.push('httpAuthorizationHeader');
});

View File

@ -15,48 +15,48 @@
*/ */
/** /**
* This Storyboard module contains our adaptive authentication and authorization * This Storyboard module contains our authentication and authorization logic.
* logic.
*
* @author Michael Krotscheck
*/ */
angular.module('sb.auth', angular.module('sb.auth', [ 'sb.services', 'sb.templates', 'ui.router',
[ 'sb.services', 'sb.templates', 'ui.router'] 'sb.util', 'LocalStorageModule']
) )
.config(function ($stateProvider, $urlRouterProvider, .config(function ($stateProvider, SessionResolver) {
AuthProviderResolver) {
'use strict'; 'use strict';
// Default rerouting.
$urlRouterProvider.when('/auth', '/auth/provider/list');
$urlRouterProvider.when('/auth/provider', '/auth/provider/list');
// Declare the states for this module. // Declare the states for this module.
$stateProvider $stateProvider
.state('auth', { .state('auth', {
abstract: true, abstract: true,
url: '/auth', template: '<div ui-view></div>',
template: '<div ui-view></div>' url: '/auth'
}) })
.state('auth.provider', { .state('auth.authorize', {
abstract: true, url: '/authorize?error&error_description',
url: '/provider', templateUrl: 'app/templates/auth/busy.html',
template: '<div ui-view></div>' controller: 'AuthAuthorizeController',
})
.state('auth.provider.list', {
url: '/list',
templateUrl: 'app/templates/auth/provider/list.html',
controller: 'AuthListController',
resolve: { resolve: {
authProviders: AuthProviderResolver.resolveAuthProviders isLoggedOut: SessionResolver.requireLoggedOut
} }
}) })
.state('auth.provider.id', { .state('auth.deauthorize', {
url: '/:id', url: '/deauthorize',
templateUrl: 'app/templates/auth/provider/login.html', templateUrl: 'app/templates/auth/busy.html',
controller: 'AuthLoginController', controller: 'AuthDeauthorizeController',
resolve: { resolve: {
authProvider: AuthProviderResolver.resolveAuthProvider('id') isLoggedIn: SessionResolver.requireLoggedIn
} }
})
.state('auth.token', {
url: '/token?code&state&error&error_description',
templateUrl: 'app/templates/auth/busy.html',
controller: 'AuthTokenController',
resolve: {
isLoggedOut: SessionResolver.requireLoggedOut
}
})
.state('auth.error', {
url: '/error?error&error_description',
templateUrl: 'app/templates/auth/error.html',
controller: 'AuthErrorController'
}); });
}); });

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* A list of constants used by the session service to maintain the user's
* current authentication state.
*/
angular.module('sb.auth').value('SessionState', {
/**
* Session state constant, used to indicate that the user is logged in.
*/
LOGGED_IN: 'logged_in',
/**
* Session state constant, used to indicate that the user is logged out.
*/
LOGGED_OUT: 'logged_out',
/**
* Session state constant, used during initialization when we're not quite
* certain yet whether we're logged in or logged out.
*/
PENDING: 'pending'
});

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* A set of utility methods that may be used during state declaration to enforce
* session state. They return asynchronous promises which will either resolve
* or reject the state change, depending on what you're asking for.
*/
angular.module('sb.auth').constant('SessionResolver',
(function () {
'use strict';
/**
* Resolve the promise based on the current session state. We can't
* inject here, since the injector's not ready yet.
*/
function resolveSessionState(deferred, desiredSessionState, Session) {
return function () {
var sessionState = Session.getSessionState();
if (sessionState === desiredSessionState) {
deferred.resolve(sessionState);
} else {
deferred.reject(sessionState);
}
};
}
return {
/**
* This resolver asserts that the user is logged
* out before allowing a route. Otherwise it fails.
*/
requireLoggedOut: function ($q, $log, Session, SessionState) {
$log.debug('Resolving logged-out-only route...');
var deferred = $q.defer();
var resolveLoggedOut = resolveSessionState(deferred,
SessionState.LOGGED_OUT, Session);
// Do we have to wait for state resolution?
if (Session.getSessionState() === SessionState.PENDING) {
Session.resolveSessionState().then(resolveLoggedOut);
} else {
resolveLoggedOut();
}
return deferred.promise;
},
/**
* This resolver asserts that the user is logged
* in before allowing a route. Otherwise it fails.
*/
requireLoggedIn: function ($q, $log, Session, $rootScope,
SessionState) {
$log.debug('Resolving logged-in-only route...');
var deferred = $q.defer();
var resolveLoggedIn = resolveSessionState(deferred,
SessionState.LOGGED_IN, Session);
// Do we have to wait for state resolution?
if (Session.getSessionState() === SessionState.PENDING) {
Session.resolveSessionState().then(resolveLoggedIn);
} else {
resolveLoggedIn();
}
return deferred.promise;
}
};
})());

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* AccessToken storage service, an abstraction layer between our token storage
* and the rest of the system. This feature uses localStorage, which means that
* our application will NOT support IE7. Once that becomes a requirement, we'll
* have to use this abstraction layer to store data in a cookie instead.
*/
angular.module('sb.auth').factory('AccessToken',
function (localStorageService) {
'use strict';
/**
* Our local storage key name constants
*/
var TOKEN_TYPE = 'token_type';
var ACCESS_TOKEN = 'access_token';
var REFRESH_TOKEN = 'refresh_token';
var ID_TOKEN = 'id_token';
var EXPIRES_IN = 'expires_in';
var ISSUE_DATE = 'issue_date';
return {
/**
* Clears the token
*/
clear: function () {
localStorageService.remove(TOKEN_TYPE);
localStorageService.remove(ACCESS_TOKEN);
localStorageService.remove(REFRESH_TOKEN);
localStorageService.remove(ID_TOKEN);
localStorageService.remove(EXPIRES_IN);
localStorageService.remove(ISSUE_DATE);
},
/**
* Sets all token properties at once.
*/
setToken: function (jsonToken) {
this.setTokenType(jsonToken.token_type);
this.setAccessToken(jsonToken.access_token);
this.setRefreshToken(jsonToken.refresh_token);
this.setIdToken(jsonToken.id_token);
this.setIssueDate(jsonToken.issue_date);
this.setExpiresIn(jsonToken.expires_in);
},
/**
* Is the current access token expired?
*/
isExpired: function () {
var expiresIn = this.getExpiresIn() || 0;
var issueDate = this.getIssueDate() || 0;
var now = Math.round((new Date()).getTime() / 1000);
return issueDate + expiresIn < now;
},
/**
* Get the token type. Bearer, etc.
*/
getTokenType: function () {
return localStorageService.get(TOKEN_TYPE);
},
/**
* Set the token type.
*/
setTokenType: function (value) {
return localStorageService.set(TOKEN_TYPE, value);
},
/**
* Retrieve the date this token was issued.
*/
getIssueDate: function () {
return localStorageService.get(ISSUE_DATE) || null;
},
/**
* Set the issue date for the current access token.
*/
setIssueDate: function (value) {
return localStorageService.set(ISSUE_DATE, value);
},
/**
* Get the number of seconds after the issue date when this token
* is considered expired.
*/
getExpiresIn: function () {
return localStorageService.get(EXPIRES_IN) || 0;
},
/**
* Set the number of seconds from the issue date when this token
* will expire.
*/
setExpiresIn: function (value) {
return localStorageService.set(EXPIRES_IN, value);
},
/**
* Retrieve the access token.
*/
getAccessToken: function () {
return localStorageService.get(ACCESS_TOKEN) || null;
},
/**
* Set the access token.
*/
setAccessToken: function (value) {
return localStorageService.set(ACCESS_TOKEN, value);
},
/**
* Retrieve the refresh token.
*/
getRefreshToken: function () {
return localStorageService.get(REFRESH_TOKEN) || null;
},
/**
* Set the refresh token.
*/
setRefreshToken: function (value) {
return localStorageService.set(REFRESH_TOKEN, value);
},
/**
* Retrieve the id token.
*/
getIdToken: function () {
return localStorageService.get(ID_TOKEN) || null;
},
/**
* Set the id token.
*/
setIdToken: function (value) {
return localStorageService.set(ID_TOKEN, value);
}
};
});

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* The current user service. It pays attention to changes in the application's
* session state, and loads the user found in the AccessToken when a valid
* session is detected.
*/
angular.module('sb.auth').factory('CurrentUser',
function (SessionState, Session, AccessToken, $rootScope, $log, $q, User) {
'use strict';
/**
* The current user
*/
var currentUser = null;
/**
* Load the current user, if such exists.
*/
function loadCurrentUser() {
if (Session.getSessionState() === SessionState.LOGGED_IN) {
var userId = AccessToken.getIdToken();
$log.debug('Loading Current User ' + userId);
currentUser = User.get({id: userId});
} else {
currentUser = null;
}
}
$rootScope.$on(SessionState.LOGGED_IN, loadCurrentUser);
$rootScope.$on(SessionState.LOGGED_OUT, loadCurrentUser);
loadCurrentUser();
// Expose the methods for this service.
return {
/**
* Retrieve the current user.
*/
get: function () {
return currentUser;
}
};
});

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* Our OpenID token resource, which adheres to the OpenID connect specification
* found here; http://openid.net/specs/openid-connect-basic-1_0.html
*/
angular.module('sb.auth').factory('OpenId',
function ($location, $window, $log, $http, $q, StringUtil, UrlUtil,
storyboardApiBase, localStorageService) {
'use strict';
var storageKey = 'openid_authorize_state';
var authorizeUrl = storyboardApiBase + '/openid/authorize';
var tokenUrl = storyboardApiBase + '/openid/token';
var redirectUri = UrlUtil.buildApplicationUrl('/auth/token');
var clientId = $location.host();
return {
/**
* Asks the OAuth endpoint for an authorization token given
* the passed parameters.
*/
authorize: function () {
// Create and store a random state parameter.
var state = StringUtil.randomAlphaNumeric(20);
localStorageService.set(storageKey, state);
var openIdParams = {
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: 'user',
state: state
};
$window.location.href = authorizeUrl + '?' +
UrlUtil.serializeParameters(openIdParams);
},
/**
* Asks our OpenID endpoint to convert an authorization token to
* an access token.
*/
token: function (params) {
var deferred = $q.defer();
var authorizationCode = params.code;
var tokenParams = {
grant_type: 'authorization_code',
code: authorizationCode
};
var url = tokenUrl + '?' +
UrlUtil.serializeParameters(tokenParams);
$http({method: 'POST', url: url})
.then(function (response) {
$log.debug('Token creation succeeded.');
// Extract the data
var data = response.data;
// Derive an issue date, from the Date header if
// possible.
var dateHeader = response.headers('Date');
if (!dateHeader) {
data.issue_date = Math.floor(Date.now() / 1000);
} else {
data.issue_date = Math.floor(
new Date(dateHeader) / 1000
);
}
deferred.resolve(data);
},
function (response) {
$log.debug('Token creation failed.');
// Construct a conformant error response.
var error = response.data;
if (!error.hasOwnProperty('error')) {
error = {
error: response.status,
error_description: response.data
};
}
deferred.reject(error);
});
return deferred.promise;
}
};
});

View File

@ -0,0 +1,170 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* Session management service - keeps track of our current session state, mostly
* by verifying the token state returned from the OpenID service.
*/
angular.module('sb.auth').factory('Session',
function (SessionState, AccessToken, $rootScope, $log, $q, User) {
'use strict';
/**
* The current session state.
*
* @type String
*/
var sessionState = SessionState.PENDING;
/**
* Initialize the session.
*/
function initializeSession() {
var deferred = $q.defer();
if (!AccessToken.getAccessToken() || AccessToken.isExpired()) {
$log.debug('No token found');
updateSessionState(SessionState.LOGGED_OUT);
deferred.resolve();
} else {
// Validate the token currently in the cache.
validateToken()
.then(function () {
$log.debug('Token validated');
updateSessionState(SessionState.LOGGED_IN);
deferred.resolve(sessionState);
}, function () {
$log.debug('Token not validated');
AccessToken.clear();
updateSessionState(SessionState.LOGGED_OUT);
deferred.resolve(sessionState);
});
}
return deferred.promise;
}
/**
* Validate the token.
*/
function validateToken() {
var deferred = $q.defer();
var id = AccessToken.getIdToken();
User.read({id: id},
function (user) {
deferred.resolve(user);
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
}
/**
* Handles state updates and broadcasts.
*/
function updateSessionState(newState) {
if (newState !== sessionState) {
sessionState = newState;
$rootScope.$broadcast(sessionState);
}
}
/**
* Destroy the session (Clear the token).
*/
function destroySession() {
AccessToken.clear();
updateSessionState(SessionState.LOGGED_OUT);
}
/**
* Initialize and test our current session token.
*/
initializeSession();
// If we ever encounter a 401 error, make sure the session is destroyed.
$rootScope.$on('http_401', function () {
destroySession();
});
// Expose the methods for this service.
return {
/**
* The current session state.
*/
getSessionState: function () {
return sessionState;
},
/**
* Resolve the current session state, as a promise.
*/
resolveSessionState: function () {
var deferred = $q.defer();
if (sessionState !== SessionState.PENDING) {
deferred.resolve(sessionState);
} else {
var unwatch = $rootScope.$watch(function () {
return sessionState;
}, function () {
deferred.resolve(sessionState);
unwatch();
});
}
return deferred.promise;
},
/**
* Are we logged in?
*/
isLoggedIn: function () {
return sessionState === SessionState.LOGGED_IN;
},
/**
* Destroy the session.
*/
destroySession: function () {
destroySession();
},
/**
* Update the session with a new (or null) token.
*/
updateSession: function (token) {
var deferred = $q.defer();
if (!token) {
destroySession();
deferred.resolve(sessionState);
} else {
AccessToken.setToken(token);
initializeSession().then(
function () {
deferred.resolve(sessionState);
},
function () {
deferred.resolve(sessionState);
}
);
}
return deferred.promise;
}
};
});

View File

@ -18,7 +18,5 @@
* The Storyboard Services module contains all of the necessary API resources * The Storyboard Services module contains all of the necessary API resources
* used by the storyboard client. Its resources are available via injection to * used by the storyboard client. Its resources are available via injection to
* any module that declares it as a dependency. * any module that declares it as a dependency.
*
* @author Michael Krotscheck
*/ */
angular.module('sb.services', ['ngResource', 'ngCookies']); angular.module('sb.services', ['ngResource']);

View File

@ -1,84 +0,0 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* This collection of utility methods allow us to pre-resolve AuthProvider
* resources before a UI route switch is completed.
*
* @author Michael Krotscheck
*/
angular.module('sb.services').constant('AuthProviderResolver', {
/**
* Resolves all available authorization providers.
*/
resolveAuthProviders: function ($q, AuthProvider, $log) {
'use strict';
$log.debug('Resolving AuthProviders');
var deferred = $q.defer();
AuthProvider.query(
function (result) {
deferred.resolve(result);
},
function (error) {
$log.warn('Route resolution rejected for AuthProviders');
deferred.reject(error);
});
return deferred.promise;
},
/**
* Resolves an AuthProvider based on the unique ID passed via the
* stateParams.
*/
resolveAuthProvider: function (stateParamName) {
'use strict';
return function ($q, AuthProvider, $stateParams, $log) {
var deferred = $q.defer();
if (!$stateParams.hasOwnProperty(stateParamName)) {
$log.warn('State did not contain property of name ' +
stateParamName);
deferred.reject({
'error': true
});
} else {
var id = $stateParams[stateParamName];
$log.debug('Resolving AuthProvider: ' + id);
AuthProvider.get({'id': id},
function (result) {
deferred.resolve(result);
},
function (error) {
$log.warn('Route resolution rejected for ' +
'AuthProvider ' + id);
deferred.reject(error);
});
return deferred.promise;
}
};
}
});

View File

@ -18,10 +18,35 @@
* Controller for our application header. * Controller for our application header.
*/ */
angular.module('storyboard').controller('HeaderController', angular.module('storyboard').controller('HeaderController',
function ($scope, $modal, NewStoryService) { function ($scope, NewStoryService, Session, SessionState, CurrentUser) {
'use strict'; 'use strict';
/**
* Load and maintain the current user.
*/
$scope.currentUser = CurrentUser.get();
/**
* Create a new story.
*/
$scope.newStory = function () { $scope.newStory = function () {
NewStoryService.showNewStoryModal(); NewStoryService.showNewStoryModal();
}; };
/**
* View handle to show the current logged in state.
*/
$scope.isLoggedIn =
(Session.getSessionState() === SessionState.LOGGED_IN);
// Watch for changes to the session state.
$scope.$watch(
function () {
return Session.getSessionState();
},
function (sessionState) {
$scope.isLoggedIn = sessionState === SessionState.LOGGED_IN;
$scope.currentUser = CurrentUser.get();
}
);
}); });

View File

@ -48,13 +48,13 @@ angular.module('storyboard',
$httpProvider.defaults.headers.common['X-Client'] = 'Storyboard'; $httpProvider.defaults.headers.common['X-Client'] = 'Storyboard';
}) })
.run(function ($log, $rootScope, $location) { .run(function ($log, $rootScope, $state) {
'use strict'; 'use strict';
// Listen to changes on the root scope. If it's an error in the state // Listen to changes on the root scope. If it's an error in the state
// changes (i.e. a 404) take the user back to the index. // changes (i.e. a 404) take the user back to the index.
$rootScope.$on('$stateChangeError', $rootScope.$on('$stateChangeError',
function () { function () {
$location.path('/'); $state.go('index');
}); });
}); });

View File

@ -1,5 +1,5 @@
<!-- <!--
~ Copyright (c) 2013 Hewlett-Packard Development Company, L.P. ~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~ ~
~ Licensed under the Apache License, Version 2.0 (the "License"); you may ~ Licensed under the Apache License, Version 2.0 (the "License"); you may
~ not use this file except in compliance with the License. You may obtain ~ not use this file except in compliance with the License. You may obtain
@ -17,10 +17,8 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<h1>Login with {{authProvider.title}}</h1> <p class="text-center">
<p class="lead"> <i class="fa fa-spinner fa-lg fa-spin"></i>
This feature requires the existence of a functioning API
Authentication layer, and is therefore disabled.
</p> </p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,42 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ Licensed 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.
-->
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Oh no!</h1>
<p class="lead">We encountered an unexpected error while trying to
log you in. The error message below should be helpful,
though if it's not you can contact our engineers in
#storyboard on
<a href="http://freenode.net/" target="_blank">
Freenode
</a>.
</p>
<dl class="dl-horizontal text-danger">
<dt>Error Code:</dt>
<dd>{{error}}</dd>
<dt>Error Description:</dt>
<dd>{{errorDescription}}</dd>
</dl>
<!-- TODO(krotscheck): If a user reaches this point, they should
be easily able to submit a bug report to storyboard -->
</div>
</div>
</div>

View File

@ -1,33 +0,0 @@
<!--
~ Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
~
~ Licensed 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.
-->
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>How would you like to log in?</h1>
<hr/>
</div>
<div class="col-sm-8 col-xs-12">
<a ng-repeat="provider in authProviders"
ng-class="[provider.type]"
class="auth-provider btn btn-info btn-lg btn-block"
href="#!/auth/provider/{{provider.id}}">
<i class="fa fa-caret-right"></i> {{provider.title}}
</a>
</div>
</div>
</div>

View File

@ -46,8 +46,8 @@
<li class="visible-xs"> <li class="visible-xs">
<p class="navbar-text" ng-show="isLoggedIn"> <p class="navbar-text" ng-show="isLoggedIn">
<i class="fa fa-user"></i>&nbsp; <i class="fa fa-user"></i>&nbsp;
{{currentUser.firstName}} {{currentUser.first_name}}
{{currentUser.lastName}} {{currentUser.last_name}}
</p> </p>
</li> </li>
@ -67,10 +67,10 @@
</li> </li>
<!-- Login/Logout button, XS only. --> <!-- Login/Logout button, XS only. -->
<li class="visible-xs"> <li class="visible-xs">
<a href="#!/auth/login" ng-hide="isLoggedIn"> <a href="#!/auth/authorize" ng-hide="isLoggedIn">
Log in Log in
</a> </a>
<a href="#!/auth/logout" ng-show="isLoggedIn"> <a href="#!/auth/deauthorize" ng-show="isLoggedIn">
Log out Log out
</a> </a>
</li> </li>
@ -90,19 +90,19 @@
<li ng-show="isLoggedIn"> <li ng-show="isLoggedIn">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-user"></i>&nbsp; <i class="fa fa-user"></i>&nbsp;
{{currentUser.firstName}} {{currentUser.first_name}}
{{currentUser.lastName}}&nbsp; {{currentUser.last_name}}&nbsp;
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a href="#!/auth/logout">Logout</a> <a href="#!/auth/deauthorize">Logout</a>
</li> </li>
</ul> </ul>
</li> </li>
<!-- Login, non-XS only. --> <!-- Login, non-XS only. -->
<li ng-hide="isLoggedIn"> <li ng-hide="isLoggedIn">
<a href="#!/auth">Log in</a> <a href="#!/auth/authorize">Log in</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2014 Mirantis Inc.
*
* Licensed 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.
*/
/*
* A collection of string utilities.
*
* @author Nikita Konovalov
*/
angular.module('sb.util').factory('StringUtil',
function () {
'use strict';
return {
/**
* Helper to generate a random alphanumeric string for the state
* parameter.
*
* @param length The length of the string to generate.
* @returns {string} A random alphanumeric string.
*/
randomAlphaNumeric: function (length) {
var possible =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789';
return this.random(length, possible);
},
/**
* Helper to generate a random string of specified length, using a
* provided list of characters.
*
* @param length The length of the string to generate.
* @param characters The list of valid characters.
* @returns {string} A random string composed of provided
* characters.
*/
random: function (length, characters) {
var text = '';
for (var i = 0; i < length; i++) {
text += characters.charAt(Math.floor(
Math.random() * characters.length));
}
return text;
}
};
}
);

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed 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.
*/
/**
* URL and location manipulation utilities.
*
* @author Nikita Konovalov
*/
angular.module('sb.util').factory('UrlUtil',
function ($location) {
'use strict';
return {
/**
* Return the full URL prefix of the application, without the #!
* component.
*/
getFullUrlPrefix: function () {
var protocol = $location.protocol();
var host = $location.host();
var port = $location.port();
return protocol + '://' + host + ':' + port;
},
/**
* Build a HashBang url for this application given the provided
* fragment.
*/
buildApplicationUrl: function (fragment) {
return this.getFullUrlPrefix() + '/#!' + fragment;
},
/**
* Serialize an object into HTTP parameters.
*/
serializeParameters: function (params) {
var pairs = [];
for (var prop in params) {
// Filter out system params.
if (!params.hasOwnProperty(prop)) {
continue;
}
pairs.push(
encodeURIComponent(prop) +
'=' +
encodeURIComponent(params[prop])
);
}
return pairs.join('&');
},
/**
* Deserialize URI query parameters into an object.
*/
deserializeParameters: function (queryString) {
var params = {};
var queryComponents = queryString.split('&');
for (var i = 0; i < queryComponents.length; i++) {
var parts = queryComponents[i].split('=');
var key = decodeURIComponent(parts[0]) || null;
var value = decodeURIComponent(parts[1]) || null;
if (!!key && !!value) {
params[key] = value;
}
}
return params;
}
};
}
);

View File

@ -15,19 +15,21 @@
*/ */
/** /**
* This controller handles the logic for the authorization provider list page. * Utility injector, injects the query parameters from the NON-hashbang URL as
* * $searchParams.
* @author Michael Krotscheck
*/ */
angular.module('sb.auth').controller('AuthListController', angular.module('sb.util').factory('$searchParams',
function ($scope, authProviders, $state) { function ($window, UrlUtil) {
'use strict'; 'use strict';
// If there's only one auth provider, just use that. var params = {};
if (!!authProviders && authProviders.length === 1) { var search = $window.location.search;
$state.go('auth.provider.id', {id: authProviders[0].id}); if (!!search) {
if (search.charAt(0) === '?') {
search = search.substr(1);
} }
$scope.authProviders = authProviders; return UrlUtil.deserializeParameters(search);
}
return params;
}); });

View File

@ -34,8 +34,7 @@
<script src="angular-bootstrap/ui-bootstrap-tpls.js"></script> <script src="angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="angular-ui-router/release/angular-ui-router.js"></script> <script src="angular-ui-router/release/angular-ui-router.js"></script>
<script src="angular-resource/angular-resource.js"></script> <script src="angular-resource/angular-resource.js"></script>
<script src="angular-mocks/angular-mocks.js"></script> <script src="angular-local-storage/angular-local-storage.js"></script>
<script src="angular-cookies/angular-cookies.js"></script>
<script src="angular-sanitize/angular-sanitize.js"></script> <script src="angular-sanitize/angular-sanitize.js"></script>
<script src="bootstrap/dist/js/bootstrap.js"></script> <script src="bootstrap/dist/js/bootstrap.js"></script>

View File

@ -34,7 +34,7 @@ describe('sb.services', function () {
expect(module).toBeTruthy(); expect(module).toBeTruthy();
}); });
it('should load cookies module', function () { it('should load resource module', function () {
expect(hasModule('ngCookies')).toBeTruthy(); expect(hasModule('ngResource')).toBeTruthy();
}); });
}); });