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));
 |