This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/static/js/services/api-service.js

330 lines
10 KiB
JavaScript

/**
* 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', function(Restangular, $q) {
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) {
var operations = userRelatedResource['operations'];
for (var i = 0; i < operations.length; ++i) {
var operation = operations[i];
if (operation['method'].toLowerCase() == method) {
return operation['nickname'];
}
}
}
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
};
var buildMethodsForEndpointResource = function(endpointResource, resourceMap) {
var name = endpointResource['name'];
var operations = endpointResource['operations'];
for (var i = 0; i < operations.length; ++i) {
var operation = operations[i];
buildMethodsForOperation(operation, endpointResource, resourceMap);
}
};
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:' +
'<form style="margin-top: 10px" action="javascript:void(0)">' +
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
'</form>',
"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, resource, resourceMap) {
var method = operation['method'].toLowerCase();
var operationName = operation['nickname'];
var path = resource['path'];
// Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) {
var one = Restangular.one(buildUrl(path, 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['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(path, opt_parameters), opt_background);
};
}
// If the resource has a user-related resource, 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 (resource['quayUserRelated']) {
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[resource['quayUserRelated']]);
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 resourceMap = {};
// Build the map of resource names to their objects.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
resourceMap[endpointResource['name']] = endpointResource;
}
// Construct the methods for each API endpoint.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
buildMethodsForEndpointResource(endpointResource, 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;
}
}
bootbox.dialog({
"message": message,
"title": defaultMessage,
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
};
return apiService;
}]);