/** * Service which exposes the server-defined API as a nice set of helper methods and automatic * callbacks. Any method defined on the server is exposed here as an equivalent method. Also * defines some helper functions for working with API responses. */ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService', function(Restangular, $q, UtilService) { var apiService = {}; var getResource = function(path, opt_background) { var resource = {}; resource.url = path; resource.withOptions = function(options) { this.options = options; return this; }; resource.get = function(processor, opt_errorHandler) { var options = this.options; var performer = Restangular.one(this.url); var result = { 'loading': true, 'value': null, 'hasError': false }; if (opt_background) { performer.withHttpConfig({ 'ignoreLoadingBar': true }); } performer.get(options).then(function(resp) { result.value = processor(resp); result.loading = false; }, function(resp) { result.hasError = true; result.loading = false; if (opt_errorHandler) { opt_errorHandler(resp); } }); return result; }; return resource; }; var buildUrl = function(path, parameters, opt_forcessl) { // We already have /api/v1/ on the URLs, so remove them from the paths. path = path.substr('/api/v1/'.length, path.length); // Build the path, adjusted with the inline parameters. var used = {}; var url = ''; for (var i = 0; i < path.length; ++i) { var c = path[i]; if (c == '{') { var end = path.indexOf('}', i); var varName = path.substr(i + 1, end - i - 1); if (!parameters[varName]) { throw new Error('Missing parameter: ' + varName); } used[varName] = true; url += parameters[varName]; i = end; continue; } url += c; } // Append any query parameters. var isFirst = true; for (var paramName in parameters) { if (!parameters.hasOwnProperty(paramName)) { continue; } if (used[paramName]) { continue; } var value = parameters[paramName]; if (value) { url += isFirst ? '?' : '&'; url += paramName + '=' + encodeURIComponent(value) isFirst = false; } } // If we are forcing SSL, return an absolutel URL with an SSL prefix. if (opt_forcessl) { path = 'https://' + window.location.host + '/api/v1/' + path; } return url; }; var getGenericOperationName = function(userOperationName) { return userOperationName.replace('User', ''); }; var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) { if (userRelatedResource) { if (userRelatedResource[method.toLowerCase()]) { return userRelatedResource[method.toLowerCase()]['operationId']; } } throw new Error('Could not find user operation matching org operation: ' + orgOperationName); }; var freshLoginInProgress = []; var reject = function(msg) { for (var i = 0; i < freshLoginInProgress.length; ++i) { freshLoginInProgress[i].deferred.reject({'data': {'message': msg}}); } freshLoginInProgress = []; }; var retry = function() { for (var i = 0; i < freshLoginInProgress.length; ++i) { freshLoginInProgress[i].retry(); } freshLoginInProgress = []; }; var freshLoginFailCheck = function(opName, opArgs) { return function(resp) { var deferred = $q.defer(); // If the error is a fresh login required, show the dialog. if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') { var retryOperation = function() { apiService[opName].apply(apiService, opArgs).then(function(resp) { deferred.resolve(resp); }, function(resp) { deferred.reject(resp); }); }; var verifyNow = function() { var info = { 'password': $('#freshPassword').val() }; $('#freshPassword').val(''); // Conduct the sign in of the user. apiService.verifyUser(info).then(function() { // On success, retry the operations. if it succeeds, then resolve the // deferred promise with the result. Otherwise, reject the same. retry(); }, function(resp) { // Reject with the sign in error. reject('Invalid verification credentials'); }); }; // Add the retry call to the in progress list. If there is more than a single // in progress call, we skip showing the dialog (since it has already been // shown). freshLoginInProgress.push({ 'deferred': deferred, 'retry': retryOperation }) if (freshLoginInProgress.length > 1) { return deferred.promise; } var box = bootbox.dialog({ "message": 'It has been more than a few minutes since you last logged in, ' + 'so please verify your password to perform this sensitive operation:' + '
' + '' + '
', "title": 'Please Verify', "buttons": { "verify": { "label": "Verify", "className": "btn-success", "callback": verifyNow }, "close": { "label": "Cancel", "className": "btn-default", "callback": function() { reject('Verification canceled') } } } }); box.bind('shown.bs.modal', function(){ box.find("input").focus(); box.find("form").submit(function() { if (!$('#freshPassword').val()) { return; } box.modal('hide'); verifyNow(); }); }); // Return a new promise. We'll accept or reject it based on the result // of the login. return deferred.promise; } // Otherwise, we just 'raise' the error via the reject method on the promise. return $q.reject(resp); }; }; var buildMethodsForOperation = function(operation, method, path, resourceMap) { var operationName = operation['operationId']; var urlPath = path['x-path']; // Add the operation itself. apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) { var one = Restangular.one(buildUrl(urlPath, opt_parameters, opt_forcessl)); if (opt_background) { one.withHttpConfig({ 'ignoreLoadingBar': true }); } var opObj = one['custom' + method.toUpperCase()](opt_options); // If the operation requires_fresh_login, then add a specialized error handler that // will defer the operation's result if sudo is requested. if (operation['x-requires-fresh-login']) { opObj = opObj.catch(freshLoginFailCheck(operationName, arguments)); } return opObj; }; // If the method for the operation is a GET, add an operationAsResource method. if (method == 'get') { apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) { return getResource(buildUrl(urlPath, opt_parameters), opt_background); }; } // If the operation has a user-related operation, then make a generic operation for this operation // that can call both the user and the organization versions of the operation, depending on the // parameters given. if (path['x-user-related']) { var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[path['x-user-related']]); var genericOperationName = getGenericOperationName(userOperationName); apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) { if (orgname) { if (orgname.name) { orgname = orgname.name; } var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background); return apiService[operationName](opt_options, params); } else { return apiService[userOperationName](opt_options, opt_parameters, opt_background); } }; } }; if (!window.__endpoints) { return apiService; } var allowedMethods = ['get', 'post', 'put', 'delete']; var resourceMap = {}; var forEachOperation = function(callback) { for (var path in window.__endpoints) { if (!window.__endpoints.hasOwnProperty(path)) { continue; } for (var method in window.__endpoints[path]) { if (!window.__endpoints[path].hasOwnProperty(method)) { continue; } if (allowedMethods.indexOf(method.toLowerCase()) < 0) { continue; } callback(window.__endpoints[path][method], method, window.__endpoints[path]); } } }; // Build the map of resource names to their objects. forEachOperation(function(operation, method, path) { resourceMap[path['x-name']] = path; }); // Construct the methods for each API endpoint. forEachOperation(function(operation, method, path) { buildMethodsForOperation(operation, method, path, resourceMap); }); apiService.getErrorMessage = function(resp, defaultMessage) { var message = defaultMessage; if (resp['data']) { message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message; } return message; }; apiService.errorDisplay = function(defaultMessage, opt_handler) { return function(resp) { var message = apiService.getErrorMessage(resp, defaultMessage); if (opt_handler) { var handlerMessage = opt_handler(resp); if (handlerMessage) { message = handlerMessage; } } message = UtilService.stringToHTML(message); bootbox.dialog({ "message": message, "title": defaultMessage, "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); }; }; return apiService; }]);