gpii-universal
Version:
Cross platform, core components of the GPII personalization infrastructure.
244 lines (222 loc) • 9.87 kB
JavaScript
/**
* GPII Untrusted Settings Data Source
*
* Copyright 2017 OCAD University
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
"use strict";
var fluid = require("infusion"),
gpii = fluid.registerNamespace("gpii");
require("accessRequester");
// To reuse gpii.oauth2.getExpiresIn() and gpii.oauth2.getTimestampExpires()
fluid.require("%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js");
// gpii.flowManager.settingsDataSource provides a get() API that returns a promise
// whose resolved value is user settings. The internal steps performed by this API:
// 1. check the access token requested last time. If it's still valid, use it to request then return user settings;
// 2. If the access token has not been requested or it has expired, request an access token via accessRequester subcomponent;
// 3. Save the received access token as member options;
// 4. Use the access token to request then return user settings.
fluid.defaults("gpii.flowManager.settingsDataSource", {
gradeNames: ["fluid.component"],
// This option should be distributed down from, e.g., gpii.flowManager.config.untrusted.base
cloudURL: null,
settingsGetUrlTemplate: "%cloudURL/%gpiiKey/settings/%device",
settingsPutUrlTemplate: "%cloudURL/%gpiiKey/settings",
accessTokenUrlTemplate: "%cloudURL/access_token",
// The URLs below are calculated from the flow manager base URL and their templates.
settingsGetUrl: {
expander: {
funcName: "fluid.stringTemplate",
args: ["{that}.options.settingsGetUrlTemplate", {
cloudURL: "{that}.options.cloudURL"
}]
}
},
settingsPutUrl: {
expander: {
funcName: "fluid.stringTemplate",
args: ["{that}.options.settingsPutUrlTemplate", {
cloudURL: "{that}.options.cloudURL"
}]
}
},
accessTokenUrl: {
expander: {
funcName: "fluid.stringTemplate",
args: ["{that}.options.accessTokenUrlTemplate", {
cloudURL: "{that}.options.cloudURL"
}]
}
},
// TODO: Reading the client credential from the file system is a temporary solution. This option
// should be removed once a proper access requester is in place (https://issues.gpii.net/browse/GPII-2436).
clientCredentialFilePath: null,
// The minimum number of seconds of the lfe time of an access token for it to continuing to be used.
minAccessTokenLifeTimeInSecond: 10,
distributeOptions: {
settingsGetUrl: {
source: "{that}.options.settingsGetUrl",
target: "{that > settingsDataSourceGetImpl}.options.url"
},
settingsPutUrl: {
source: "{that}.options.settingsPutUrl",
target: "{that > settingsDataSourcePutImpl}.options.url"
},
accessTokenUrl: {
source: "{that}.options.accessTokenUrl",
target: "{that > accessRequester}.options.url"
},
clientCredentialFilePath: {
source: "{that}.options.clientCredentialFilePath",
target: "{that clientCredentialDataSource}.options.path"
}
},
members: {
accessTokens: {
// To keep track of the most recent access token and its expiresIn timestamp for each GPII key. The structure looks like:
// "gpiiKey1": {
// accessToken: "a-accessToken-value",
// timestampExpires: "an-timestamp-the-accessToken-expires"
// }
// ...
}
},
components: {
settingsDataSourceGetImpl: {
type: "kettle.dataSource.URL",
options: {
// url: distributed down from the parent component gpii.flowManager.settingsDataSource
termMap: {
"gpiiKey": "%gpiiKey",
"device": "%device"
}
}
},
settingsDataSourcePutImpl: {
type: "kettle.dataSource.URL",
options: {
// url: distributed down from the parent component gpii.flowManager.settingsDataSource
termMap: {
"gpiiKey": "%gpiiKey"
},
writable: true,
writeMethod: "PUT"
}
},
accessRequester: {
type: "gpii.accessRequester",
options: {
clientCredentialDataSourceGrade: "gpii.accessRequester.clientCredentialDataSource.file"
}
}
},
invokers: {
get: {
funcName: "gpii.flowManager.settingsDataSource.get",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
// gpiiKey, device
},
set: {
funcName: "gpii.flowManager.settingsDataSource.set",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
// gpiiKey, preferences
},
save: {
funcName: "gpii.flowManager.settingsDataSource.save",
args: ["{that}.accessTokens", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
// gpiiKey, accessToken, timestampExpires
}
}
});
/**
* Retrieve user settings from the cloud using the access token requested for the keyed in GPII key.
* @param {Component} that - An instance of gpii.flowManager.settingsDataSource.
* @param {String} gpiiKey - A GPII key.
* @param {Object} device - The device information provided by the device reporter.
* @return {Promise} A promise whose resolved value is the user settings.
*/
gpii.flowManager.settingsDataSource.get = function (that, gpiiKey, device) {
var accessTokenPromise = gpii.flowManager.settingsDataSource.findValidAccessToken(that, gpiiKey);
var promiseTogo = fluid.promise();
accessTokenPromise.then(function (accessToken) {
var settingsPromise = that.settingsDataSourceGetImpl.get({
gpiiKey: gpiiKey,
device: JSON.stringify(device)
}, {
headers: {
"Authorization": "Bearer " + accessToken
}
});
fluid.promise.follow(settingsPromise, promiseTogo);
}, function (err) {
promiseTogo.reject(err);
});
return promiseTogo;
};
/**
* Update user preferences to the cloud using the access token requested for the keyed in GPII key.
* @param {Component} that - An instance of gpii.flowManager.settingsDataSource.
* @param {String} gpiiKey A GPII key.
* @param {Object} preferences The to-be-updated preferences.
* @return {Promise} A promise whose resolved value is the status of the update.
*/
gpii.flowManager.settingsDataSource.set = function (that, gpiiKey, preferences) {
var accessTokenPromise = gpii.flowManager.settingsDataSource.findValidAccessToken(that, gpiiKey);
var promiseTogo = fluid.promise();
accessTokenPromise.then(function (accessToken) {
var updatePromise = that.settingsDataSourcePutImpl.set({
gpiiKey: gpiiKey
}, preferences, {
headers: {
"Authorization": "Bearer " + accessToken
}
});
fluid.promise.follow(updatePromise, promiseTogo);
}, function (err) {
promiseTogo.reject(err);
});
return promiseTogo;
};
/**
* Find a valid access token. It first checks the saved access token for the keyed in GPII key,
* If it has expired, request and return a new one from the cloud, otherwise, return the saved access token.
* @param {Component} that - An instance of gpii.flowManager.settingsDataSource.
* @param {String} gpiiKey - A GPII key.
* @return {Promise} A promise whose resolved value is a valid access token.
*/
gpii.flowManager.settingsDataSource.findValidAccessToken = function (that, gpiiKey) {
var allAccessTokens = that.accessTokens;
var accessToken = fluid.get(allAccessTokens, [gpiiKey, "accessToken"]);
var expiresIn = gpii.oauth2.getExpiresIn(new Date(), fluid.get(allAccessTokens, [gpiiKey, "timestampExpires"]));
// If the locally saved access token exists and is still valid, return it.
// Otherwise, request an new access token from the cloud and return.
// The new access token is saved locally for the continuing use.
if (!accessToken || !expiresIn || expiresIn < that.options.minAccessTokenLifeTimeInSecond) {
var accessTokenPromise = that.accessRequester.getAccessToken(gpiiKey);
var mapper = function (accessTokenObj) {
var accessTokenFromCloud = accessTokenObj.access_token;
var timestampExpiresFromCloud = gpii.oauth2.getTimestampExpires(new Date(), accessTokenObj.expiresIn);
that.save(gpiiKey, accessTokenFromCloud, timestampExpiresFromCloud);
return accessTokenFromCloud;
};
return fluid.promise.map(accessTokenPromise, mapper);
} else {
return fluid.promise().resolve(accessToken);
}
};
/**
* Save the access token and its timestampExpires in the index of the GPII key for the next use.
* @param {Component} allAccessTokens - All saved access tokens
* @param {String} gpiiKey - The GPII key that the access token associates with.
* @param {String} accessToken - The access token to be saved.
* @param {String} timestampExpires - A timestampExpires to be saved.
*/
gpii.flowManager.settingsDataSource.save = function (allAccessTokens, gpiiKey, accessToken, timestampExpires) {
fluid.set(allAccessTokens, [gpiiKey, "accessToken"], accessToken);
fluid.set(allAccessTokens, [gpiiKey, "timestampExpires"], timestampExpires);
};