forked from coolaj86/walnut.js
		
	
		
			
	
	
		
			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)); |