transposit
Advanced tools
Comparing version 0.7.8 to 1.0.0-rc1
@@ -88,3 +88,3 @@ (function webpackUniversalModuleDefinition(root, factory) { | ||
\***************************/ | ||
/*! exports provided: TRANSPOSIT_CONSUME_KEY_PREFIX, Transposit */ | ||
/*! exports provided: LOCAL_STORAGE_KEY, Transposit */ | ||
/***/ (function(module, __webpack_exports__, __webpack_require__) { | ||
@@ -94,3 +94,3 @@ | ||
__webpack_require__.r(__webpack_exports__); | ||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TRANSPOSIT_CONSUME_KEY_PREFIX", function() { return TRANSPOSIT_CONSUME_KEY_PREFIX; }); | ||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "LOCAL_STORAGE_KEY", function() { return LOCAL_STORAGE_KEY; }); | ||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Transposit", function() { return Transposit; }); | ||
@@ -147,62 +147,66 @@ /* | ||
}; | ||
// From https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | ||
function getParameterByName(name) { | ||
var url = window.location.href; | ||
name = name.replace(/[\[\]]/g, "\\$&"); | ||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"); | ||
var results = regex.exec(url); | ||
if (!results) { | ||
return null; | ||
} | ||
if (!results[2]) { | ||
return ""; | ||
} | ||
return decodeURIComponent(results[2].replace(/\+/g, " ")); | ||
// https://my-app.transposit.io?clientJwt=...needKeys=... -> https://my-app.transposit.io | ||
function hereWithoutSearch() { | ||
return "" + window.location.origin + window.location.pathname; | ||
} | ||
var TRANSPOSIT_CONSUME_KEY_PREFIX = "TRANSPOSIT_CONSUME_KEY"; | ||
var LOCAL_STORAGE_KEY = "TRANSPOSIT_SESSION"; | ||
var Transposit = /** @class */ (function () { | ||
function Transposit(serviceMaintainer, serviceName, tpsHostedAppApiUrl) { | ||
if (tpsHostedAppApiUrl === void 0) { tpsHostedAppApiUrl = "https://api.transposit.com"; } | ||
this.serviceMaintainer = serviceMaintainer; | ||
this.serviceName = serviceName; | ||
this.tpsHostedAppApiUrl = tpsHostedAppApiUrl; | ||
function Transposit(hostedAppOrigin) { | ||
if (hostedAppOrigin === void 0) { hostedAppOrigin = ""; } | ||
this.hostedAppOrigin = hostedAppOrigin; | ||
this.claims = null; | ||
this.claims = this.loadClaims(); | ||
} | ||
Transposit.prototype.getConsumeKey = function () { | ||
return TRANSPOSIT_CONSUME_KEY_PREFIX + "/" + this.serviceMaintainer + "/" + this.serviceName; | ||
Transposit.prototype.uri = function (path) { | ||
if (path === void 0) { path = ""; } | ||
return "" + this.hostedAppOrigin + path; | ||
}; | ||
Transposit.prototype.retrieveClientClaims = function () { | ||
var clientClaimJSON = localStorage.getItem(this.getConsumeKey()); | ||
if (!clientClaimJSON) { | ||
Transposit.prototype.loadClaims = function () { | ||
var claimsJSON = localStorage.getItem(LOCAL_STORAGE_KEY); | ||
if (!claimsJSON) { | ||
return null; | ||
} | ||
return JSON.parse(clientClaimJSON); | ||
var claims; | ||
try { | ||
claims = JSON.parse(claimsJSON); | ||
} | ||
catch (err) { | ||
return null; | ||
} | ||
if (!this.checkClaimsValid(claims)) { | ||
return null; | ||
} | ||
return claims; | ||
}; | ||
Transposit.prototype.areClientClaimsValid = function (clientClaims) { | ||
var expiration = clientClaims.exp * 1000; | ||
Transposit.prototype.persistClaims = function (claimsJSON) { | ||
localStorage.setItem(LOCAL_STORAGE_KEY, claimsJSON); | ||
}; | ||
Transposit.prototype.clearClaims = function () { | ||
localStorage.removeItem(LOCAL_STORAGE_KEY); | ||
}; | ||
Transposit.prototype.checkClaimsValid = function (claims) { | ||
var expiration = claims.exp * 1000; | ||
var now = Date.now(); | ||
return expiration > now; | ||
}; | ||
Transposit.prototype.persistClientClaims = function (clientClaimsJSON) { | ||
localStorage.setItem(this.getConsumeKey(), clientClaimsJSON); | ||
Transposit.prototype.assertLoggedIn = function () { | ||
if (this.claims === null) { | ||
throw new Error("No client claims found."); | ||
} | ||
}; | ||
Transposit.prototype.clearClientClaims = function () { | ||
localStorage.removeItem(this.getConsumeKey()); | ||
Transposit.prototype.isLoggedIn = function () { | ||
return this.claims !== null; | ||
}; | ||
Transposit.prototype.apiUrl = function (relativePath) { | ||
if (relativePath === void 0) { relativePath = ""; } | ||
return this.tpsHostedAppApiUrl + "/app/" + this.serviceMaintainer + "/" + this.serviceName + relativePath; | ||
}; | ||
Transposit.prototype.handleLogin = function (callback) { | ||
// Read query parameters | ||
var maybeClientJwtString = getParameterByName("clientJwt"); | ||
if (maybeClientJwtString === null) { | ||
var searchParams = new URLSearchParams(window.location.search); | ||
if (!searchParams.has("clientJwt")) { | ||
throw new Error("clientJwt query parameter could not be found. This method should only be called after redirection during login."); | ||
} | ||
var clientJwtString = maybeClientJwtString; | ||
var maybeNeedsKeys = getParameterByName("needsKeys"); | ||
if (maybeNeedsKeys === null) { | ||
var clientJwtString = searchParams.get("clientJwt"); | ||
if (!searchParams.has("needsKeys")) { | ||
throw new Error("needsKeys query parameter could not be found. This is unexpected."); | ||
} | ||
var needsKeys = maybeNeedsKeys === "true"; | ||
// Parse JWT string and persist claims | ||
var needsKeys = searchParams.get("needsKeys") === "true"; | ||
// Parse JWT string | ||
var jwtParts = clientJwtString.split("."); | ||
@@ -212,5 +216,5 @@ if (jwtParts.length !== 3) { | ||
} | ||
var clientClaimsJSON; | ||
var claimsJSON; | ||
try { | ||
clientClaimsJSON = atob(jwtParts[1]); | ||
claimsJSON = atob(jwtParts[1]); | ||
} | ||
@@ -220,4 +224,5 @@ catch (err) { | ||
} | ||
var claims; | ||
try { | ||
JSON.parse(clientClaimsJSON); // validate JSON | ||
claims = JSON.parse(claimsJSON); | ||
} | ||
@@ -227,4 +232,9 @@ catch (err) { | ||
} | ||
this.persistClientClaims(clientClaimsJSON); | ||
// Login has succeeded, either callback or default path replacement | ||
if (!this.checkClaimsValid(claims)) { | ||
throw new Error("clientJwt query parameter does not appear to be a valid JWT string. clientJwt is expired."); | ||
} | ||
// Persist claims. Login has succeeded. | ||
this.claims = claims; | ||
this.persistClaims(claimsJSON); | ||
// Perform callback or default path replacement | ||
if (callback) { | ||
@@ -238,4 +248,3 @@ if (typeof callback !== "function") { | ||
if (needsKeys) { | ||
var hereWithoutQueryParameters = window.location.protocol + "//" + window.location.host + window.location.pathname; | ||
window.location.href = this.getConnectLocation(hereWithoutQueryParameters); | ||
window.location.href = this.settingsUri(hereWithoutSearch()); | ||
} | ||
@@ -247,42 +256,17 @@ else { | ||
}; | ||
Transposit.prototype.logOut = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var clientClaims, err_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
clientClaims = this.retrieveClientClaims(); | ||
if (!clientClaims) { | ||
// Already logged out, nothing to do | ||
return [2 /*return*/]; | ||
} | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, fetch(this.apiUrl("/api/v1/logout"), { | ||
credentials: "include", | ||
method: "POST", | ||
headers: { | ||
"content-type": "application/json", | ||
"X-PUBLIC-TOKEN": clientClaims.publicToken, | ||
}, | ||
})]; | ||
case 2: | ||
_a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
err_1 = _a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 4: | ||
// Delete JWT from local storage | ||
this.clearClientClaims(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
Transposit.prototype.logOut = function (redirectUri) { | ||
this.clearClaims(); | ||
this.claims = null; | ||
window.location.href = this.uri("/logout?redirectUri=" + encodeURIComponent(redirectUri)); | ||
}; | ||
Transposit.prototype.getConnectLocation = function (requestUri) { | ||
return this.apiUrl("/connect?redirectUri=" + | ||
encodeURIComponent(requestUri || window.location.href)); | ||
Transposit.prototype.settingsUri = function (redirectUri) { | ||
return this.uri("/settings?redirectUri=" + encodeURIComponent(redirectUri || window.location.href)); | ||
}; | ||
Transposit.prototype.startLoginUri = function (redirectUri) { | ||
return this.uri("/login/accounts?redirectUri=" + encodeURIComponent(redirectUri || window.location.href)); | ||
}; | ||
// Deprecated in favor of settingsUri | ||
Transposit.prototype.getConnectLocation = function (redirectUri) { | ||
return this.settingsUri(redirectUri); | ||
}; | ||
// Deprecated in favor of startLoginUri | ||
@@ -292,48 +276,31 @@ Transposit.prototype.getGoogleLoginLocation = function (redirectUri) { | ||
}; | ||
Transposit.prototype.startLoginUri = function (redirectUri) { | ||
return this.apiUrl("/login/accounts?redirectUri=" + | ||
encodeURIComponent(redirectUri || window.location.href)); | ||
}; | ||
// Deprecated | ||
Transposit.prototype.getLoginLocation = function () { | ||
return this.apiUrl("/login"); | ||
return this.uri("/login"); | ||
}; | ||
Transposit.prototype.getUserEmail = function () { | ||
var clientClaims = this.retrieveClientClaims(); | ||
if (!clientClaims) { | ||
return null; | ||
} | ||
return clientClaims.email; | ||
this.assertLoggedIn(); | ||
return this.claims.email; | ||
}; | ||
Transposit.prototype.getUserName = function () { | ||
var clientClaims = this.retrieveClientClaims(); | ||
if (!clientClaims) { | ||
return null; | ||
} | ||
return clientClaims.name; | ||
this.assertLoggedIn(); | ||
return this.claims.name; | ||
}; | ||
Transposit.prototype.isLoggedIn = function () { | ||
var clientClaims = this.retrieveClientClaims(); | ||
return clientClaims !== null && this.areClientClaimsValid(clientClaims); | ||
}; | ||
Transposit.prototype.runOperation = function (operationId, params) { | ||
if (params === void 0) { params = {}; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var headerInfo, clientClaims, response, e_1; | ||
var headers, response; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
headerInfo = { | ||
"content-type": "application/json", | ||
headers = { | ||
"Content-Type": "application/json", | ||
}; | ||
clientClaims = this.retrieveClientClaims(); | ||
if (clientClaims) { | ||
headerInfo["X-PUBLIC-TOKEN"] = clientClaims.publicToken; | ||
if (this.claims) { | ||
headers["X-PUBLIC-TOKEN"] = this.claims.publicToken; | ||
} | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 6, , 7]); | ||
return [4 /*yield*/, fetch(this.apiUrl("/api/v1/execute/" + operationId), { | ||
return [4 /*yield*/, fetch(this.uri("/api/v1/execute/" + operationId), { | ||
credentials: "include", | ||
method: "POST", | ||
headers: headerInfo, | ||
headers: headers, | ||
body: JSON.stringify({ | ||
@@ -343,13 +310,8 @@ parameters: params, | ||
})]; | ||
case 2: | ||
case 1: | ||
response = _a.sent(); | ||
if (!(response.status >= 200 && response.status < 300)) return [3 /*break*/, 4]; | ||
if (!(response.status >= 200 && response.status < 300)) return [3 /*break*/, 3]; | ||
return [4 /*yield*/, response.json()]; | ||
case 3: return [2 /*return*/, (_a.sent())]; | ||
case 4: throw response; | ||
case 5: return [3 /*break*/, 7]; | ||
case 6: | ||
e_1 = _a.sent(); | ||
throw e_1; | ||
case 7: return [2 /*return*/]; | ||
case 2: return [2 /*return*/, (_a.sent())]; | ||
case 3: throw response; | ||
} | ||
@@ -370,3 +332,3 @@ }); | ||
\**********************/ | ||
/*! exports provided: TRANSPOSIT_CONSUME_KEY_PREFIX, Transposit */ | ||
/*! exports provided: LOCAL_STORAGE_KEY, Transposit */ | ||
/***/ (function(module, __webpack_exports__, __webpack_require__) { | ||
@@ -377,3 +339,3 @@ | ||
/* harmony import */ var _Transposit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Transposit */ "./src/Transposit.ts"); | ||
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TRANSPOSIT_CONSUME_KEY_PREFIX", function() { return _Transposit__WEBPACK_IMPORTED_MODULE_0__["TRANSPOSIT_CONSUME_KEY_PREFIX"]; }); | ||
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "LOCAL_STORAGE_KEY", function() { return _Transposit__WEBPACK_IMPORTED_MODULE_0__["LOCAL_STORAGE_KEY"]; }); | ||
@@ -380,0 +342,0 @@ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Transposit", function() { return _Transposit__WEBPACK_IMPORTED_MODULE_0__["Transposit"]; }); |
@@ -1,2 +0,2 @@ | ||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Transposit=t():e.Transposit=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);var r=function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{s(r.next(e))}catch(e){i(e)}}function l(e){try{s(r.throw(e))}catch(e){i(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,l)}s((r=r.apply(e,t||[])).next())})},o=function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function l(i){return function(l){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,l])}}};function i(e){var t=window.location.href;e=e.replace(/[\[\]]/g,"\\$&");var n=new RegExp("[?&]"+e+"(=([^&#]*)|&|#|$)").exec(t);return n?n[2]?decodeURIComponent(n[2].replace(/\+/g," ")):"":null}var a=function(){function e(e,t,n){void 0===n&&(n="https://api.transposit.com"),this.serviceMaintainer=e,this.serviceName=t,this.tpsHostedAppApiUrl=n}return e.prototype.getConsumeKey=function(){return"TRANSPOSIT_CONSUME_KEY/"+this.serviceMaintainer+"/"+this.serviceName},e.prototype.retrieveClientClaims=function(){var e=localStorage.getItem(this.getConsumeKey());return e?JSON.parse(e):null},e.prototype.areClientClaimsValid=function(e){return 1e3*e.exp>Date.now()},e.prototype.persistClientClaims=function(e){localStorage.setItem(this.getConsumeKey(),e)},e.prototype.clearClientClaims=function(){localStorage.removeItem(this.getConsumeKey())},e.prototype.apiUrl=function(e){return void 0===e&&(e=""),this.tpsHostedAppApiUrl+"/app/"+this.serviceMaintainer+"/"+this.serviceName+e},e.prototype.handleLogin=function(e){var t=i("clientJwt");if(null===t)throw new Error("clientJwt query parameter could not be found. This method should only be called after redirection during login.");var n=t,r=i("needsKeys");if(null===r)throw new Error("needsKeys query parameter could not be found. This is unexpected.");var o,a="true"===r,l=n.split(".");if(3!==l.length)throw new Error("clientJwt query parameter does not appear to be a valid JWT string. This method should only be called after redirection during login.");try{o=atob(l[1])}catch(e){throw new Error("clientJwt query parameter does not appear to be a valid JWT string. This method should only be called after redirection during login.")}try{JSON.parse(o)}catch(e){throw new Error("clientJwt query parameter does not appear to be a valid JWT string. This method should only be called after redirection during login.")}if(this.persistClientClaims(o),e){if("function"!=typeof e)throw new Error("Provided callback is not a function.");e({needsKeys:a})}else if(a){var s=window.location.protocol+"//"+window.location.host+window.location.pathname;window.location.href=this.getConnectLocation(s)}else window.history.replaceState({},document.title,window.location.pathname)},e.prototype.logOut=function(){return r(this,void 0,void 0,function(){var e;return o(this,function(t){switch(t.label){case 0:if(!(e=this.retrieveClientClaims()))return[2];t.label=1;case 1:return t.trys.push([1,3,,4]),[4,fetch(this.apiUrl("/api/v1/logout"),{credentials:"include",method:"POST",headers:{"content-type":"application/json","X-PUBLIC-TOKEN":e.publicToken}})];case 2:return t.sent(),[3,4];case 3:return t.sent(),[3,4];case 4:return this.clearClientClaims(),[2]}})})},e.prototype.getConnectLocation=function(e){return this.apiUrl("/connect?redirectUri="+encodeURIComponent(e||window.location.href))},e.prototype.getGoogleLoginLocation=function(e){return this.startLoginUri(e)},e.prototype.startLoginUri=function(e){return this.apiUrl("/login/accounts?redirectUri="+encodeURIComponent(e||window.location.href))},e.prototype.getLoginLocation=function(){return this.apiUrl("/login")},e.prototype.getUserEmail=function(){var e=this.retrieveClientClaims();return e?e.email:null},e.prototype.getUserName=function(){var e=this.retrieveClientClaims();return e?e.name:null},e.prototype.isLoggedIn=function(){var e=this.retrieveClientClaims();return null!==e&&this.areClientClaimsValid(e)},e.prototype.runOperation=function(e,t){return void 0===t&&(t={}),r(this,void 0,void 0,function(){var n,r,i;return o(this,function(o){switch(o.label){case 0:n={"content-type":"application/json"},(r=this.retrieveClientClaims())&&(n["X-PUBLIC-TOKEN"]=r.publicToken),o.label=1;case 1:return o.trys.push([1,6,,7]),[4,fetch(this.apiUrl("/api/v1/execute/"+e),{credentials:"include",method:"POST",headers:n,body:JSON.stringify({parameters:t})})];case 2:return(i=o.sent()).status>=200&&i.status<300?[4,i.json()]:[3,4];case 3:return[2,o.sent()];case 4:throw i;case 5:return[3,7];case 6:throw o.sent();case 7:return[2]}})})},e}();n.d(t,"TRANSPOSIT_CONSUME_KEY_PREFIX",function(){return"TRANSPOSIT_CONSUME_KEY"}),n.d(t,"Transposit",function(){return a})}])}); | ||
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Transposit=e():t.Transposit=e()}(window,function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.r=function(t){Object.defineProperty(t,"__esModule",{value:!0})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";n.r(e);var r=function(t,e,n,r){return new(n||(n=Promise))(function(o,i){function a(t){try{c(r.next(t))}catch(t){i(t)}}function s(t){try{c(r.throw(t))}catch(t){i(t)}}function c(t){t.done?o(t.value):new n(function(e){e(t.value)}).then(a,s)}c((r=r.apply(t,e||[])).next())})},o=function(t,e){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=e.call(t,a)}catch(t){i=[6,t],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};var i=function(){function t(t){void 0===t&&(t=""),this.hostedAppOrigin=t,this.claims=null,this.claims=this.loadClaims()}return t.prototype.uri=function(t){return void 0===t&&(t=""),""+this.hostedAppOrigin+t},t.prototype.loadClaims=function(){var t,e=localStorage.getItem("TRANSPOSIT_SESSION");if(!e)return null;try{t=JSON.parse(e)}catch(t){return null}return this.checkClaimsValid(t)?t:null},t.prototype.persistClaims=function(t){localStorage.setItem("TRANSPOSIT_SESSION",t)},t.prototype.clearClaims=function(){localStorage.removeItem("TRANSPOSIT_SESSION")},t.prototype.checkClaimsValid=function(t){return 1e3*t.exp>Date.now()},t.prototype.assertLoggedIn=function(){if(null===this.claims)throw new Error("No client claims found.")},t.prototype.isLoggedIn=function(){return null!==this.claims},t.prototype.handleLogin=function(t){var e=new URLSearchParams(window.location.search);if(!e.has("clientJwt"))throw new Error("clientJwt query parameter could not be found. This method should only be called after redirection during login.");var n=e.get("clientJwt");if(!e.has("needsKeys"))throw new Error("needsKeys query parameter could not be found. This is unexpected.");var r,o,i="true"===e.get("needsKeys"),a=n.split(".");if(3!==a.length)throw new Error("clientJwt query parameter does not appear to be a valid JWT string. This method should only be called after redirection during login.");try{r=atob(a[1])}catch(t){throw new Error("clientJwt query parameter does not appear to be a valid JWT string. This method should only be called after redirection during login.")}try{o=JSON.parse(r)}catch(t){throw new Error("clientJwt query parameter does not appear to be a valid JWT string. This method should only be called after redirection during login.")}if(!this.checkClaimsValid(o))throw new Error("clientJwt query parameter does not appear to be a valid JWT string. clientJwt is expired.");if(this.claims=o,this.persistClaims(r),t){if("function"!=typeof t)throw new Error("Provided callback is not a function.");t({needsKeys:i})}else i?window.location.href=this.settingsUri(""+window.location.origin+window.location.pathname):window.history.replaceState({},document.title,window.location.pathname)},t.prototype.logOut=function(t){this.clearClaims(),this.claims=null,window.location.href=this.uri("/logout?redirectUri="+encodeURIComponent(t))},t.prototype.settingsUri=function(t){return this.uri("/settings?redirectUri="+encodeURIComponent(t||window.location.href))},t.prototype.startLoginUri=function(t){return this.uri("/login/accounts?redirectUri="+encodeURIComponent(t||window.location.href))},t.prototype.getConnectLocation=function(t){return this.settingsUri(t)},t.prototype.getGoogleLoginLocation=function(t){return this.startLoginUri(t)},t.prototype.getLoginLocation=function(){return this.uri("/login")},t.prototype.getUserEmail=function(){return this.assertLoggedIn(),this.claims.email},t.prototype.getUserName=function(){return this.assertLoggedIn(),this.claims.name},t.prototype.runOperation=function(t,e){return void 0===e&&(e={}),r(this,void 0,void 0,function(){var n,r;return o(this,function(o){switch(o.label){case 0:return n={"Content-Type":"application/json"},this.claims&&(n["X-PUBLIC-TOKEN"]=this.claims.publicToken),[4,fetch(this.uri("/api/v1/execute/"+t),{credentials:"include",method:"POST",headers:n,body:JSON.stringify({parameters:e})})];case 1:return(r=o.sent()).status>=200&&r.status<300?[4,r.json()]:[3,3];case 2:return[2,o.sent()];case 3:throw r}})})},t}();n.d(e,"LOCAL_STORAGE_KEY",function(){return"TRANSPOSIT_SESSION"}),n.d(e,"Transposit",function(){return i})}])}); | ||
//# sourceMappingURL=bundle.prod.map |
@@ -15,26 +15,26 @@ import { EndRequestLog } from "."; | ||
} | ||
export declare const TRANSPOSIT_CONSUME_KEY_PREFIX = "TRANSPOSIT_CONSUME_KEY"; | ||
export declare const LOCAL_STORAGE_KEY = "TRANSPOSIT_SESSION"; | ||
export declare class Transposit { | ||
private serviceMaintainer; | ||
private serviceName; | ||
private tpsHostedAppApiUrl; | ||
constructor(serviceMaintainer: string, serviceName: string, tpsHostedAppApiUrl?: string); | ||
private getConsumeKey; | ||
private retrieveClientClaims; | ||
private areClientClaimsValid; | ||
private persistClientClaims; | ||
private clearClientClaims; | ||
private apiUrl; | ||
private hostedAppOrigin; | ||
private claims; | ||
constructor(hostedAppOrigin?: string); | ||
private uri; | ||
private loadClaims; | ||
private persistClaims; | ||
private clearClaims; | ||
private checkClaimsValid; | ||
private assertLoggedIn; | ||
isLoggedIn(): boolean; | ||
handleLogin(callback?: (info: { | ||
needsKeys: boolean; | ||
}) => void): void; | ||
logOut(): Promise<void>; | ||
getConnectLocation(requestUri?: string): string; | ||
logOut(redirectUri: string): void; | ||
settingsUri(redirectUri?: string): string; | ||
startLoginUri(redirectUri?: string): string; | ||
getConnectLocation(redirectUri?: string): string; | ||
getGoogleLoginLocation(redirectUri?: string): string; | ||
startLoginUri(redirectUri?: string): string; | ||
getLoginLocation(): string; | ||
getUserEmail(): string | null; | ||
getUserName(): string | null; | ||
isLoggedIn(): boolean; | ||
runOperation(operationId: string, params?: OperationParameters): Promise<EndRequestLog>; | ||
} |
@@ -23,3 +23,3 @@ --- | ||
```markup | ||
<script src="https://unpkg.com/transposit@0.7.2/dist/bundle.prod.js"></script> | ||
<script src="https://unpkg.com/transposit@1.0.0/dist/bundle.prod.js"></script> | ||
``` | ||
@@ -40,3 +40,3 @@ | ||
const transposit = new Transposit(serviceMaintainer, serviceName); | ||
const transposit = new Transposit("https://hello-world-xyz12.transposit.io"); | ||
``` | ||
@@ -49,3 +49,3 @@ | ||
const transposit = new Transposit(serviceMaintainer, serviceName); | ||
const transposit = new Transposit("https://hello-world-xyz12.transposit.io"); | ||
``` | ||
@@ -56,3 +56,3 @@ | ||
```javascript | ||
var transposit = new Transposit.Transposit(serviceMaintainer, serviceName); | ||
var transposit = new Transposit.Transposit("https://hello-world-xyz12.transposit.io"); | ||
``` | ||
@@ -82,8 +82,8 @@ | ||
### Authorizations | ||
### Settings | ||
If your application requires user credentials for data connections, send users to the Transposit-hosted data connections page. You can get the URL for this page from the SDK: | ||
If your application requires user credentials for data connections, send users to the Transposit-hosted settings page. You can get the URL for this page from the SDK: | ||
```javascript | ||
transposit.getConnectionLocation(); | ||
transposit.settingsUri(); | ||
``` | ||
@@ -90,0 +90,0 @@ |
@@ -39,8 +39,12 @@ # Reference | ||
`transposit.logOut()` | ||
`transposit.logOut(redirectUri)` | ||
Invalidates stored claims and clears them from localStorage. | ||
Invalidates stored claims and clears them from localStorage. Redirects to Tranposit's logout page. | ||
**Returns**: Promise<void> | ||
| Argument | Type | | | ||
| :---------- | :----- | :---------------------------------------------------- | | ||
| redirectUri | String | where to end up after successful logout | | ||
**Returns**: void | ||
## Run operation | ||
@@ -69,5 +73,5 @@ | ||
## Get connect location | ||
## Get settings uri | ||
`transposit.getConnectLocation([redirectUri=window.location.href])` | ||
`transposit.settingsUri([redirectUri=window.location.href])` | ||
@@ -78,3 +82,3 @@ | Argument | Type | | | ||
**Returns** (String): A url to redirect to for user authorization. | ||
**Returns** (String): A url to redirect to for user settings. | ||
@@ -84,4 +88,4 @@ **Example** | ||
```javascript | ||
transposit.getConnectLocation("localhost"); | ||
// => "https://api.transposit.com/app/v1/gardener/hose/connect?redirectUri=localhost" | ||
transposit.settingsUri("https://localhost"); | ||
// => "https://hello-world-xyz12.transposit.io?redirectUri=https%3A%2F%2Flocalhost" | ||
``` | ||
@@ -102,4 +106,4 @@ | ||
```javascript | ||
transposit.getConnectLocation("localhost"); | ||
// => "https://api.transposit.com/app/v1/gardener/hose/login/accounts?redirectUri=localhost" | ||
transposit.startLoginUri("https://localhost"); | ||
// => "https://hello-world-xyz12.transposit.io/login/accounts?redirectUri=https%3A%2F%2Flocalhost" | ||
``` | ||
@@ -106,0 +110,0 @@ |
{ | ||
"name": "transposit", | ||
"version": "0.7.8", | ||
"version": "1.0.0-rc1", | ||
"author": "Transposit", | ||
@@ -5,0 +5,0 @@ "description": "SDK for web apps using Transposit as a backend", |
@@ -22,6 +22,6 @@ <img src="https://www.transposit.com/img/transposit-logo-black.png" width="182px" alt="Transposit"/> | ||
```html | ||
<script src="https://unpkg.com/transposit@0.7.3/dist/bundle.prod.js"></script> | ||
<script src="https://unpkg.com/transposit@1.0.0/dist/bundle.prod.js"></script> | ||
``` | ||
Instantiate the SDK with the `maintainer`/`name` pair that uniquely identifies your application: | ||
Instantiate the SDK with the hosted app origin that uniquely identifies your application: | ||
@@ -31,3 +31,3 @@ ```javascript | ||
const transposit = new Transposit("jplace", "hello_world"); | ||
const transposit = new Transposit("https://hello-world-xyz12.transposit.io"); | ||
``` | ||
@@ -37,3 +37,3 @@ | ||
<script> | ||
const transposit = new Transposit.Transposit("jplace", "hello_world"); | ||
const transposit = new Transposit.Transposit("https://hello-world-xyz12.transposit.io"); | ||
</script> | ||
@@ -123,5 +123,3 @@ ``` | ||
function signout() { | ||
transposit.logOut().then(() => { | ||
window.location.href = "/signin"; | ||
}); | ||
transposit.logOut(`${window.location.origin}/signin`); | ||
} | ||
@@ -133,9 +131,9 @@ </script> | ||
Allow users to grant access to their third-party data. Use the SDK to link users to a Transposit page where they can securely provide credentials. | ||
Allow users to grant access to their third-party data. Use the SDK to link users to the Transposit settings page. | ||
```html | ||
<button type="button" onclick="connect()">Connect</button> | ||
<button type="button" onclick="settings()">Settings</button> | ||
<script> | ||
function connect() { | ||
window.location.href = transposit.getConnectLocation(); | ||
function settings() { | ||
window.location.href = transposit.settingsUri(); | ||
} | ||
@@ -142,0 +140,0 @@ </script> |
@@ -18,7 +18,16 @@ /* | ||
import * as MockDate from "mockdate"; | ||
import { Transposit, TRANSPOSIT_CONSUME_KEY_PREFIX } from "../Transposit"; | ||
import { ClientClaims, LOCAL_STORAGE_KEY, Transposit } from "../Transposit"; | ||
import DoneCallback = jest.DoneCallback; | ||
function createUnsignedJwt(claims: any): string { | ||
const ARBYS_ORIGIN: string = "https://arbys-beef-xyz12.transposit.io"; | ||
function arbysUri(path: string = ""): string { | ||
return `${ARBYS_ORIGIN}${path}`; | ||
} | ||
const NOW_MINUS_3_DAYS: number = 1521996119000; | ||
const NOW: number = 1522255319000; | ||
const NOW_PLUS_3_DAYS: number = 1522514519000; | ||
function createUnsignedJwt(claims: ClientClaims): string { | ||
const header: string = btoa(JSON.stringify({ alg: "none" })); | ||
@@ -29,65 +38,81 @@ const body: string = btoa(JSON.stringify(claims)); | ||
function setHref(origin: string, pathname: string, search: string): void { | ||
window.location.href = `${origin}${pathname}${search}`; | ||
(window.location as any).origin = origin; // origin is normally read-only, but not in tests :) | ||
window.location.pathname = pathname; | ||
window.location.search = search; | ||
} | ||
describe("Transposit", () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
MockDate.reset(); | ||
localStorage.clear(); | ||
MockDate.set(NOW); | ||
setHref(ARBYS_ORIGIN, "/", ""); | ||
}); | ||
const jplaceArbysClaims: any = Object.freeze({ | ||
iss: "https://api.transposit.com", | ||
const jplaceArbysClaims: ClientClaims = Object.freeze({ | ||
iss: ARBYS_ORIGIN, | ||
sub: "jplace@transposit.com", | ||
exp: 1522255319, | ||
iat: 1521650519, | ||
exp: NOW_PLUS_3_DAYS / 1000, | ||
iat: NOW_MINUS_3_DAYS / 1000, | ||
publicToken: "thisisapublictoken", | ||
repository: "jplace/arbys_beef", | ||
email: "jplace@transposit.com", | ||
name: "Jordan 'The Beef' Place", | ||
}); | ||
const jplaceArbysJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
function makeArbysTransposit(): Transposit { | ||
return new Transposit("jplace", "arbys_beef"); | ||
} | ||
describe("isLoggedIn", () => { | ||
it("knows when you've just logged in", () => { | ||
setHref( | ||
ARBYS_ORIGIN, | ||
"/", | ||
`?clientJwt=${jplaceArbysJwt}&needsKeys=false`, | ||
); | ||
describe("getGoogleLoginLocation", () => { | ||
it("returns the correct location", () => { | ||
const transposit: Transposit = new Transposit("jplace", "arbys_beef"); | ||
const transposit: Transposit = new Transposit(); | ||
transposit.handleLogin(); | ||
expect(transposit.getGoogleLoginLocation("https://altoids.com")).toEqual( | ||
"https://api.transposit.com/app/jplace/arbys_beef/login/accounts?redirectUri=https%3A%2F%2Faltoids.com", | ||
); | ||
expect(transposit.startLoginUri("https://altoids.com")).toEqual( | ||
"https://api.transposit.com/app/jplace/arbys_beef/login/accounts?redirectUri=https%3A%2F%2Faltoids.com", | ||
); | ||
expect(transposit.isLoggedIn()).toBe(true); | ||
}); | ||
it("returns the correct location (non-default transposit location)", () => { | ||
const transposit: Transposit = new Transposit( | ||
"jplace", | ||
"arbys_beef", | ||
"https://monkey.transposit.com", | ||
it("knows when you're logged out", () => { | ||
const transposit: Transposit = new Transposit(); | ||
expect(transposit.isLoggedIn()).toBe(false); | ||
}); | ||
it("knows when your session expired", () => { | ||
setHref( | ||
ARBYS_ORIGIN, | ||
"/", | ||
`?clientJwt=${jplaceArbysJwt}&needsKeys=false`, | ||
); | ||
expect(transposit.getGoogleLoginLocation("https://altoids.com")).toEqual( | ||
"https://monkey.transposit.com/app/jplace/arbys_beef/login/accounts?redirectUri=https%3A%2F%2Faltoids.com", | ||
); | ||
expect(transposit.startLoginUri("https://altoids.com")).toEqual( | ||
"https://monkey.transposit.com/app/jplace/arbys_beef/login/accounts?redirectUri=https%3A%2F%2Faltoids.com", | ||
); | ||
let transposit: Transposit = new Transposit(); | ||
transposit.handleLogin(); | ||
// 3 days after expiration | ||
MockDate.set((jplaceArbysClaims.exp + 60 * 60 * 24 * 3) * 1000); | ||
transposit = new Transposit(); | ||
expect(transposit.isLoggedIn()).toBe(false); | ||
}); | ||
}); | ||
describe("login", () => { | ||
it("redirects on login", () => { | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
describe("handleLogin", () => { | ||
it("calls replaceState", () => { | ||
setHref( | ||
ARBYS_ORIGIN, | ||
"/", | ||
`?clientJwt=${jplaceArbysJwt}&needsKeys=false`, | ||
); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=false`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
transposit.handleLogin(); | ||
expect( | ||
JSON.parse( | ||
localStorage.getItem( | ||
`${TRANSPOSIT_CONSUME_KEY_PREFIX}/jplace/arbys_beef`, | ||
)!, | ||
), | ||
).toEqual(jplaceArbysClaims); | ||
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!)).toEqual( | ||
jplaceArbysClaims, | ||
); | ||
expect(window.history.replaceState).toHaveBeenCalledWith( | ||
@@ -101,50 +126,34 @@ {}, | ||
it("redirects when needs keys", () => { | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
setHref(ARBYS_ORIGIN, "/", `?clientJwt=${jplaceArbysJwt}&needsKeys=true`); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=true`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
transposit.handleLogin(); | ||
expect( | ||
JSON.parse( | ||
localStorage.getItem( | ||
`${TRANSPOSIT_CONSUME_KEY_PREFIX}/jplace/arbys_beef`, | ||
)!, | ||
), | ||
).toEqual(jplaceArbysClaims); | ||
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!)).toEqual( | ||
jplaceArbysClaims, | ||
); | ||
expect(window.location.href).toEqual( | ||
"https://api.transposit.com/app/jplace/arbys_beef/connect?redirectUri=http%3A%2F%2Flocalhost%2F", | ||
"/settings?redirectUri=https%3A%2F%2Farbys-beef-xyz12.transposit.io%2F", | ||
); | ||
}); | ||
it("calls callback on login", () => { | ||
it("calls callback", () => { | ||
setHref(ARBYS_ORIGIN, "/", `?clientJwt=${jplaceArbysJwt}&needsKeys=true`); | ||
const mockCallback = jest.fn(); | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=true`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
transposit.handleLogin(mockCallback); | ||
expect( | ||
JSON.parse( | ||
localStorage.getItem( | ||
`${TRANSPOSIT_CONSUME_KEY_PREFIX}/jplace/arbys_beef`, | ||
)!, | ||
), | ||
).toEqual(jplaceArbysClaims); | ||
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!)).toEqual( | ||
jplaceArbysClaims, | ||
); | ||
expect(mockCallback).toHaveBeenCalledWith({ needsKeys: true }); | ||
expect(window.history.replaceState).not.toHaveBeenCalled(); | ||
expect(window.location.href).toEqual( | ||
`https://arbys.com/?clientJwt=${clientJwt}&needsKeys=true`, | ||
); | ||
}); | ||
it("throws if callback is not a function", (done: DoneCallback) => { | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
setHref(ARBYS_ORIGIN, "/", `?clientJwt=${jplaceArbysJwt}&needsKeys=true`); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=true`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
try { | ||
@@ -160,5 +169,5 @@ transposit.handleLogin("string" as any); | ||
it("throws without jwt", (done: DoneCallback) => { | ||
window.location.href = `https://arbys.com/`; | ||
setHref(ARBYS_ORIGIN, "/", ""); | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
try { | ||
@@ -176,7 +185,5 @@ transposit.handleLogin(); | ||
it("throws without needsKeys", (done: DoneCallback) => { | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
setHref(ARBYS_ORIGIN, "/", `?clientJwt=${jplaceArbysJwt}`); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
try { | ||
@@ -194,5 +201,5 @@ transposit.handleLogin(); | ||
function testInvalidJwt(done: DoneCallback, invalidJwt: string) { | ||
window.location.href = `https://arbys.com/?clientJwt=${invalidJwt}&needsKeys=false`; | ||
setHref(ARBYS_ORIGIN, "/", `?clientJwt=${invalidJwt}&needsKeys=false`); | ||
const transposit: Transposit = makeArbysTransposit(); | ||
const transposit: Transposit = new Transposit(); | ||
try { | ||
@@ -227,35 +234,9 @@ transposit.handleLogin(); | ||
}); | ||
}); | ||
describe("isLoggedIn", () => { | ||
it("knows when you're logged in", () => { | ||
// 3 days before expiration | ||
MockDate.set((jplaceArbysClaims.exp - 60 * 60 * 24 * 3) * 1000); | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=false`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
transposit.handleLogin(); | ||
expect(transposit.isLoggedIn()).toBe(true); | ||
it("throws with invalid jwt (expired)", (done: DoneCallback) => { | ||
const expiredClaims = Object.assign({}, jplaceArbysClaims, { | ||
exp: NOW_MINUS_3_DAYS / 1000, | ||
}); | ||
testInvalidJwt(done, createUnsignedJwt(expiredClaims)); | ||
}); | ||
it("knows when you're logged out", () => { | ||
window.location.href = `https://arbys.com/`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
expect(transposit.isLoggedIn()).toBe(false); | ||
}); | ||
it("knows when your session expired", () => { | ||
// 3 days after expiration | ||
MockDate.set((jplaceArbysClaims.exp + 60 * 60 * 24 * 3) * 1000); | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=false`; | ||
const transposit: Transposit = makeArbysTransposit(); | ||
transposit.handleLogin(); | ||
expect(transposit.isLoggedIn()).toBe(false); | ||
}); | ||
}); | ||
@@ -267,26 +248,34 @@ | ||
beforeEach(() => { | ||
const clientJwt: string = createUnsignedJwt(jplaceArbysClaims); | ||
window.location.href = `https://arbys.com/?clientJwt=${clientJwt}&needsKeys=false`; | ||
transposit = makeArbysTransposit(); | ||
setHref( | ||
ARBYS_ORIGIN, | ||
"/", | ||
`?clientJwt=${jplaceArbysJwt}&needsKeys=false`, | ||
); | ||
transposit = new Transposit(); | ||
transposit.handleLogin(); | ||
}); | ||
it("handles successful logout", async () => { | ||
expect.assertions(1); | ||
it("handles successful logout", () => { | ||
transposit.logOut(arbysUri("/login")); | ||
(window.fetch as jest.Mock<{}>).mockImplementation(() => | ||
Promise.resolve(), | ||
expect(localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull(); | ||
expect(transposit.isLoggedIn()).toBe(false); | ||
expect(window.location.href).toEqual( | ||
"/logout?redirectUri=https%3A%2F%2Farbys-beef-xyz12.transposit.io%2Flogin", | ||
); | ||
}); | ||
}); | ||
await transposit.logOut(); | ||
describe("startLoginUri", () => { | ||
it("returns the correct location", () => { | ||
const transposit: Transposit = new Transposit(ARBYS_ORIGIN); | ||
expect( | ||
localStorage.getItem( | ||
`${TRANSPOSIT_CONSUME_KEY_PREFIX}/jplace/arbys_beef`, | ||
), | ||
).toBeNull(); | ||
expect(transposit.startLoginUri("https://altoids.com")).toEqual( | ||
"https://arbys-beef-xyz12.transposit.io/login/accounts?redirectUri=https%3A%2F%2Faltoids.com", | ||
); | ||
expect(transposit.getGoogleLoginLocation("https://altoids.com")).toEqual( | ||
"https://arbys-beef-xyz12.transposit.io/login/accounts?redirectUri=https%3A%2F%2Faltoids.com", | ||
); | ||
}); | ||
}); | ||
}); |
@@ -34,43 +34,50 @@ /* | ||
// From https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | ||
function getParameterByName(name: string): string | null { | ||
const url = window.location.href; | ||
name = name.replace(/[\[\]]/g, "\\$&"); | ||
const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"); | ||
const results = regex.exec(url); | ||
if (!results) { | ||
return null; | ||
} | ||
if (!results[2]) { | ||
return ""; | ||
} | ||
return decodeURIComponent(results[2].replace(/\+/g, " ")); | ||
// https://my-app.transposit.io?clientJwt=...needKeys=... -> https://my-app.transposit.io | ||
function hereWithoutSearch(): string { | ||
return `${window.location.origin}${window.location.pathname}`; | ||
} | ||
export const TRANSPOSIT_CONSUME_KEY_PREFIX = "TRANSPOSIT_CONSUME_KEY"; | ||
export const LOCAL_STORAGE_KEY = "TRANSPOSIT_SESSION"; | ||
export class Transposit { | ||
constructor( | ||
private serviceMaintainer: string, | ||
private serviceName: string, | ||
private tpsHostedAppApiUrl: string = "https://api.transposit.com", | ||
) {} | ||
private claims: ClientClaims | null = null; | ||
private getConsumeKey(): string { | ||
return `${TRANSPOSIT_CONSUME_KEY_PREFIX}/${this.serviceMaintainer}/${ | ||
this.serviceName | ||
}`; | ||
constructor(private hostedAppOrigin: string = "") { | ||
this.claims = this.loadClaims(); | ||
} | ||
private retrieveClientClaims(): ClientClaims | null { | ||
const clientClaimJSON = localStorage.getItem(this.getConsumeKey()); | ||
if (!clientClaimJSON) { | ||
private uri(path: string = ""): string { | ||
return `${this.hostedAppOrigin}${path}`; | ||
} | ||
private loadClaims(): ClientClaims | null { | ||
const claimsJSON = localStorage.getItem(LOCAL_STORAGE_KEY); | ||
if (!claimsJSON) { | ||
return null; | ||
} | ||
return JSON.parse(clientClaimJSON); | ||
let claims: ClientClaims; | ||
try { | ||
claims = JSON.parse(claimsJSON); | ||
} catch (err) { | ||
return null; | ||
} | ||
if (!this.checkClaimsValid(claims)) { | ||
return null; | ||
} | ||
return claims; | ||
} | ||
private areClientClaimsValid(clientClaims: ClientClaims): boolean { | ||
const expiration = clientClaims.exp * 1000; | ||
private persistClaims(claimsJSON: string): void { | ||
localStorage.setItem(LOCAL_STORAGE_KEY, claimsJSON); | ||
} | ||
private clearClaims(): void { | ||
localStorage.removeItem(LOCAL_STORAGE_KEY); | ||
} | ||
private checkClaimsValid(claims: ClientClaims): boolean { | ||
const expiration = claims.exp * 1000; | ||
const now = Date.now(); | ||
@@ -80,21 +87,18 @@ return expiration > now; | ||
private persistClientClaims(clientClaimsJSON: string): void { | ||
localStorage.setItem(this.getConsumeKey(), clientClaimsJSON); | ||
private assertLoggedIn(): void { | ||
if (this.claims === null) { | ||
throw new Error("No client claims found."); | ||
} | ||
} | ||
private clearClientClaims(): void { | ||
localStorage.removeItem(this.getConsumeKey()); | ||
isLoggedIn(): boolean { | ||
return this.claims !== null; | ||
} | ||
private apiUrl(relativePath: string = ""): string { | ||
return `${this.tpsHostedAppApiUrl}/app/${this.serviceMaintainer}/${ | ||
this.serviceName | ||
}${relativePath}`; | ||
} | ||
handleLogin(callback?: (info: { needsKeys: boolean }) => void): void { | ||
// Read query parameters | ||
const maybeClientJwtString = getParameterByName("clientJwt"); | ||
if (maybeClientJwtString === null) { | ||
const searchParams = new URLSearchParams(window.location.search); | ||
if (!searchParams.has("clientJwt")) { | ||
throw new Error( | ||
@@ -104,6 +108,5 @@ "clientJwt query parameter could not be found. This method should only be called after redirection during login.", | ||
} | ||
const clientJwtString = maybeClientJwtString; | ||
const clientJwtString = searchParams.get("clientJwt")!; | ||
const maybeNeedsKeys = getParameterByName("needsKeys"); | ||
if (maybeNeedsKeys === null) { | ||
if (!searchParams.has("needsKeys")) { | ||
throw new Error( | ||
@@ -113,5 +116,5 @@ "needsKeys query parameter could not be found. This is unexpected.", | ||
} | ||
const needsKeys = maybeNeedsKeys === "true"; | ||
const needsKeys = searchParams.get("needsKeys")! === "true"; | ||
// Parse JWT string and persist claims | ||
// Parse JWT string | ||
@@ -124,5 +127,5 @@ const jwtParts: string[] = clientJwtString.split("."); | ||
} | ||
let clientClaimsJSON: string; | ||
let claimsJSON: string; | ||
try { | ||
clientClaimsJSON = atob(jwtParts[1]); | ||
claimsJSON = atob(jwtParts[1]); | ||
} catch (err) { | ||
@@ -133,4 +136,5 @@ throw new Error( | ||
} | ||
let claims: ClientClaims; | ||
try { | ||
JSON.parse(clientClaimsJSON); // validate JSON | ||
claims = JSON.parse(claimsJSON); | ||
} catch (err) { | ||
@@ -141,7 +145,15 @@ throw new Error( | ||
} | ||
if (!this.checkClaimsValid(claims)) { | ||
throw new Error( | ||
"clientJwt query parameter does not appear to be a valid JWT string. clientJwt is expired.", | ||
); | ||
} | ||
this.persistClientClaims(clientClaimsJSON); | ||
// Persist claims. Login has succeeded. | ||
// Login has succeeded, either callback or default path replacement | ||
this.claims = claims; | ||
this.persistClaims(claimsJSON); | ||
// Perform callback or default path replacement | ||
if (callback) { | ||
@@ -154,8 +166,3 @@ if (typeof callback !== "function") { | ||
if (needsKeys) { | ||
const hereWithoutQueryParameters: string = `${ | ||
window.location.protocol | ||
}//${window.location.host}${window.location.pathname}`; | ||
window.location.href = this.getConnectLocation( | ||
hereWithoutQueryParameters, | ||
); | ||
window.location.href = this.settingsUri(hereWithoutSearch()); | ||
} else { | ||
@@ -171,34 +178,32 @@ window.history.replaceState( | ||
async logOut(): Promise<void> { | ||
const clientClaims = this.retrieveClientClaims(); | ||
if (!clientClaims) { | ||
// Already logged out, nothing to do | ||
return; | ||
} | ||
logOut(redirectUri: string): void { | ||
this.clearClaims(); | ||
this.claims = null; | ||
// Attempt to invalidate session with Transposit | ||
try { | ||
await fetch(this.apiUrl(`/api/v1/logout`), { | ||
credentials: "include", | ||
method: "POST", | ||
headers: { | ||
"content-type": "application/json", | ||
"X-PUBLIC-TOKEN": clientClaims.publicToken, | ||
}, | ||
}); | ||
} catch (err) { | ||
// Logout is a best effort, do nothing if there is an error remotely | ||
} | ||
window.location.href = this.uri( | ||
`/logout?redirectUri=${encodeURIComponent(redirectUri)}`, | ||
); | ||
} | ||
// Delete JWT from local storage | ||
this.clearClientClaims(); | ||
settingsUri(redirectUri?: string): string { | ||
return this.uri( | ||
`/settings?redirectUri=${encodeURIComponent( | ||
redirectUri || window.location.href, | ||
)}`, | ||
); | ||
} | ||
getConnectLocation(requestUri?: string): string { | ||
return this.apiUrl( | ||
"/connect?redirectUri=" + | ||
encodeURIComponent(requestUri || window.location.href), | ||
startLoginUri(redirectUri?: string): string { | ||
return this.uri( | ||
`/login/accounts?redirectUri=${encodeURIComponent( | ||
redirectUri || window.location.href, | ||
)}`, | ||
); | ||
} | ||
// Deprecated in favor of settingsUri | ||
getConnectLocation(redirectUri?: string): string { | ||
return this.settingsUri(redirectUri); | ||
} | ||
// Deprecated in favor of startLoginUri | ||
@@ -209,36 +214,17 @@ getGoogleLoginLocation(redirectUri?: string): string { | ||
startLoginUri(redirectUri?: string): string { | ||
return this.apiUrl( | ||
"/login/accounts?redirectUri=" + | ||
encodeURIComponent(redirectUri || window.location.href), | ||
); | ||
} | ||
// Deprecated | ||
getLoginLocation(): string { | ||
return this.apiUrl("/login"); | ||
return this.uri("/login"); | ||
} | ||
getUserEmail(): string | null { | ||
const clientClaims = this.retrieveClientClaims(); | ||
if (!clientClaims) { | ||
return null; | ||
} | ||
return clientClaims.email; | ||
this.assertLoggedIn(); | ||
return this.claims!.email; | ||
} | ||
getUserName(): string | null { | ||
const clientClaims = this.retrieveClientClaims(); | ||
if (!clientClaims) { | ||
return null; | ||
} | ||
return clientClaims.name; | ||
this.assertLoggedIn(); | ||
return this.claims!.name; | ||
} | ||
isLoggedIn(): boolean { | ||
const clientClaims = this.retrieveClientClaims(); | ||
return clientClaims !== null && this.areClientClaimsValid(clientClaims); | ||
} | ||
async runOperation( | ||
@@ -248,35 +234,24 @@ operationId: string, | ||
): Promise<EndRequestLog> { | ||
const headerInfo = { | ||
"content-type": "application/json", | ||
} as any; | ||
const clientClaims = this.retrieveClientClaims(); | ||
if (clientClaims) { | ||
headerInfo["X-PUBLIC-TOKEN"] = clientClaims.publicToken; | ||
const headers: HeadersInit = { | ||
"Content-Type": "application/json", | ||
}; | ||
if (this.claims) { | ||
headers["X-PUBLIC-TOKEN"] = this.claims.publicToken; | ||
} | ||
// Note from MDN: The Promise returned from fetch() won’t reject on HTTP error status even | ||
// if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status | ||
// set to false), and it will only reject on network failure or if anything prevented the request from completing. | ||
try { | ||
const response = await fetch( | ||
this.apiUrl(`/api/v1/execute/${operationId}`), | ||
{ | ||
credentials: "include", | ||
method: "POST", | ||
headers: headerInfo, | ||
body: JSON.stringify({ | ||
parameters: params, | ||
}), | ||
}, | ||
); | ||
if (response.status >= 200 && response.status < 300) { | ||
return (await response.json()) as EndRequestLog; | ||
} else { | ||
throw response; | ||
} | ||
} catch (e) { | ||
throw e; | ||
const response = await fetch(this.uri(`/api/v1/execute/${operationId}`), { | ||
credentials: "include", | ||
method: "POST", | ||
headers, | ||
body: JSON.stringify({ | ||
parameters: params, | ||
}), | ||
}); | ||
if (response.status >= 200 && response.status < 300) { | ||
return (await response.json()) as EndRequestLog; | ||
} else { | ||
throw response; | ||
} | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
2
97099
1163
155