KyperTech/matter

View on GitHub
src/index.js

Summary

Maintainability
F
3 days
Test Coverage
import config from './config'
import logger from './utils/logger'
import * as dom from './utils/dom'
import * as request from './utils/request'
import * as ProviderAuth from './utils/providerAuth'
import token from './utils/token'
import * as envStorage from './utils/envStorage'
import {
  isString, isArray,
  isObject, has,
  some, every
} from 'lodash'

export default class Matter {
  /** Constructor
   * @param {String|Object} project Project name or object containing project name and owner
   */
  constructor (project, opts) {
    if (!project) {
      logger.error({
        description: 'Project name required to use Matter.',
        func: 'constructor', obj: 'Matter'
      })
      throw new Error('Project name is required to use Matter')
    }
    if (isObject(project)) {
      this.name = project.name
      this.owner = project.owner || null
    } else {
      this.name = project
    }
    if (opts) {
      this.options = opts
      config.applySettings(opts)
    }
    logger.debug({
      description: 'Matter object built.', matter: this,
      func: 'constructor', obj: 'Matter'
    })
  }

  /** Get current logged in status
   * @return {Boolean}
   * @example
   * //Check if there is an account currently logged in
   * if(matter.isLoggedIn){
   *   console.log('There is currently an account logged in.')
   * } else {
   *   console.warn('There is no account currently logged in.')
   * }
   */
  get isLoggedIn () {
    return isString(this.token.string)
  }

  /** Endpoint generation that handles default/provided settings and environment
   * @return {String} endpoint - endpoint for tessellate application
   */
  get endpoint () {
    // Remove url if host is a tessellate server
    if (typeof window !== 'undefined' && has(window, 'location') && window.location.host.indexOf('tessellate') !== -1) {
      logger.info({
        description: 'App is Tessellate and Host is Tessellate Server, serverUrl simplified!',
        func: 'endpoint', obj: 'Matter'
      })
      return ''
    }
    // Handle tessellate as name
    if (this.name !== 'tessellate') {
      return this.owner ? `${config.serverUrl}/users/${this.owner}/projects/${this.name}` : `${config.serverUrl}/projects/${this.name}`
    }
    return config.serverUrl
  }

  /** Save current user (handled automatically by default)
   * @param {Object} userData - Account data to set for current user
   * @example
   * //Save account response to current user
   * matter.currentUser = {username: 'testuser1', email: 'test@email.com'}
   * console.log('New current user set:', matter.currentUser)
   */
  set currentUser (userData) {
    logger.debug({
      description: 'Current User set.', user: userData,
      func: 'currentUser', obj: 'Matter'
    })
    envStorage.setItem(config.tokenUserDataName, userData)
  }

  /** Get currently logged in user or returns null
   * @return {Object|null}
   * @example
   * //Return account if logged in
   * if(matter.isLoggedIn){
   * console.log('Current user account: ', matter.currentUser)
   * } else {
   * console.log('No current user. Current user: ', matter.currentUser)
   * }
   * matter.currentUser
   * console.log('New current user set:', matter.currentUser)
   */
  get currentUser () {
    if (this.storage.getItem(config.tokenUserDataName)) {
      return this.storage.getItem(config.tokenUserDataName)
    }
    return null
  }

  /* Utility to handle safley writing to localStorage, sessionStorage, and cookies
   * @return {Object}
   */
  get storage () {
    return envStorage
  }

  /** Utility to handle token writing/deleting/decoding
   * @return {Object}
   */
  get token () {
    return token
  }

  /** Utils placed in base library
   * @return {Object}
   */
  get utils () {
    return { logger, request, storage: envStorage, dom }
  }

  /** Signup a new user
   * @param {Object} signupData - Object containing data to use while signing up to application.
   * @param {String} signupData.username - Username of new user (error will be returned if username is taken)
   * @param {String} signupData.email - Email of new user (error will be returned if email is already used)
   * @param {String} signupData.password - Password to be used with account (will be encrypted).
   * @return {Promise}
   * @example
   * //Signup a new user
   * var signupData = {username: 'testuser1', email:'test@email.com', password: 'testpassword'}
   * matter.signup(signupData).then(function(signupRes){
   *  console.log('New user signed up successfully. New account: ', signupRes.account)
   * }, function(err){
   *  console.error('Error signing up:', err)
   * })
   */
  signup (signupData) {
    logger.debug({
      description: 'Signup called.', signupData,
      func: 'signup', obj: 'Matter'
    })
    if (!signupData) {
      logger.error({
        description: 'Signup information is required to signup.',
        func: 'signup', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Signup data is required to signup.',
        status: 'NULL_DATA'
      })
    }
    if (isString(signupData)) {
      return this.authUsingProvider(signupData)
    }
    // Handle no username or email
    if (!signupData.username || !signupData.email) {
      logger.error({
        description: 'Email and Username required to signup.',
        func: 'signup', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Email and Username required to signup.',
        status: 'EMPTY_DATA'
      })
    }
    if (!signupData.password) {
      logger.error({
        description: 'Password is required to signup.',
        func: 'signup', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Password is required to signup.',
        status: 'PASS_REQUIRED'
      })
    }
    return request.post(`${this.endpoint}/signup`, signupData).then(response => {
      if (response.token) {
        this.token.string = response.token
      }
      if (response.user) {
        this.currentUser = response.user
      }
      logger.info({
        description: 'Signup successful.', user: this.currentUser,
        func: 'signup', obj: 'Matter'
      })
      return this.currentUser
    })['catch'](error => {
      logger.error({
        description: 'Error requesting signup.', error,
        signupData, func: 'signup', obj: 'Matter'
      })
      return Promise.reject(error)
    })
  }

  /** Login by username/email
   * @param {Object} loginData - Object containing data to use while logging in to application.
   * @param {String} loginData.username - Username of user to login as
   * @param {String} loginData.email - Email of new user (Optional instead of username)
   * @param {String} loginData.password - Password to be used with account (will be encrypted).
   * @return {Promise}
   * @example
   * //Login as 'testuser1'
   * var loginData = {username: 'testuser1', password: 'testpassword'}
   * matter.login(loginData).then(function(loginRes){
   *  console.log('New user logged in succesfully. Account: ', loginRes.user)
   * }, function(err){
   *  console.error('Error logging in:', err)
   * })
   */
  login (loginData) {
    if (!loginData || (!isObject(loginData) && !isString(loginData))) {
      logger.error({
        description: 'Username/Email and Password are required to login',
        func: 'login', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Login data is required to login.',
        status: 'DATA_REQUIRED'
      })
    }
    // Provider login
    if (isString(loginData)) {
      return this.authUsingProvider(loginData)
    }
    // No username or email
    if (!loginData.username && !loginData.email) {
      logger.error({
        description: 'Email or Username required to login.',
        func: 'login', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Email or Username required to login.',
        status: 'ID_REQUIRED'
      })
    }
    // Handle null or invalid password
    if (!loginData.password || loginData.password === '') {
      return Promise.reject({
        message: 'Password is required to login.',
        status: 'PASS_REQUIRED'
      })
    }
    // Username/Email Login
    return request.put(`${this.endpoint}/login`, loginData)
    .then(response => {
      if (response.data && response.data.status && response.data.status === 409) {
        logger.error({
          description: 'User not found.', response,
          func: 'login', obj: 'Matter'
        })
        return Promise.reject(response.data)
      }
      if (response.token) {
        this.token.string = response.token
      }
      if (response.user) {
        this.currentUser = response.user
      }
      logger.info({
        description: 'Successful login.', user: this.currentUser,
        func: 'login', obj: 'Matter'
      })
      return this.currentUser
    })['catch'](error => {
      logger.error({
        description: 'Error requesting login.',
        error, func: 'login', obj: 'Matter'
      })
      if (error.status === 409 || error.status === 400) {
        error = error.response.text
      }
      return Promise.reject(error)
    })
  }

  /** logout
   * @description Log out of currently logged in user account
   * @return {Promise}
   * @example
   * //Logout of currently logged in account
   * matter.logout().then(function(loginRes){
   *  console.log('Logged out successfully')
   * }, function(err){
   *  console.error('Error logging out:', err)
   * })
   */
  logout () {
    // TODO: Handle logging out of providers
    if (!this.isLoggedIn) {
      logger.warn({
        description: 'No logged in account to log out.',
        func: 'logout', obj: 'Matter'
      })
      return Promise.reject({
        message: 'No logged in account to log out.',
        status: 'NULL_ACCOUNT'
      })
    }
    return request.put(`${this.endpoint}/logout`).then(response => {
      logger.info({
        description: 'Logout successful.',
        response, func: 'logout', obj: 'Matter'
      })
      this.currentUser = null
      this.token.delete()
      return response
    })['catch'](error => {
      logger.error({
        description: 'Error requesting log out: ',
        error, func: 'logout', obj: 'Matter'
      })
      this.storage.removeItem(config.tokenUserDataName)
      this.token.delete()
      return Promise.reject(error)
    })
  }

  /** Authenticate using external provider
   * @param {String} provider - Provider name
   * @return {Promise}
   * @example
   * //Signup using google
   * matter.authUsingProvider('google').then(function(signupRes){
   *  console.log('New user logged in succesfully. Account: ', signupRes.user)
   * }, function(err){
   *  console.error('Error logging in:', err)
   * })
   */
  authUsingProvider (provider) {
    if (!provider) {
      logger.info({
        description: 'Provider required to sign up.',
        func: 'authUsingProvider', obj: 'Matter'
      })
      return Promise.reject({message: 'Provider data is required to signup.'})
    }
    return ProviderAuth.authWithServer(provider).then(response => {
      logger.info({
        description: 'Provider login successful.',
        response, func: 'authUsingProvider', obj: 'Matter'
      })
      if (response && response.token) {
        this.token.string = response.token
      }
      if (response && response.user || response.data) {
        this.currentUser = response.data || response.user
      }
      return this.currentUser
    }, error => {
      logger.error({
        description: 'Provider signup error.', error,
        func: 'authUsingProvider', obj: 'Matter'
      })
      return Promise.reject(error)
    })
  }

  /** getCurrentUser
   * @return {Promise}
   * @example
   * //Logout of currently logged in account
   * matter.getCurrentUser().then(function(currentAccount){
   *  console.log('Currently logged in account:', currentAccount)
   * }, function(err){
   *  console.error('Error logging out:', err)
   * })
   */
  getCurrentUser () {
    if (this.currentUser) {
      logger.debug({
        description: 'Current is already available. Returning user.',
        func: 'currentUser', obj: 'Matter'
      })
      return Promise.resolve(this.currentUser)
    }
    if (!this.isLoggedIn) {
      logger.debug({
        description: 'Current user is null.',
        func: 'currentUser', obj: 'Matter'
      })
      return Promise.resolve(null)
    }
    return request.get(`${this.endpoint}/user`).then(response => {
      // TODO: Save user information locally
      logger.log({
        description: 'Current User Request responded.',
        response, func: 'currentUser', obj: 'Matter'
      })
      this.currentUser = response
      return response
    })['catch'](error => {
      if (error.status === 401) {
        logger.warn({
          description: 'Called for current user without token.',
          error, func: 'currentUser', obj: 'Matter'
        })
        token.delete()
        return Promise.resolve(null)
      }
      logger.error({
        description: 'Error requesting current user.',
        error, func: 'currentUser', obj: 'Matter'
      })
      return Promise.reject(error)
    })
  }

  /** updateAccount
   * @param {Object} updateData - Data to update within profile (only provided data will be modified).
   * @return {Promise}
   * @example
   * //Update current account's profile
   * matter.updateAccount().then(function(updatedAccount){
   *  console.log('Currently logged in account:', updatedAccount)
   * }, function(err){
   *  console.error('Error updating profile:', err)
   * })
   */
  updateAccount (updateData) {
    if (!this.isLoggedIn) {
      logger.error({
        description: 'No current user profile to update.',
        func: 'updateAccount', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Must be logged in to update account.'
      })
    }
    if (!updateData) {
      logger.error({
        description: 'Data is required to update profile.',
        func: 'updateAccount', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Data required to update account.',
        status: 'NULL_DATA'
      })
    }
    // Send update request
    return request.put(`${this.endpoint}/users/${this.currentUser.username}`, updateData)
    .then(response => {
      logger.info({
        description: 'Update profile request responded.',
        response, func: 'updateAccount', obj: 'Matter'
      })
      this.currentUser = response
      return response
    })['catch'](error => {
      logger.error({
        description: 'Error requesting current user.',
        error, func: 'updateAccount', obj: 'Matter'
      })
      return Promise.reject(error)
    })
  }

  /** uploadAvatar
   * @description Upload account avatar to Tessellate
   * @param {Object} file - File object to upload
   * @return {Promise}
   * @example
   * //Upload image to tessellate
   * matter.uploadAvatar(file).then(function(imgUrl){
   *  console.log('Currently logged in account:', imgUrl)
   * }, function(err){
   *  console.error('Error uploading image:', err)
   * })
   */
  uploadAvatar (file) {
    if (!this.isLoggedIn) {
      logger.error({
        description: 'Must be logged in to upload an image.',
        func: 'uploadAvatar', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Must be logged in to upload image.'
      })
    }
    if (!file) {
      logger.error({
        description: 'File is required to upload Avatar.',
        func: 'uploadAvatar', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Data required to update profile.',
        status: 'NULL_DATA'
      })
    }
    const reqData = { files: [ { key: 'image', file } ] }
    // Send update request
    return request.put(`${this.endpoint}/users/${this.currentUser.username}/avatar`, reqData)
  }

  /** changePassword
   * @param {String} updateData New password for account.
   * @return {Promise}
   * @example
   * //Update current account's password
   * var newPassword = 'asdfasdfasdf'
   * matter.changePassword(newPassword).then(function(updatedAccount){
   *  console.log('Currently logged in account:', updatedAccount)
   * }, function(err){
   *  console.error('Error changing password:', err)
   * })
   */
  changePassword (newPassword) {
    if (!this.isLoggedIn) {
      logger.error({
        description: 'No current user profile for which to change password.',
        func: 'changePassword', obj: 'Matter'
      })
      return Promise.reject({
        message: 'Must be logged in to change password.'
      })
    }
    // Send update request
    return request.put(`${this.endpoint}/user/password`, newPassword)
  }

  /** recoverAccount
   * @param {String} updateData New password for account.
   * @return {Promise}
   * @example
   * //Recover current users password
   * matter.recoverAccount().then(function(updatedAccount){
   *  console.log('Currently logged in account:', updatedAccount)
   * }, function(err){
   *  console.error('Error updating profile:', err)
   * })
   */
  recoverAccount (accountData) {
    if (!accountData) {
      logger.error({
        description: 'Account data is required to recover an account.',
        func: 'recoverAccount', obj: 'Matter'
      })
      return Promise.reject({message: 'Account data is required to recover an account.'})
    }
    let account = {}
    if (isString(accountData)) {
      account = accountData.indexOf('@') !== -1 ? { email: accountData } : { username: accountData }
    } else {
      account = accountData
    }
    logger.debug({
      description: 'Requesting recovery of account.', account,
      func: 'recoverAccount', obj: 'Matter'
    })
    return request.put(`${this.endpoint}/user/recover`, account)
  }

  /** Check that user is in a single group or in all of a list of groups
   * @param {Array} checkGroups - List of groups to check for account membership
   * @return {Boolean}
   * @example
   * //Check for group membership
   * var isBoth =
   * if(matter.isInGroup('admins')){
   * console.log('Current account is an admin!')
   * } else {
   * console.warn('Current account is not an admin.')
   * }
   */
  isInGroup (checkGroups) {
    if (!this.isLoggedIn) {
      logger.error({
        description: 'No logged in user to check for groups.',
        func: 'isInGroup', obj: 'Matter'
      })
      return false
    }
    if (!checkGroups) {
      logger.log({
        description: 'Invalid group(s).',
        func: 'isInGroup', obj: 'Matter'
      })
      return false
    }
    // Check if user is within groups
    if (isString(checkGroups)) {
      const groupName = checkGroups
      // Single role or string list of roles
      const groupsArray = groupName.split(',')
      if (groupsArray.length > 1) {
        // String list of groupts
        logger.info({
          description: 'String list of groups.', list: groupsArray,
          func: 'isInGroup', obj: 'Matter'
        })
        return this.isInGroups(groupsArray)
      }
      // Single group
      let groups = this.token.data.groups || []
      logger.log({
        description: 'Checking if user is in group.',
        group: groupName, userGroups: this.token.data.groups,
        func: 'isInGroup', obj: 'Matter'
      })
      return some(groups, (group) => {
        return groupName === group.name
      })
    }
    if (isArray(checkGroups)) {
      return this.isInGroups(checkGroups)
    }
    return false
  }

  /** Check that user is in all of a list of groups
   * @param {Array|String} checkGroups - List of groups to check for account membership
   * @return {Boolean}
   * @example
   * //Check for group membership
   * var isBoth = matter.isInGroups(['admins', 'users'])
   * if(isBoth){
   * console.log('Current account is both an admin and a user')
   * } else {
   * console.warn('Current account is not both an admin and a user')
   * }
   */
  isInGroups (checkGroups) {
    if (!this.isLoggedIn) {
      logger.log({
        description: 'No logged in user to check.',
        func: 'isInGroups', obj: 'Matter'
      })
      return false
    }
    if (!checkGroups) {
      logger.log({
        description: 'Invalid group(s).',
        func: 'isInGroup', obj: 'Matter'
      })
      return false
    }
    // Check if user is in some of the provided groups
    if (isArray(checkGroups)) {
      return every(checkGroups.map(group => {
        if (isString(group)) {
          // Group is string
          return this.isInGroup(group)
        }
        // Group is object
        if (has(group, 'name')) {
          return this.isInGroup(group.name)
        }
        logger.error({
          description: 'Invalid group object.',
          group: group, func: 'isInGroups', obj: 'Matter'
        })
        return false
      }), true)
    }
    if (isString(checkGroups)) {
      // TODO: Handle spaces within string list
      const groupsArray = checkGroups.split(',')
      if (groupsArray.length > 1) {
        return this.isInGroups(groupsArray)
      }
      return this.isInGroup(groupsArray[0])
    }
    logger.error({
      description: 'Invalid groups list.',
      func: 'isInGroups', obj: 'Matter'
    })
    return false
  }
}