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