908 lines
27 KiB
JavaScript
908 lines
27 KiB
JavaScript
(function (exports) {
|
|
'use strict';
|
|
|
|
var TherapySession;
|
|
var Oauth3 = (exports.OAUTH3 || require('./oauth3'));
|
|
|
|
//
|
|
// Pure convenience / utility funcs
|
|
//
|
|
function createSession() {
|
|
return { logins: [], accounts: [] };
|
|
}
|
|
function removeItem(array, item) {
|
|
var i = array.indexOf(item);
|
|
|
|
if (-1 !== i) {
|
|
array.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
var TLogins = {};
|
|
var TAccounts = {};
|
|
var InternalApi;
|
|
var api;
|
|
|
|
function create(opts) {
|
|
var myInstance = {};
|
|
var conf = {
|
|
session: createSession()
|
|
, sessionKey: opts.namespace + '.' + opts.sessionKey // 'session'
|
|
, cache: opts.cache
|
|
, config: opts.config
|
|
, usernameMinLength: opts.usernameMinLength
|
|
, secretMinLength: opts.secretMinLength
|
|
};
|
|
|
|
Object.keys(TherapySession.api).forEach(function (key) {
|
|
myInstance[key] = function () {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.unshift(conf);
|
|
return TherapySession.api[key].apply(null, args);
|
|
};
|
|
});
|
|
|
|
myInstance.getId = TherapySession.getId;
|
|
myInstance.openAuthorizationDialog = function () {
|
|
// TODO guarantee that this happens assignment happens before initialization?
|
|
return (opts.invokeLogin || opts.config.invokeLogin).apply(null, arguments);
|
|
};
|
|
myInstance.usernameMinLength = opts.usernameMinLength;
|
|
myInstance.secretMinLength = opts.secretMinLength;
|
|
myInstance.api = api;
|
|
|
|
myInstance._conf = conf;
|
|
|
|
return myInstance;
|
|
}
|
|
|
|
// TODO track and compare granted scopes locally
|
|
function save(conf, updates) {
|
|
// TODO make sure session.logins[0] is most recent
|
|
api.updateSession(conf, updates.login, updates.accounts);
|
|
|
|
// TODO should this be done by the LocalApiStorage?
|
|
// TODO how to have different accounts selected in different tabs?
|
|
localStorage.setItem(conf.sessionKey, JSON.stringify(conf.session));
|
|
return Oauth3.PromiseA.resolve(conf.session);
|
|
}
|
|
|
|
function restore(conf) {
|
|
// Being very careful not to trigger a false onLogin or onLogout via $watch
|
|
var storedSession;
|
|
|
|
if (conf.session.token) {
|
|
return api.sanityCheckAccounts(conf);
|
|
// return Oauth3.PromiseA.resolve(conf.session);
|
|
}
|
|
|
|
storedSession = JSON.parse(localStorage.getItem(conf.sessionKey) || null) || createSession();
|
|
|
|
if (storedSession.token) {
|
|
conf.session = storedSession;
|
|
return api.sanityCheckAccounts(conf);
|
|
//return Oauth3.PromiseA.resolve(conf.session);
|
|
} else {
|
|
return Oauth3.PromiseA.reject(new Error("No Session"));
|
|
}
|
|
}
|
|
|
|
function destroy(conf) {
|
|
conf.session = createSession();
|
|
localStorage.removeItem(conf.sessionKey);
|
|
return conf.cache.destroy(conf).then(function (session) {
|
|
return session;
|
|
});
|
|
}
|
|
|
|
function accounts(conf, login) {
|
|
return Oauth3.request({
|
|
url: conf.config.apiBaseUri + conf.config.apiPrefix + '/accounts'
|
|
, method: 'GET'
|
|
, headers: { 'Authorization': 'Bearer ' + login.token }
|
|
}).then(function (resp) {
|
|
var accounts = resp.data && (resp.data.accounts || resp.data.result || resp.data.results)
|
|
|| resp.data || { error: { message: "Unknown Error when retrieving accounts" } }
|
|
;
|
|
|
|
if (accounts.error) {
|
|
console.error("[ERROR] couldn't fetch accounts", accounts);
|
|
return Oauth3.PromiseA.reject(new Error("Could not verify login:" + accounts.error.message));
|
|
}
|
|
|
|
if (!Array.isArray(accounts)) {
|
|
console.error("[Uknown ERROR] couldn't fetch accounts, no proper error", accounts);
|
|
// TODO destroy(conf);
|
|
return Oauth3.PromiseA.reject(new Error("could not verify login")); // destroy(conf);
|
|
}
|
|
|
|
return accounts;
|
|
});
|
|
}
|
|
|
|
// TODO move to LocalApiLogin?
|
|
function testLoginAccounts(conf, login) {
|
|
// TODO cache this also, but with a shorter shelf life?
|
|
return TherapySession.api.accounts(conf, login).then(function (accounts) {
|
|
return { login: login, accounts: accounts };
|
|
}, function (err) {
|
|
console.error("[Error] couldn't get accounts (might not be linked)");
|
|
console.warn(err);
|
|
return { login: login, accounts: [] };
|
|
});
|
|
}
|
|
|
|
function logout(conf) {
|
|
console.log('DEBUG logout', conf);
|
|
return Oauth3.logout(conf.config.providerUri, {}).then(function () {
|
|
console.log('DEBUG Oauth3.logout');
|
|
return destroy(conf);
|
|
}, function () {
|
|
return destroy(conf);
|
|
});
|
|
}
|
|
|
|
function backgroundLogin(conf, opts) {
|
|
opts = opts || {};
|
|
|
|
opts.background = true;
|
|
return TherapySession.api.login(conf, opts);
|
|
}
|
|
|
|
function login(conf, opts) {
|
|
console.log('##### DEBUG TherapySession');
|
|
console.log(conf);
|
|
console.log(opts);
|
|
// this should work first party and third party
|
|
var promise;
|
|
var providerUri = (opts && opts.providerUri) || conf.config.providerUri;
|
|
|
|
opts = opts || {};
|
|
//opts.redirectUri = conf.config.appUri + '/oauth3.html';
|
|
|
|
// TODO note that this must be called on a click event
|
|
// otherwise the browser will block the popup
|
|
function forceLogin() {
|
|
opts.appId = opts.appId || conf.config.appId;
|
|
opts.clientUri = opts.clientUri || conf.config.clientUri;
|
|
opts.clientAgreeTos = opts.clientAgreeTos || conf.config.clientAgreeTos;
|
|
var username = opts.username;
|
|
// TODO why is login modifying the opts?
|
|
return Oauth3.login(providerUri, opts).then(function (params) {
|
|
return TLogins.getLoginFromTokenParams(conf, providerUri, username, params).then(function (login) {
|
|
return testLoginAccounts(conf, login).then(function (updates) {
|
|
return save(conf, updates);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!opts.force) {
|
|
promise = restore(conf, opts.scope);
|
|
} else {
|
|
promise = Oauth3.PromiseA.reject();
|
|
}
|
|
|
|
// TODO check for scope in session
|
|
return promise.then(function (session) {
|
|
if (!session.appScopedId || opts && opts.force) {
|
|
return forceLogin();
|
|
}
|
|
|
|
var promise = Oauth3.PromiseA.resolve();
|
|
|
|
// TODO check expirey
|
|
session.logins.forEach(function (login) {
|
|
promise = promise.then(function () {
|
|
return testLoginAccounts(conf, login).then(function (updates) {
|
|
return save(conf, updates);
|
|
});
|
|
});
|
|
});
|
|
|
|
return promise;
|
|
}, forceLogin).then(function (session) {
|
|
// testLoginAccounts().then(save);
|
|
return session;
|
|
});
|
|
}
|
|
|
|
function requireSession(conf, opts) {
|
|
var promise = Oauth3.PromiseA.resolve(opts);
|
|
|
|
// TODO create middleware stack
|
|
return promise.then(function () {
|
|
return TLogins.requireLogin(conf, opts);
|
|
}).then(function () {
|
|
return TAccounts.requireAccount(conf, opts);
|
|
});
|
|
// .then(selectAccount).then(verifyAccount)
|
|
}
|
|
|
|
function onLogin(conf, _scope, fn) {
|
|
// This is better than using a promise.notify
|
|
// because the watches will unwatch when the controller is destroyed
|
|
_scope.__stsessionshared__ = conf;
|
|
_scope.$watch('__stsessionshared__.session', function (newValue, oldValue) {
|
|
if (newValue.accountId && oldValue.accountId !== newValue.accountId) {
|
|
fn(conf.session);
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
function onLogout(conf, _scope, fn) {
|
|
_scope.__stsessionshared__ = conf;
|
|
_scope.$watch('__stsessionshared__.session', function (newValue, oldValue) {
|
|
if (!newValue.accountId && oldValue.accountId) {
|
|
fn(null);
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
|
|
function getToken(conf, accountId) {
|
|
var session = conf.session;
|
|
var logins = [];
|
|
var login;
|
|
accountId = TAccounts.getId(accountId) || accountId;
|
|
|
|
// search logins first because we know we're actually
|
|
// logged in with said login, y'know?
|
|
session.logins.forEach(function (login) {
|
|
login.accounts.forEach(function (account) {
|
|
if (TAccounts.getId(account) === accountId) {
|
|
logins.push(login);
|
|
}
|
|
});
|
|
});
|
|
|
|
login = logins.sort(function (a, b) {
|
|
// b - a // most recent first
|
|
return (new Date(b.expiresAt).value || 0) - (new Date(a.expiresAt).value || 0);
|
|
})[0];
|
|
|
|
return login && login.token;
|
|
}
|
|
|
|
// this should be done at every login
|
|
// even an existing login may gain new accounts
|
|
function addAccountsToSession(conf, login, accounts) {
|
|
var now = Date.now();
|
|
|
|
login.accounts = accounts.map(function (account) {
|
|
account.addedAt = account.addedAt || now;
|
|
return {
|
|
id: TAccounts.getId(account)
|
|
, addedAt: now
|
|
};
|
|
});
|
|
|
|
accounts.forEach(function (newAccount) {
|
|
if (!conf.session.accounts.some(function (other, i) {
|
|
if (TAccounts.getId(other) === TAccounts.getId(newAccount)) {
|
|
conf.session.accounts[i] = newAccount;
|
|
return true;
|
|
}
|
|
})) {
|
|
conf.session.accounts.push(newAccount);
|
|
}
|
|
});
|
|
|
|
conf.session.accounts.sort(function (a, b) {
|
|
return b.addedAt - a.addedAt;
|
|
});
|
|
}
|
|
|
|
// this should be done on login and logout
|
|
// an old login may have lost or gained accounts
|
|
function pruneAccountsFromSession(conf) {
|
|
var session = conf.session;
|
|
var accounts = session.accounts.slice(0);
|
|
|
|
// remember, you can't modify an array while it's in-loop
|
|
// well, you can... but it would be bad!
|
|
accounts.forEach(function (account) {
|
|
if (!session.logins.some(function (login) {
|
|
return login.accounts.some(function (a) {
|
|
return TAccounts.getId(a) === TAccounts.getId(account);
|
|
});
|
|
})) {
|
|
removeItem(session.accounts, account);
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshCurrentAccount(conf) {
|
|
var session = conf.session;
|
|
|
|
// select a default session
|
|
if (1 === session.accounts.length) {
|
|
session.accountId = TAccounts.getId(session.accounts[0]);
|
|
session.id = session.accountId;
|
|
session.appScopedId = session.accountId;
|
|
session.token = session.accountId && api.getToken(conf, session.accountId) || null;
|
|
session.userVerifiedAt = session.accounts[0].userVerifiedAt;
|
|
return;
|
|
}
|
|
|
|
if (!session.logins.some(function (account) {
|
|
if (session.accountId === TAccounts.getId(account)) {
|
|
session.accountId = TAccounts.getId(account);
|
|
session.id = session.accountId;
|
|
session.appScopedId = session.accountId;
|
|
session.token = session.accountId && api.getToken(conf, session.accountId) || null;
|
|
session.userVerifiedAt = account.userVerifiedAt;
|
|
}
|
|
})) {
|
|
session.accountId = null;
|
|
session.id = null;
|
|
session.appScopedId = null;
|
|
session.token = null;
|
|
session.userVerifiedAt = null;
|
|
}
|
|
}
|
|
|
|
function updateSession(conf, login, accounts) {
|
|
var session = conf.session;
|
|
|
|
login.addedAt = login.addedAt || Date.now();
|
|
|
|
// sanity check login
|
|
if (0 === accounts.length) {
|
|
login.selectedAccountId = null;
|
|
}
|
|
else if (1 === accounts.length) {
|
|
login.selectedAccountId = TAccounts.getId(accounts[0]);
|
|
}
|
|
else if (accounts.length >= 1) {
|
|
login.selectedAccountId = null;
|
|
}
|
|
else {
|
|
throw new Error("[SANITY CHECK FAILED] bad account length'");
|
|
}
|
|
|
|
api.addAccountsToSession(conf, login, accounts);
|
|
|
|
// update login if it exists
|
|
// (or add it if it doesn't)
|
|
if (!session.logins.some(function (other, i) {
|
|
if ((login.loginId && other.loginId === login.loginId) || (other.token === login.token)) {
|
|
session.logins[i] = login;
|
|
return true;
|
|
}
|
|
})) {
|
|
session.logins.push(login);
|
|
}
|
|
|
|
api.pruneAccountsFromSession(conf);
|
|
|
|
api.refreshCurrentAccount(conf);
|
|
|
|
session.logins.sort(function (a, b) {
|
|
return b.addedAt - a.addedAt;
|
|
});
|
|
}
|
|
|
|
function sanityCheckAccounts(conf) {
|
|
var promise;
|
|
var session = conf.session;
|
|
|
|
// XXX this is just a bugfix for previously deployed code
|
|
// it probably only affects about 10 users and can be deleted
|
|
// at some point in the future (or left as a sanity check)
|
|
|
|
if (session.accounts.every(function (account) {
|
|
if (account.appScopedId) {
|
|
return true;
|
|
}
|
|
})) {
|
|
return Oauth3.PromiseA.resolve(session);
|
|
}
|
|
|
|
promise = Oauth3.PromiseA.resolve();
|
|
session.logins.forEach(function (login) {
|
|
promise = promise.then(function () {
|
|
return testLoginAccounts(conf, login).then(function (updates) {
|
|
return save(conf, updates);
|
|
});
|
|
});
|
|
});
|
|
|
|
return promise.then(function (session) {
|
|
return session;
|
|
}, function () {
|
|
// this is just bad news...
|
|
return conf.cache.destroy(conf).then(function () {
|
|
window.alert("Sorry, but an error occurred which can only be fixed by logging you out"
|
|
+ " and refreshing the page.\n\nThis will happen automatically.\n\nIf you get this"
|
|
+ " message even after the page refreshes, please contact support@betopool.com."
|
|
);
|
|
window.location.reload();
|
|
return Oauth3.PromiseA.reject(new Error("A session error occured. You must log out and log back in."));
|
|
});
|
|
});
|
|
}
|
|
|
|
// TODO is this more logins or accounts or session? session?
|
|
function handleOrphanLogins(conf) {
|
|
var promise;
|
|
var session = conf.session;
|
|
|
|
promise = Oauth3.PromiseA.resolve();
|
|
|
|
if (session.logins.some(function (login) {
|
|
return !login.accounts.length;
|
|
})) {
|
|
if (session.accounts.length > 1) {
|
|
throw new Error("[Not Implemented] can't yet attach new social logins when more than one local account is in the session."
|
|
+ " Please logout and sign back in with your Local Account only. Then attach the other login.");
|
|
}
|
|
session.logins.forEach(function (login) {
|
|
if (!login.accounts.length) {
|
|
promise = promise.then(function () {
|
|
return TAccounts.attachLoginToAccount(conf, session.accounts[0], login);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return promise.then(function () {
|
|
return session;
|
|
});
|
|
}
|
|
|
|
TLogins.getLoginFromTokenParams = function (conf, providerUri, username, params) {
|
|
var err;
|
|
var accessToken;
|
|
var refreshToken;
|
|
var expiresAt;
|
|
var match;
|
|
var data;
|
|
var login;
|
|
var now = Date.now();
|
|
|
|
if (!params) {
|
|
err = new Error("[Developer Error] No params were passed to the token parser");
|
|
err.code = 'E_DEV_ERROR';
|
|
err.uri = 'https://oauth3.org/docs/errors/#E_DEV_ERROR';
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
console.log('[DEBUG] params', params);
|
|
|
|
accessToken = (params.oauth3_token || params.oauth3Token || params.jwt || params.access_token || params.accessToken || params.token);
|
|
refreshToken = (params.oauth3Refresh || params.oauth3_refresh || params.jwt_refresh || params.refresh_token || params.refreshToken);
|
|
|
|
if (!accessToken) {
|
|
if (!(params.error || params.error_description)) {
|
|
err = new Error("[Server Error] The server did not grant access nor give an error message");
|
|
err.code = "E_SERVER_ERROR";
|
|
err.uri = params.error_uri || '';
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
err = new Error(params.error_description || ": invalid username or secret");
|
|
err.code = params.error || "_access_denied";
|
|
err.uri = params.error_uri || '';
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
// JWT <<base64>>.<<base64>>.<<base64>>
|
|
// pass yada.yada.yada
|
|
// fail yada yada.yada
|
|
// fail y?da.yada.yada
|
|
match = accessToken.match(/^[A-Za-z0-9+=_\/\-]+\.([A-Za-z0-9+=_\/\-]+)\.[A-Za-z0-9+=_\/\-]+$/);
|
|
if (match) {
|
|
try {
|
|
data = JSON.parse(atob(match[1]));
|
|
} catch(e) {
|
|
data = {};
|
|
}
|
|
} else {
|
|
data = {};
|
|
}
|
|
|
|
// TODO support fewer expiry methods
|
|
expiresAt = [
|
|
params.expires_at, params.expiresAt, params.expires_in, params.expiresIn, params.expires, data.exp
|
|
].map(function (exp) {
|
|
exp = parseInt(exp, 10) || 0;
|
|
var year = 365 * 24 * 60 * 60 * 1000;
|
|
var min = now - (1 * year);
|
|
var max = now + (2 * year);
|
|
|
|
// date of expiration, already in ms
|
|
if (exp > min && exp < max) {
|
|
return exp;
|
|
}
|
|
// date of expiration in seconds
|
|
if (exp > (min / 1000) && exp < (max / 1000)) {
|
|
return exp * 1000;
|
|
}
|
|
// time remaining in seconds
|
|
if (exp > 1 && exp < (2 * year)) {
|
|
return now + (exp * 1000);
|
|
}
|
|
}).filter(function (exp) {
|
|
return exp;
|
|
})[0] || (Date.now() + 1 * 60 * 60 * 1000);
|
|
|
|
// TODO drop prefixes everywhere
|
|
providerUri = providerUri.replace(/^(https?:\/\/)?(www\.)?/, '');
|
|
login = {
|
|
token: accessToken
|
|
, refreshToken: refreshToken
|
|
, expiresAt: expiresAt
|
|
, appScopedId: params.app_scoped_id || params.appScopedId
|
|
|| data.idx || data.usr || username
|
|
|| null
|
|
, loginId: params.loginId || params.login_id
|
|
|| data.id || data.usr
|
|
, accountId: params.accountId || params.account_id
|
|
|| data.acx || data.acc
|
|
// TODO app_name in oauth3.json "AJ on Facebook"
|
|
, comment: data.sub || data.com ||
|
|
(
|
|
(username && (username + ' via ') || '')
|
|
+ (providerUri)
|
|
)
|
|
, loginType: ('password' === data.grt || username) ? 'localaccount' : null
|
|
, providerUri: providerUri
|
|
};
|
|
|
|
return Oauth3.PromiseA.resolve(login);
|
|
};
|
|
|
|
TLogins.requireLogin = function (conf, opts) {
|
|
return restore(conf).then(function (session) {
|
|
return session;
|
|
}, function (/*err*/) {
|
|
|
|
return conf.config.invokeLogin(opts);
|
|
});
|
|
};
|
|
|
|
TLogins.create = function (conf, username, type, secret, kdf, mfa) {
|
|
// secret is optional (for server-side requirement checking)
|
|
// kdf is mandatory (
|
|
return Oauth3.request({
|
|
url: conf.config.apiBaseUri + '/api'
|
|
+ '/org.oauth3.provider'
|
|
+ '/logins/'
|
|
, method: 'POST'
|
|
, data: {
|
|
id: username
|
|
, type: type
|
|
, secret: secret
|
|
, kdf: kdf
|
|
, mfa: mfa
|
|
}
|
|
});
|
|
};
|
|
|
|
TLogins.softTestUsername = function (conf, username) {
|
|
if ('string' !== typeof username) {
|
|
throw new Error("[Developer Error] username should be a string");
|
|
}
|
|
|
|
/*
|
|
if (!/^[0-9a-z\.\-_]+$/i.test(username)) {
|
|
// TODO validate this is true on the server
|
|
return new Error("Only alphanumeric characters, '-', '_', and '.' are allowed in usernames.");
|
|
}
|
|
*/
|
|
|
|
if (!/^[^@]+@[^\.]+\.[^\.]+$/i.test(username)) {
|
|
// TODO validate this is true on the server
|
|
return new Error("You must use an email address.");
|
|
}
|
|
|
|
if (username.length < conf.usernameMinLength) {
|
|
// TODO validate this is true on the server
|
|
return new Error('Username too short. Use at least '
|
|
+ conf.usernameMinLength + ' characters.');
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
TLogins.getMeta = function (conf, username) {
|
|
// TODO support username as type
|
|
var type = null;
|
|
|
|
// TODO update backend to /api/promoonlyonline/username/:username?
|
|
return Oauth3.request({
|
|
url: conf.config.apiBaseUri + '/api'
|
|
+ '/org.oauth3.provider'
|
|
+ '/logins/meta/' + type + '/' + username
|
|
, method: 'GET'
|
|
}).then(function (resp) {
|
|
if (!resp.data.kdf) {
|
|
return Oauth3.PromiseA.reject(new Error("metadata for username does not exist"));
|
|
}
|
|
|
|
return resp.data;
|
|
}, function (err) {
|
|
if (/does not exist/.test(err.message)) {
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
throw err;
|
|
});
|
|
};
|
|
|
|
TLogins.meta = function (conf, username, type) {
|
|
// TODO support username as type
|
|
|
|
// TODO update backend to /api/promoonlyonline/username/:username?
|
|
return Oauth3.request({
|
|
url: conf.config.apiBaseUri + '/api'
|
|
+ '/org.oauth3.provider'
|
|
+ '/logins/meta/' + type + '/' + username
|
|
, method: 'GET'
|
|
}).then(function (resp) {
|
|
// TODO better check
|
|
if (!resp.data.salt) {
|
|
return Oauth3.PromiseA.reject(new Error("data for username does not exist"));
|
|
}
|
|
|
|
return resp.data;
|
|
}, function (err) {
|
|
if (/does not exist/.test(err.message)) {
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
throw err;
|
|
});
|
|
};
|
|
|
|
TLogins.hardTestUsername = function (conf, username) {
|
|
// TODO support username as type
|
|
var type = null;
|
|
|
|
// TODO update backend to /api/promoonlyonline/username/:username?
|
|
return Oauth3.request({
|
|
url: conf.config.apiBaseUri + '/api'
|
|
+ '/org.oauth3.provider'
|
|
+ '/logins/check/' + type + '/' + username
|
|
, method: 'GET'
|
|
}).then(function (result) {
|
|
if (!result.data.exists) {
|
|
return Oauth3.PromiseA.reject(new Error("username does not exist"));
|
|
}
|
|
}, function (err) {
|
|
if (/does not exist/.test(err.message)) {
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
throw err;
|
|
});
|
|
};
|
|
|
|
TAccounts.getId = function (o, p) {
|
|
// object
|
|
if (!o) {
|
|
return null;
|
|
}
|
|
// prefix
|
|
if (!p) {
|
|
return o.appScopedId || o.app_scoped_id || o.id || null;
|
|
} else {
|
|
return o[p + 'AppScopedId'] || o[p + '_app_scoped_id'] || o[p + 'Id'] || o[p + '_id'] || null;
|
|
}
|
|
};
|
|
|
|
TAccounts.realCreateAccount = function (conf, login) {
|
|
return Oauth3.request({
|
|
url: conf.config.apiBaseUri + '/api'
|
|
+ '/org.oauth3.provider'
|
|
+ '/accounts'
|
|
, method: 'POST'
|
|
, data: { account: {}
|
|
, logins: [{
|
|
// TODO make appScopedIds even for root app
|
|
id: login.appScopedId || login.app_scoped_id || login.loginId || login.login_id || login.id
|
|
, token: login.token || login.accessToken || login.accessToken
|
|
}]
|
|
}
|
|
, headers: {
|
|
Authorization: 'Bearer ' + login.token
|
|
}
|
|
}).then(function (resp) {
|
|
return resp.data;
|
|
}, function (err) {
|
|
return Oauth3.PromiseA.reject(err);
|
|
});
|
|
};
|
|
|
|
// TODO move to LocalApiLogin ?
|
|
TAccounts.attachLoginToAccount = function (conf, account, newLogin) {
|
|
var url = conf.config.apiBaseUri + '/api'
|
|
+ '/org.oauth3.provider'
|
|
+ '/accounts/' + account.appScopedId + '/logins';
|
|
var token = TherapySession.api.getToken(conf, account);
|
|
|
|
return Oauth3.request({
|
|
url: url
|
|
, method: 'POST'
|
|
, data: { logins: [{
|
|
id: newLogin.appScopedId || newLogin.app_scoped_id || newLogin.loginId || newLogin.login_id || newLogin.id
|
|
, token: newLogin.token || newLogin.accessToken || newLogin.access_token
|
|
}] }
|
|
, headers: { 'Authorization': 'Bearer ' + token }
|
|
}).then(function (resp) {
|
|
if (!resp.data) {
|
|
return Oauth3.PromiseA.reject(new Error("no response when linking login to account"));
|
|
}
|
|
if (resp.data.error) {
|
|
return Oauth3.PromiseA.reject(resp.data.error);
|
|
}
|
|
|
|
// return nothing
|
|
}, function (err) {
|
|
console.error('[Error] failed to attach login to account');
|
|
console.warn(err.message);
|
|
console.warn(err.stack);
|
|
return Oauth3.PromiseA.reject(err);
|
|
});
|
|
};
|
|
|
|
TAccounts.requireAccountHelper = function (conf) {
|
|
var session = conf.session;
|
|
var promise;
|
|
var locallogins;
|
|
var err;
|
|
|
|
if (session.accounts.length) {
|
|
return Oauth3.PromiseA.resolve(session);
|
|
}
|
|
|
|
if (!session.logins.length) {
|
|
console.error("doesn't have any logins");
|
|
return Oauth3.PromiseA.reject(new Error("[Developer Error] do not call requireAccount when you have not called requireLogin."));
|
|
}
|
|
|
|
locallogins = session.logins.filter(function (login) {
|
|
return 'localaccount' === login.loginType;
|
|
});
|
|
|
|
if (!locallogins.length) {
|
|
console.error("no local accounts");
|
|
err = new Error("Login with your Local Account at least once before linking other accounts.");
|
|
err.code = "E_NO_LOCAL_ACCOUNT";
|
|
return Oauth3.PromiseA.reject(err);
|
|
}
|
|
|
|
// at this point we have a valid locallogin, but still no localaccount
|
|
promise = Oauth3.PromiseA.resolve();
|
|
|
|
locallogins.forEach(function (login) {
|
|
promise = promise.then(function () {
|
|
return TAccounts.realCreateAccount(conf, login).then(function (account) {
|
|
login.accounts.push(account);
|
|
return save(conf, { login: login, accounts: login.accounts });
|
|
});
|
|
});
|
|
});
|
|
|
|
return promise.then(function (session) {
|
|
return session;
|
|
});
|
|
};
|
|
|
|
TAccounts.requireAccount = function (conf) {
|
|
return TAccounts.requireAccountHelper(conf).then(function () {
|
|
return api.handleOrphanLogins(conf);
|
|
});
|
|
};
|
|
|
|
// TODO move to LocalApiAccount ?
|
|
TAccounts.cloneAccount = function (conf, account) {
|
|
// retrieve the most fresh token of all associated logins
|
|
var token = TherapySession.api.getToken(conf, account);
|
|
var id = TAccounts.getId(account);
|
|
// We don't want to modify the original object and end up
|
|
// with potentially whole stakes in the local storage session key
|
|
account = JSON.parse(JSON.stringify(account));
|
|
|
|
account.token = token;
|
|
account.accountId = account.accountId || account.appScopedId || id;
|
|
account.appScopedId = account.appScopedId || id;
|
|
|
|
return account;
|
|
};
|
|
|
|
// TODO check for account and account create if not exists in requireSession
|
|
// TODO move to LocalApiAccount ?
|
|
TAccounts.selectAccount = function (conf, accountId) {
|
|
var session = conf.session;
|
|
// needs to return the account with a valid login
|
|
var account;
|
|
if (!accountId) {
|
|
accountId = session.accountId;
|
|
}
|
|
|
|
if (!session.accounts.some(function (a) {
|
|
if (!accountId || accountId === TAccounts.getId(a)) {
|
|
account = a;
|
|
return true;
|
|
}
|
|
})) {
|
|
account = session.accounts[0];
|
|
}
|
|
|
|
if (!account) {
|
|
console.error("Developer Error: require session before selecting an account");
|
|
console.error(session);
|
|
throw new Error("Developer Error: require session before selecting an account");
|
|
}
|
|
|
|
account = TAccounts.cloneAccount(conf, account);
|
|
session.accountId = account.accountId;
|
|
session.id = account.accountId;
|
|
session.appScopedId = account.accountId;
|
|
session.token = account.token;
|
|
|
|
// XXX really?
|
|
conf.account = account;
|
|
return account;
|
|
};
|
|
|
|
InternalApi = {
|
|
accounts: accounts
|
|
, login: login
|
|
, getToken: getToken
|
|
};
|
|
|
|
api = {
|
|
save: save
|
|
, restore: restore
|
|
, checkSession: restore
|
|
, destroy: destroy
|
|
, require: requireSession
|
|
, accounts: accounts
|
|
, requireSession: requireSession
|
|
, getToken: getToken
|
|
, addAccountsToSession: addAccountsToSession
|
|
, pruneAccountsFromSession: pruneAccountsFromSession
|
|
, refreshCurrentAccount: refreshCurrentAccount
|
|
, updateSession: updateSession
|
|
, sanityCheckAccounts: sanityCheckAccounts
|
|
, handleOrphanLogins: handleOrphanLogins
|
|
, validateUsername: TLogins.softTestUsername
|
|
, checkUsername: TLogins.hardTestUsername
|
|
, getMeta: TLogins.meta
|
|
, createLogin: TLogins.create
|
|
, login: login
|
|
// this is intended for the resourceOwnerPassword strategy
|
|
, backgroundLogin: backgroundLogin
|
|
, logout: logout
|
|
, onLogin: onLogin
|
|
, onLogout: onLogout
|
|
, requireAccount: TAccounts.requireAccount
|
|
, selectAccount: TAccounts.selectAccount // TODO nix this 'un
|
|
, account: TAccounts.selectAccount
|
|
, testLoginAccounts: testLoginAccounts
|
|
, cloneAccount: TAccounts.cloneAccount
|
|
//, getId: TAccounts.getId
|
|
};
|
|
|
|
TherapySession = {
|
|
create: create
|
|
, api: api
|
|
, getId: TAccounts.getId
|
|
};
|
|
|
|
// XXX
|
|
// These are underscore prefixed because they aren't official API yet
|
|
// I need more time to figure out the proper separation
|
|
TherapySession._logins = TLogins;
|
|
TherapySession._accounts = TAccounts;
|
|
|
|
exports.TherapySession = TherapySession.TherapySession = TherapySession;
|
|
|
|
if ('undefined' !== typeof module) {
|
|
module.exports = TherapySession;
|
|
}
|
|
}('undefined' !== typeof exports ? exports : window));
|