dashboard.js

/*
 * Copyright (c) 2018-2019 Zippie Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */
const axios = require('axios')
const AuthedMessage = require('./message')
const secp256k1 = require('secp256k1')

/**
 * Normalize public key parameter by ensuring compressed Buffer instance.
 * @protected
 * @param {Buffer|String} pubkey 
 */
function PublicKey (pubkey) {
  pubkey = typeof pubkey === 'string' ? Buffer.from(pubkey, 'hex') : pubkey
  pubkey = secp256k1.publicKeyConvert(pubkey, false)
  return pubkey
}

/**
 * Initialize a new dashboard api connection to URI
 * @class
 * @public
 * 
 * @param {string} [uri='https://dashboard-service.zippie.org'] Dashboard service URI
 * 
 * @example
 * const Dashboard = require('../lib/dashboard.js')
 * const api = new Dashboard('https://dashboard-service.zippie.org')
 * api.initWithKey(SERVICE_KEY)
 * 
 * api.CheckAllowed(some_key, permission)
 *   .then(isAllowed => {
 *     console.info(some_key, permission, isAllowed)
 *   })
 *   .catch(error => {
 *     console.error('Error:', error)
 *   })
 */
function Dashboard (__uri) {
  __uri = __uri || 'https://dashboard-service.zippie.org'

  let __signer

  /**
   * Initialize dashboard API connection using provided user private key.
   * @public
   * @method Dashboard#initWithKey
   * 
   * @param {*} key 
   * 
   * @example
   * const Dashboard = require('../lib/dashboard.js')
   * const api = new Dashboard('https://dashboard-service.zippie.org')
   * api.initWithKey(SERVICE_KEY)
   * 
   * api.CheckAllowed(some_key, permission)
   *   .then(isAllowed => {
   *     console.info(some_key, permission, isAllowed)
   *   })
   *   .catch(error => {
   *     console.error('Error:', error)
   *   })
   */
  const initWithKey = (key) => {
    __signer = signWithKey(key)
  }

  /**
   * Initialize dashboard API connection using vault and key derivation path.
   * @public
   * @method Dashboard#initWithVault
   * 
   * @param {*} vault 
   * @param {*} derive 
   * 
   * @example
   * const Dashboard = require('../lib/dashboard.js')
   * const api = new Dashboard('https://dashboard-service.zippie.org')
   * api.initWithVault(vault, 'm/0')
   * 
   * api.CheckAllowed(some_key, permission)
   *   .then(isAllowed => {
   *     console.info(some_key, permission, isAllowed)
   *   })
   *   .catch(error => {
   *     console.error('Error:', error)
   *   })
   */
  const initWithVault = (vault, derive) => {
    __signer = signWithVault(vault, derive)
  }

  /** @private */
  const signWithKey = (key) => {
    return function (mesg) {
        return Promise.resolve(mesg.sign(key))
    }
  }

  /** @private */
  const signWithVault = (vault, derive) => {
    return function (mesg) {
      return mesg.signWithVault(vault, derive)
    }
  }

  /** @private */
  const signAndSend = async (action, mesg) => {
    try {
      const result = await axios.post(__uri + '/api/v1' + action, (await __signer(mesg)).serialize())

      if ('error' in result.data) {
        return Promise.reject(result.data.error)
      }

      return Promise.resolve(result.data.result)
    } catch (e) {
      if (e.response.data.error) return Promise.reject(e.response.status + ' - ' + JSON.stringify(e.response.data))
      if (e.response) return Promise.reject(e.response.status + ' - ' + e.response.statusText)
      if (e.errno) return Promise.reject(e.errno)
      return Promise.reject(e)
    }
  }

  /**
   * Get a list of all organisations in database, or if pubkey supplied, get organisations
   * that the user is a part of.
   * 
   * @method Dashboard#ListOrganisations
   * @public
   * 
   * @param {PublicKey} userpub Users public key.
   * 
   * @returns {OrganisationListItem[]}
   */
  this.ListOrganisations = (userpub) => {
    userpub = userpub ? PublicKey(userpub).toString('hex') : undefined
    return signAndSend('/ListOrganisations', new AuthedMessage({ userpub }))
  }

  /**
   * Create a new empty named organisation.
   * 
   * @method Dashboard#CreateOrganisation
   * @public
   * 
   * @param {String} name The name of the organisation
   * 
   * @returns {String} Organisation public key
   */
  this.CreateOrganisation = (name) => {
    return signAndSend('/CreateOrganisation', new AuthedMessage({
      name,
    }))
  }

  /**
   * Get the organisation metadata
   * 
   * @method Dashboard#GetOrganisationInfo
   * @public
   * 
   * @param {PublicKey} orgpub Organisation public key
   * 
   * @returns {OrganisationInfo} Organisation metadata
   */
  this.GetOrganisationInfo = (orgpub) => {
    const pubkey = PublicKey(orgpub).toString('hex')
    return signAndSend('/GetOrganisationInfo', new AuthedMessage({
      pubkey,
    }))
  }

  /**
   * Set the organisation metadata. (Setting a field to null removes it)
   * 
   * @method Dashboard#SetOrganisationInfo
   * @public
   * 
   * @param {PublicKey} orgpub Organisation public key
   * @param {Object} info Changes to metadata
   * 
   * @returns {Boolean}
   */
  this.SetOrganisationInfo = (pubkey, info) => {
    pubkey = PublicKey(pubkey).toString('hex')

    console.info(pubkey.length)
    return signAndSend('/SetOrganisationInfo', new AuthedMessage({
      pubkey,
      info
    }))
  }

  /**
   * Remove an organistaion from the database
   * 
   * @method Dashboard#RemoveOrganisation
   * @public
   * 
   * @param {PublicKey} pubkey
   * 
   * @returns {Boolean}
   */
  this.RemoveOrganisation = (pubkey) => {
    pubkey = PublicKey(pubkey).toString('hex')

    return signAndSend('/RemoveOrganisation', new AuthedMessage({
      pubkey,
    }))
  }

  /**
   * Generate an authenticated message which expires in 1 hour.
   * This is then sent to the supplied email via FMS.
   * 
   * @method Dashboard#InviteUser
   * @public
   * 
   * @param {String} email
   * @param {PublicKey} orgpub
   * @param {String} role
   * 
   * @returns {String} Invitation URL
   */
  this.InviteUser = (email, orgpub, role, expires) => {
    orgpub = PublicKey(orgpub).toString('hex')

    return signAndSend('/InviteUser', new AuthedMessage({
      email,
      organisation: orgpub,
      role,
    }, { expires }))
  }

  /**
   * @method Dashboard#AcceptInvite
   * @public
   * 
   * @param {String} name
   * @param {String} email
   * @param {Object} invite 
   * 
   * @returns {PublicKey} user pubkey
   */
  this.AcceptInvite = (name, email, invite) => {
    return signAndSend('/CreateUser', new AuthedMessage({ name, email, invite }))
  }

  /**
   * @method Dashboard#ListUsers
   * @public
   * 
   * @param {PublicKey} orgpub Organisation public key
   * 
   * @returns {UserListItem[]}
   */
  this.ListUsers = (orgpub) => {
    orgpub = PublicKey(orgpub).toString('hex')

    return signAndSend('/ListUsers', new AuthedMessage({
      organisation: orgpub
    }))
  }

  /**
   * Get user metadata
   * 
   * @method Dashboard#GetUserInfo
   * @public
   * 
   * @param {Buffer} pubkey User public key
   * 
   * @returns {UserInfo} User metadata
   */
  this.GetUserInfo = (pubkey) => {
    pubkey = PublicKey(pubkey).toString('hex')

    return signAndSend('/GetUserInfo', new AuthedMessage({
      pubkey,
    }))
  }

  /**
   * Set user metadata, setting property to null removes it.
   * 
   * @method Dashboard#SetUserInfo
   * @public
   * 
   * @param {Buffer} pubkey User public key
   * @param {Object} info User metadata modifications
   * 
   * @returns {Boolean}
   */
  this.SetUserInfo = (pubkey, info) => {
    pubkey = PublicKey(pubkey).toString('hex')

    return signAndSend('/SetUserInfo', new AuthedMessage({
      pubkey,
      info
    }))
  }

  /**
   * Remove a user from the database
   * 
   * @method Dashboard#RemoveUser
   * @public
   * 
   * @param {PublicKey} orgpub
   * @param {PublicKey} pubkey
   * 
   * @returns {Boolean}
   */
  this.RemoveUser = (orgpub, pubkey) => {
    orgpub = PublicKey(orgpub).toString('hex')
    pubkey = PublicKey(pubkey).toString('hex')

    return signAndSend('/RemoveUser', new AuthedMessage({
      organisation: orgpub,
      pubkey,
    }))
  }

  /**
   * List application members of an organisation.
   * 
   * @method Dashboard#ListApplications
   * @public
   * 
   * @param {PublicKey} orgpub Organisation public key
   * 
   * @returns {ApplicationListItem[]} Organisation user list
   */
  this.ListApplications = (orgpub) => {
    orgpub = PublicKey(orgpub).toString('hex')
    return signAndSend('/ListApplications', new AuthedMessage({
      organisation: orgpub,
    }))
  }

  /**
   * Create a new organisation application
   * 
   * @method Dashboard#CreateApplication
   * @public
   * 
   * @param {PublicKey} orgpub 
   * @param {String} name
   * @param {ApplicationInfo} info
   * 
   * @returns {PublicKey}
   */
  this.CreateApplication = (orgpub, name, info) => {
    orgpub = PublicKey(orgpub).toString('hex')
    return signAndSend('/CreateApplication', new AuthedMessage({
      name,
      orgpub: orgpub,
      info
    }))
  }

  /**
   * Get application metadata
   * 
   * @method Dashboard#GetApplicationInfo
   * @public
   * 
   * @param {PublicKey} pubkey
   * 
   * @returns {ApplicationInfo}
   */
  this.GetApplicationInfo = (pubkey) => {
    return signAndSend('/GetApplicationInfo', new AuthedMessage({
      pubkey: pubkey.toString('hex'),
    }))
  }

  /**
   * Set application metadata
   * 
   * @method Dashboard#SetApplicationInfo
   * @public
   * 
   * @param {PublicKey} pubkey 
   * @param {ApplicationInfo} info
   * 
   * @return {Boolean}
   */
  this.SetApplicationInfo = (pubkey, info) => {
    return signAndSend('/SetApplicationInfo', new AuthedMessage({
      pubkey: pubkey.toString('hex'),
      info
    }))
  }

  /**
   * Remove an application from the database
   * 
   * @method Dashboard#RemoveApplication
   * @public
   * 
   * @param {PublicKey} orgpub
   * @param {PublicKey} pubkey
   * 
   * @returns {Boolean}
   */
  this.RemoveApplication = (orgpub, pubkey) => {
    orgpub = PublicKey(orgpub).toString('hex')
    pubkey = PublicKey(pubkey).toString('hex')

    return signAndSend('/RemoveApplication', new AuthedMessage({
      organisation: orgpub,
      pubkey,
    }))
  }

  /**
   * @method Dashboard#CheckAllowed
   * @public
   * 
   * @param {PublicKey} pubkey 
   * @param {String} tag 
   * 
   * @returns {Boolean}
   */
  this.CheckAllowed = (pubkey, tag) => {
    return signAndSend('/CheckAllowed', new AuthedMessage({
      pubkey: pubkey.toString('hex'), tag: tag,
    }))
  }

  /**
   * @method Dashboard#AssignPermission
   * @public
   * 
   * @param {PublicKey} pubkey 
   * @param {String} tag 
   * 
   * @returns {Boolean}
   */
  this.AssignPermission = (pubkey, tag) => {
    return signAndSend('/AssignPermission', new AuthedMessage({
      pubkey: pubkey.toString('hex'), tag,
    }))
  }

  /**
   * @method Dashboard#RemovePermission
   * @public
   * 
   * @param {PublicKey} pubkey 
   * @param {String} tag 
   * 
   * @returns {Boolean}
   */
  this.RemovePermission = (pubkey, tag) => {
    return signAndSend('/RemovePermission', new AuthedMessage({
      pubkey: pubkey.toString('hex'), tag,
    }))
  }

  // Interface
  this.initWithKey = initWithKey.bind(this)
  this.initWithVault = initWithVault.bind(this)
}

module.exports = Dashboard