Home Reference Source

core/user/user.js

import Promise from 'es6-promise';
import assign from 'lodash/assign';
import isString from 'lodash/isString';
import isObject from 'lodash/isObject';
import isEmpty from 'lodash/isEmpty';
import url from 'url';
import { Client } from '../client';
import { AuthType, RequestMethod, KinveyRequest } from '../request';
import { KinveyError, NotFoundError, ActiveUserError } from '../errors';
import { DataStore } from '../datastore';
import { MobileIdentityConnect } from '../identity';
import { Log } from '../log';
import { isDefined } from '../utils';
import { Acl } from '../acl';
import { Metadata } from '../metadata';
import { getLiveService } from '../live';
import { UserStore } from './userstore';

/**
 * The User class is used to represent a single user on the Kinvey platform.
 * Use the user class to manage the active user lifecycle and perform user operations.
 */
export class User {
  /**
   * Create a new instance of a User.
   *
   * @param {Object} [data={}] Data for the user.
   * @param {Object} [options={}] Options.
   * @return {User} User
   */
  constructor(data = {}, options = {}) {
    /**
     * The users data.
     *
     * @type {Object}
     */
    this.data = data;

    /**
     * @private
     * The client used by this user.
     *
     * @type {Client}
     */
    this.client = options.client || Client.sharedInstance();
  }

  /**
   * The _id for the user.
   *
   * @return {?string} _id
   */
  get _id() {
    return this.data._id;
  }

  /**
   * The _acl for the user.
   *
   * @return {Acl} _acl
   */
  get _acl() {
    return new Acl(this.data);
  }

  /**
   * The metadata for the user.
   *
   * @return {Metadata} metadata
   */
  get metadata() {
    return new Metadata(this.data);
  }

  /**
   * The _kmd for the user.
   *
   * @return {Metadata} _kmd
   */
  get _kmd() {
    return this.metadata;
  }

  /**
   * The _socialIdentity for the user.
   *
   * @return {Object} _socialIdentity
   */
  get _socialIdentity() {
    return this.data._socialIdentity;
  }

  /**
   * The auth token for the user.
   *
   * @return {?string} Auth token
   */
  get authtoken() {
    return this.metadata.authtoken;
  }

  /**
   * The username for the user.
   *
   * @return {?string} Username
   */
  get username() {
    return this.data.username;
  }

  /**
   * The email for the user.
   *
   * @return {?string} Email
   */
  get email() {
    return this.data.email;
  }

  /**
   * @private
   */
  get pathname() {
    return `/user/${this.client.appKey}`;
  }

  /**
   * Checks if the user is the active user.
   *
   * @return {boolean} True the user is the active user otherwise false.
   */
  isActive() {
    const activeUser = User.getActiveUser(this.client);

    if (isDefined(activeUser) && activeUser._id === this._id) {
      return true;
    }

    return false;
  }

  /**
   * Checks if the users email is verfified.
   *
   * @return {boolean} True if the users email is verified otherwise false.
   */
  isEmailVerified() {
    const status = this.metadata.emailVerification;
    return status === 'confirmed';
  }

  /**
   * Login using a username or password.
   *
   * @param {string|Object} username Username or an object with username and password as properties.
   * @param {string} [password] Password
   * @param {Object} [options={}] Options
   * @return {Promise<User>} The user.
   */
  login(username, password, options = {}) {
    let credentials = username;
    const isActive = this.isActive();
    const activeUser = User.getActiveUser(this.client);

    if (isActive === true) {
      return Promise.reject(new ActiveUserError('This user is already the active user.'));
    }

    if (isDefined(activeUser)) {
      return Promise.reject(
        new ActiveUserError('An active user already exists. Please logout the active user before you login.')
      );
    }

    if (isObject(credentials)) {
      options = password || {};
    } else {
      credentials = {
        username: username,
        password: password
      };
    }

    if (isDefined(credentials.username)) {
      credentials.username = String(credentials.username).trim();
    }

    if (isDefined(credentials.password)) {
      credentials.password = String(credentials.password).trim();
    }

    if ((!isDefined(credentials.username)
      || credentials.username === ''
      || !isDefined(credentials.password)
      || credentials.password === ''
    ) && !isDefined(credentials._socialIdentity)) {
      return Promise.reject(
        new KinveyError('Username and/or password missing. Please provide both a username and password to login.')
      );
    }

    const request = new KinveyRequest({
      method: RequestMethod.POST,
      authType: AuthType.App,
      url: url.format({
        protocol: this.client.apiProtocol,
        host: this.client.apiHost,
        pathname: `${this.pathname}/login`
      }),
      body: credentials,
      properties: options.properties,
      timeout: options.timeout,
      client: this.client
    });

    return request.execute()
      .then(response => response.data)
      .then((data) => {
        if (isDefined(credentials._socialIdentity) && isDefined(data._socialIdentity)) {
          const identities = Object.keys(data._socialIdentity);
          identities.forEach((identity) => {
            data._socialIdentity[identity] = assign(
              {},
              credentials._socialIdentity[identity],
              data._socialIdentity[identity]
            );
          });
          data._socialIdentity = assign({}, credentials._socialIdentity, data._socialIdentity);
        }

        // Remove sensitive data
        delete data.password;

        // Store the active user
        return this.client.setActiveUser(data);
      })
      .then((data) => {
        this.data = data;
        return this;
      });
  }

  /**
   * Login using a username or password.
   *
   * @param {string|Object} username Username or an object with username and password as properties.
   * @param {string} [password] Password
   * @param {Object} [options={}] Options
   * @return {Promise<User>} The user.
   */
  static login(username, password, options = {}) {
    const user = new User({}, options);
    return user.login(username, password, options);
  }

  /**
   * Login using Mobile Identity Connect.
   *
   * @param {string} redirectUri The redirect uri.
   * @param {AuthorizationGrant} [authorizationGrant=AuthoizationGrant.AuthorizationCodeLoginPage] MIC authorization grant to use.
   * @param {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  loginWithMIC(redirectUri, authorizationGrant, options = {}) {
    const isActive = this.isActive();
    const activeUser = User.getActiveUser(this.client);

    if (isActive) {
      return Promise.reject(new ActiveUserError('This user is already the active user.'));
    }

    if (isDefined(activeUser)) {
      return Promise.reject(new ActiveUserError('An active user already exists. Please logout the active user before you login.'));
    }

    const mic = new MobileIdentityConnect({ client: this.client });
    return mic.login(redirectUri, authorizationGrant, options)
      .then(session => this.connectIdentity(MobileIdentityConnect.identity, session, options));
  }

  /**
   * Login using Mobile Identity Connect.
   *
   * @param {string} redirectUri The redirect uri.
   * @param {AuthorizationGrant} [authorizationGrant=AuthoizationGrant.AuthorizationCodeLoginPage] MIC authorization grant to use.
   * @param {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  static loginWithMIC(redirectUri, authorizationGrant, options = {}) {
    const user = new this({}, options);
    return user.loginWithMIC(redirectUri, authorizationGrant, options);
  }

  /**
   * Connect a social identity.
   *
   * @param {string} identity Social identity.
   * @param {Object} session Social identity session.
   * @param {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  connectIdentity(identity, session, options = {}) {
    const isActive = this.isActive();
    const data = {};
    const socialIdentity = data._socialIdentity || {};
    socialIdentity[identity] = session;
    data._socialIdentity = socialIdentity;

    if (isActive) {
      return this.update(data, options);
    }

    return this.login(data, options)
      .catch((error) => {
        if (error instanceof NotFoundError) {
          return this.signup(data, options)
            .then(() => this.connectIdentity(identity, session, options));
        }

        throw error;
      });
  }

  /**
   * Connect a social identity.
   *
   * @param {string} identity Social identity.
   * @param {Object} session Social identity session.
   * @param {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  static connectIdentity(identity, session, options = {}) {
    const user = new this({}, options);
    return user.connectIdentity(identity, session, options);
  }

  /**
   * @private
   * Disconnects the user from  an identity.
   *
   * @param {SocialIdentity|string} identity Identity used to connect the user.
   * @param  {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  disconnectIdentity(identity, options = {}) {
    let promise = Promise.resolve();

    if (identity === MobileIdentityConnect.identity) {
      promise = MobileIdentityConnect.logout(this, options);
    }

    return promise
      .catch((error) => {
        Log.error(error);
      })
      .then(() => {
        const data = this.data;
        const socialIdentity = data._socialIdentity || {};
        delete socialIdentity[identity];
        data._socialIdentity = socialIdentity;
        this.data = data;

        if (!this._id) {
          return this;
        }

        return this.update(data, options);
      })
      .then(() => this);
  }

  /**
   * Logout the active user.
   *
   * @param {Object} [options={}] Options
   * @return {Promise<User>} The user.
   */
  logout(options = {}) {
    // Logout from  Kinvey
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      authType: AuthType.Session,
      url: url.format({
        protocol: this.client.apiProtocol,
        host: this.client.apiHost,
        pathname: `${this.pathname}/_logout`
      }),
      properties: options.properties,
      timeout: options.timeout,
      client: this.client
    });

    return this.unregisterFromLiveService()
      .then(() => request.execute())
      .catch((error) => {
        Log.error(error);
        return null;
      })
      .then(() => {
        return this.client.setActiveUser(null);
      })
      .catch((error) => {
        Log.error(error);
        return null;
      })
      .then(() => DataStore.clearCache({ client: this.client }))
      .catch((error) => {
        Log.error(error);
        return null;
      })
      .then(() => this);
  }

  /**
   * Logout the active user.
   *
   * @param {Object} [options={}] Options
   * @return {Promise<User>} The user.
   */
  static logout(options = {}) {
    const activeUser = User.getActiveUser(options.client);

    if (isDefined(activeUser)) {
      return activeUser.logout(options);
    }

    return Promise.resolve(null);
  }

  /**
   * Sign up a user with Kinvey.
   *
   * @param {?User|?Object} data Users data.
   * @param {Object} [options] Options
   * @param {boolean} [options.state=true] If set to true, the user will be set as the active user after successfully
   *                                       being signed up.
   * @return {Promise<User>} The user.
   */
  signup(data, options = {}) {
    const activeUser = User.getActiveUser(this.client);
    options = assign({
      state: true
    }, options);

    if (options.state === true && isDefined(activeUser)) {
      throw new ActiveUserError('An active user already exists. Please logout the active user before you login.');
    }

    if (data instanceof User) {
      data = data.data;
    }

    // Merge the data
    data = assign(this.data, data);

    const request = new KinveyRequest({
      method: RequestMethod.POST,
      authType: AuthType.App,
      url: url.format({
        protocol: this.client.apiProtocol,
        host: this.client.apiHost,
        pathname: this.pathname
      }),
      body: isEmpty(data) ? null : data,
      properties: options.properties,
      timeout: options.timeout,
      client: this.client
    });

    return request.execute()
      .then(response => response.data)
      .then((data) => {
        if (options.state === true) {
          return this.client.setActiveUser(data);
        }

        return data;
      })
      .then((data) => {
        this.data = data;
        return this;
      });
  }

  /**
   * Sign up a user with Kinvey.
   *
   * @param {User|Object} data Users data.
   * @param {Object} [options] Options
   * @param {boolean} [options.state=true] If set to true, the user will be set as the active user after successfully
   *                                       being signed up.
   * @return {Promise<User>} The user.
   */
  static signup(data, options = {}) {
    const user = new this({}, options);
    return user.signup(data, options);
  }

  /**
   * Sign up a user with Kinvey using an identity.
   *
   * @param {SocialIdentity|string} identity The identity.
   * @param {Object} session Identity session
   * @param {Object} [options] Options
   * @param {boolean} [options.state=true] If set to true, the user will be set as the active user after successfully
   *                                       being signed up.
   * @return {Promise<User>} The user.
   */
  signupWithIdentity(identity, session, options = {}) {
    const data = {};
    data._socialIdentity = {};
    data._socialIdentity[identity] = session;
    return this.signup(data, options);
  }

  /**
   * Sign up a user with Kinvey using an identity.
   *
   * @param {SocialIdentity|string} identity The identity.
   * @param {Object} session Identity session
   * @param {Object} [options] Options
   * @param {boolean} [options.state=true] If set to true, the user will be set as the active user after successfully
   *                                       being signed up.
   * @return {Promise<User>} The user.
   */
  static signupWithIdentity(identity, session, options = {}) {
    const user = new this({}, options);
    return user.signupWithIdentity(identity, session, options);
  }

  /**
   * Update the users data.
   *
   * @param {Object} data Data.
   * @param {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  update(data, options = {}) {
    data = assign(this.data, data);
    const store = new UserStore();
    return store.update(data, options)
      .then((data) => {
        if (this.isActive()) {
          return this.client.setActiveUser(data);
        }

        return data;
      })
      .then((data) => {
        this.data = data;
        return this;
      });
  }

  /**
   * Update the active user.
   *
   * @param {Object} data Data.
   * @param {Object} [options] Options
   * @return {Promise<User>} The user.
   */
  static update(data, options = {}) {
    const activeUser = User.getActiveUser(options.client);

    if (isDefined(activeUser)) {
      return activeUser.update(data, options);
    }

    return Promise.resolve(null);
  }

  /**
   * Retfresh the users data.
   *
   * @param {Object} [options={}] Options
   * @return {Promise<User>} The user.
   */
  me(options = {}) {
    const request = new KinveyRequest({
      method: RequestMethod.GET,
      authType: AuthType.Session,
      url: url.format({
        protocol: this.client.apiProtocol,
        host: this.client.apiHost,
        pathname: `${this.pathname}/_me`
      }),
      properties: options.properties,
      timeout: options.timeout
    });

    return request.execute()
      .then(response => response.data)
      .then((data) => {
        // Remove sensitive data
        delete data.password;

        // Store the active user
        if (this.isActive()) {
          return this.client.setActiveUser(data);
        }

        return data;
      })
      .then((data) => {
        this.data = data;
        return this;
      });
  }

  /**
   * @returns {Promise}
   */
  static registerForLiveService() {
    const activeUser = User.getActiveUser();

    if (activeUser) {
      return activeUser.registerForLiveService();
    }

    return Promise.reject(new ActiveUserError('There is no active user'));
  }

  /**
   * @returns {Promise}
   */
  static unregisterFromLiveService() {
    const activeUser = User.getActiveUser();

    if (activeUser) {
      return activeUser.unregisterFromLiveService();
    }

    return Promise.reject(new ActiveUserError('There is no active user'));
  }

  registerForLiveService() {
    const liveService = getLiveService(this.client);
    let promise = Promise.resolve();
    if (!liveService.isInitialized()) {
      promise = liveService.fullInitialization(this);
    }
    return promise;
  }

  unregisterFromLiveService() {
    const liveService = getLiveService(this.client);
    let promise = Promise.resolve();
    if (liveService.isInitialized()) {
      promise = liveService.fullUninitialization();
    }
    return promise;
  }

  /**
   * Refresh the active user.
   *
   * @param {Object} [options={}] Options
   * @return {Promise<User>} The user.
   */
  static me(options = {}) {
    const activeUser = User.getActiveUser(options.client);

    if (activeUser) {
      return activeUser.me(options);
    }

    return Promise.resolve(null);
  }

  /**
   * Gets the active user. You can optionally provide a client
   * to use to lookup the active user.
   *
   * @param {Client} [client=Client.sharedInstance()] Client to use to lookup active user.
   * @return {?User} The active user.
   */
  static getActiveUser(client = Client.sharedInstance()) {
    const data = client.getActiveUser();

    if (isDefined(data)) {
      return new this(data, { client: client });
    }

    return null;
  }

  /**
   * Request an email to be sent to verify the users email.
   *
   * @param {string} username Username
   * @param {Object} [options={}] Options
   * @return {Promise<Object>} The response.
   */
  static verifyEmail(username, options = {}) {
    if (!username) {
      return Promise.reject(
        new KinveyError('A username was not provided.',
          'Please provide a username for the user that you would like to verify their email.')
      );
    }

    if (!isString(username)) {
      return Promise.reject(new KinveyError('The provided username is not a string.'));
    }

    const client = options.client || Client.sharedInstance();
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      authType: AuthType.App,
      url: url.format({
        protocol: client.apiProtocol,
        host: client.apiHost,
        pathname: `/rpc/${client.appKey}/${username}/user-email-verification-initiate`
      }),
      properties: options.properties,
      timeout: options.timeout,
      client: client
    });
    return request.execute()
      .then(response => response.data);
  }

  /**
   * Request an email to be sent to recover a forgot username.
   *
   * @param {string} email Email
   * @param {Object} [options={}] Options
   * @return {Promise<Object>} The response.
   */
  static forgotUsername(email, options = {}) {
    if (!email) {
      return Promise.reject(
        new KinveyError('An email was not provided.',
          'Please provide an email for the user that you would like to retrieve their username.')
      );
    }

    if (!isString(email)) {
      return Promise.reject(new KinveyError('The provided email is not a string.'));
    }

    const client = options.client || Client.sharedInstance();
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      authType: AuthType.App,
      url: url.format({
        protocol: client.apiProtocol,
        host: client.apiHost,
        pathname: `/rpc/${client.appKey}/user-forgot-username`
      }),
      properties: options.properties,
      data: { email: email },
      timeout: options.timeout,
      client: client
    });
    return request.execute()
      .then(response => response.data);
  }

  /**
   * Request an email to be sent to reset a users password.
   *
   * @param {string} username Username
   * @param {Object} [options={}] Options
   * @return {Promise<Object>} The response.
   */
  static resetPassword(username, options = {}) {
    if (!username) {
      return Promise.reject(
        new KinveyError('A username was not provided.',
          'Please provide a username for the user that you would like to verify their email.')
      );
    }

    if (!isString(username)) {
      return Promise.reject(new KinveyError('The provided username is not a string.'));
    }

    const client = options.client || Client.sharedInstance();
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      authType: AuthType.App,
      url: url.format({
        protocol: client.apiProtocol,
        host: client.apiHost,
        pathname: `/rpc/${client.appKey}/${username}/user-password-reset-initiate`
      }),
      properties: options.properties,
      timeout: options.timeout,
      client: client
    });
    return request.execute()
      .then(response => response.data);
  }

  /**
   * Lookup users.
   *
   * @param {Query} [query] Query used to filter entities.
   * @param {Object} [options] Options
   * @return {Observable} Observable.
   */
  static lookup(query, options = {}) {
    const store = new UserStore();
    return store.lookup(query, options);
  }

  /**
   * Check if a username already exists.
   *
   * @param {string} username Username
   * @param {Object} [options] Options
   * @return {boolean} True if the username already exists otherwise false.
   */
  static exists(username, options = {}) {
    const store = new UserStore();
    return store.exists(username, options);
  }

  /**
   * Remove a user.
   *
   * @param   {string}  id               Id of the user to remove.
   * @param   {Object}  [options]        Options
   * @param   {boolean} [options.hard=false]   Boolean indicating whether user should be permanently removed from  the backend (defaults to false).
   * @return  {Promise}
   */
  static remove(id, options = {}) {
    const store = new UserStore();
    return store.removeById(id, options);
  }

  /**
   * Restore a user that has been suspended.
   *
   * @throw {KinveyError} Unsupported method.
   */
  static restore() {
    return Promise.reject(new KinveyError('This function requires a master secret to be provided for your application.'
      + ' We strongly advise not to do this.'));
  }
}