5 Mar 2018

Forge Webhooks Admin UI Sample

    After Stephen's post introducing the official release of the Forge Webhooks API in production: Welcome to the Wonderful World of Webhooks, it is now time to unveil my own sample. It is a simple admin UI console that lets you create, list, delete webhooks and also observe callbacks issued by Forge.

    Let's take a look first at how to use it and I will expose few highlights of the code later:

    The live version is available here as part of a sample integrated in my bigger project Forge RCDB. Below is what you can see when loading that page, it will first prompts you to log in using your A360 account as webhooks are now exposed for DataManagement API which requires 3-legged authentication:

webhooks sample

    There is just one choice for system at the moment: "Data Management" as it is the only API exposing Webhooks at the time of this writing. Once you picked up the system, you can use the second dropdown to select the event you are interested in, let's use the first one "File Version Added".

    Before clicking Create Hook, you need to fill up the payload on the right with a valid folder Id. In order to do that you can use the DataManagement API to access that information, alternatively you can make use of another demo I have on the same website: this allows you to browse through your hubs and folders. Once you identified the folder you want to attach the webhook to, right-click and select "Folder details":

 DM demo folder details

    This will open a new tab that contains a payload where you can easily get the folder Id. Tip: use a json formatter extension for your browser, such as JSONView to make it more human readable:

Folder id

Copy that in the webhook sample and you should be good to go to create a valid hook:

Webhook creation success

    Go to the second tab "Manage Hooks" where you can find a list of your existing hooks. You can also delete a hook from there:

Manage hooks

    Finally let's test our hook, so go to your A360 account and upload a new version of a file you have in the folder you set the hook on

A360 account

    You should see the notification appears few seconds later in the "Event Log" tab:

Webhook event log

    

    Let's now take a look at the code. Webhooks are not yet part of the Forge SDK, so you will have to do a bit of custom implementation if you want to use them now. I created the following node.js wrapper that may be useful if you are using a node server or simply as an example for any other server-side language. Find the full implementation there: ForgeSvc.js

export default class ForgeSvc extends BaseSvc {

  /////////////////////////////////////////////////////////
  //
  //
  /////////////////////////////////////////////////////////
  static BASE_HOOKS_URL =
    'https://developer.api.autodesk.com/webhooks/v1'

 
  /////////////////////////////////////////////////////////
  //
  //
  /////////////////////////////////////////////////////////
  name () {

    return 'ForgeSvc'
  }

  /////////////////////////////////////////////////////////
  // REST request wrapper
  //
  /////////////////////////////////////////////////////////
  requestAsync(params) {

    return new Promise( function(resolve, reject) {

      request({

        url: params.url,
        method: params.method || 'GET',
        headers: params.headers || {
          'Authorization': 'Bearer ' + params.token
        },
        json: params.json,
        body: params.body

      }, function (err, response, body) {

        try {

          if (err) {

            return reject(err)
          }

          if (body && body.errors) {

            return reject(body.errors)
          }

          if (response && [200, 201, 202, 204].indexOf(
              response.statusCode) < 0) {

            return reject(response.statusMessage)
          }

          return resolve(body ? (body.data || body) : {})

        } catch (ex) {

          return reject(ex)
        }
      })
    })
  }

  /////////////////////////////////////////////////////////
  // GET systems/:system_id/events/:event_id/hooks/:hook_id
  //
  /////////////////////////////////////////////////////////
  getHook (token, systemId, eventId, hookId) {

    const url =
      `${ForgeSvc.BASE_HOOKS_URL}/systems/` +
      `${systemId}/events/${eventId}/hooks/${hookId}`

    return this.requestAsync({
      token: token.access_token,
      json: true,
      url
    })
  }

  /////////////////////////////////////////////////////////
  // GET systems/:system_id/hooks
  //
  /////////////////////////////////////////////////////////
  getSystemHooks (token, systemId) {

    const url =
      `${ForgeSvc.BASE_HOOKS_URL}/systems/${systemId}/hooks`

    return this.requestAsync({
      token: token.access_token,
      json: true,
      url
    })
  }

  /////////////////////////////////////////////////////////
  // GET systems/:system_id/events/:event_id/hooks
  //
  /////////////////////////////////////////////////////////
  getEventHooks (token, systemId, eventId) {

    const url =
      `${ForgeSvc.BASE_HOOKS_URL}/systems/` +
      `${systemId}/events/${eventId}/hooks`

    return this.requestAsync({
      token: token.access_token,
      json: true,
      url
    })
  }

  /////////////////////////////////////////////////////////
  // GET hooks
  //
  /////////////////////////////////////////////////////////
  getHooks (token) {

    const url = `${ForgeSvc.BASE_HOOKS_URL}/hooks`

    return this.requestAsync({
      token: token.access_token,
      json: true,
      url
    })
  }

  /////////////////////////////////////////////////////////
  // POST systems/:system_id/events/:event_id/hooks
  //
  /////////////////////////////////////////////////////////
  createEventHook (token, systemId, eventId, params) {

    const url =
      `${ForgeSvc.BASE_HOOKS_URL}/systems/` +
      `${systemId}/events/${eventId}/hooks`

    const body = Object.assign({}, params, {
      callbackUrl: this._config.hooks.callbackUrl
    })

    return this.requestAsync({
      token: token.access_token,
      method: 'POST',
      json: true,
      body,
      url
    })
  }

  /////////////////////////////////////////////////////////
  // POST systems/:system_id/hooks
  //
  /////////////////////////////////////////////////////////
  createSystemHook (token, systemId, params) {

    const url =
      `${ForgeSvc.BASE_HOOKS_URL}/systems/` +
      `${systemId}/hooks`

    const body = Object.assign({}, params, {
      callbackUrl: this._config.hooks.callbackUrl
    })

    return this.requestAsync({
      token: token.access_token,
      method: 'POST',
      json: true,
      body,
      url
    })
  }

  /////////////////////////////////////////////////////////
  // DELETE systems/:system_id/events/:event_id/hooks/:hook_id
  //
  /////////////////////////////////////////////////////////
  removeHook (token, systemId, eventId, hookId) {

    const url =
      `${ForgeSvc.BASE_HOOKS_URL}/systems/` +
      `${systemId}/events/${eventId}/hooks/${hookId}`

    return this.requestAsync({
      token: token.access_token,
      method: 'DELETE',
      url
    })
  }
}

    My server also exposes a specific set of endpoints in order to allow my client application to perform the required actions on the webhooks. However this is a bit different than what you may want to do with webhooks as all the logic is usually kept strictkly server-side.


import ServiceManager from '../services/SvcManager'
import compression from 'compression'
import express from 'express'

module.exports = function () {

  /////////////////////////////////////////////////////////
  //
  //
  /////////////////////////////////////////////////////////
  const router = express.Router()

  const shouldCompress = (req, res) => {
    return true
  }

  router.use(compression({
    filter: shouldCompress
  }))

  /////////////////////////////////////////////////////////
  // GET /hooks
  // Get All Hooks
  //
  /////////////////////////////////////////////////////////
  router.get('/', async (req, res) => {

    try {

      const forgeSvc =
        ServiceManager.getService(
          'ForgeSvc')

      const token =
        await forgeSvc.get3LeggedTokenMaster(
          req.session)

      const response = await forgeSvc.getHooks(token)

      res.json(response)

    } catch (ex) {

      res.status(ex.statusCode || 500)
      res.json(ex)
    }
  })

  /////////////////////////////////////////////////////////
  // GET /hooks/systems/systemId
  // Get System Hooks
  //
  /////////////////////////////////////////////////////////
  router.get('/systems/:systemId', async (req, res) => {

    try {

      const { systemId } = req.params

      const forgeSvc =
        ServiceManager.getService(
          'ForgeSvc')

      const token =
        await forgeSvc.get3LeggedTokenMaster(
          req.session)

      const response =
        await forgeSvc.getSystemHooks(
          token, systemId)

      res.json(response)

    } catch (ex) {

      res.status(ex.statusCode || 500)
      res.json(ex)
    }
  })

  /////////////////////////////////////////////////////////
  // GET /hooks/systems/systemId/events/eventId
  // Get Event Hooks
  //
  /////////////////////////////////////////////////////////
  router.get('/systems/:systemId/events/:eventId', async (req, res) => {

    try {

      const { systemId, eventId } = req.params

      const forgeSvc =
        ServiceManager.getService(
          'ForgeSvc')

      const token =
        await forgeSvc.get3LeggedTokenMaster(
          req.session)

      const response =
        await forgeSvc.getEventHooks(
          token, systemId, eventId)

      res.json(response)

    } catch (ex) {

      res.status(ex.statusCode || 500)
      res.json(ex)
    }
  })

  /////////////////////////////////////////////////////////
  // POST /hooks/systems/:systemId
  // Create System Hook
  //
  /////////////////////////////////////////////////////////
  router.post('/systems/:systemId', async (req, res) => {

    try {

      const { systemId } = req.params

      const params = req.body

      const forgeSvc =
        ServiceManager.getService(
          'ForgeSvc')

      const token =
        await forgeSvc.get3LeggedTokenMaster(
          req.session)

      const response =
        await forgeSvc.createSystemHook(
          token, systemId, params)

      res.json(response)

    } catch (ex) {

      res.status(ex.statusCode || 500)
      res.json(ex)
    }
  })

  /////////////////////////////////////////////////////////
  // POST /hooks/systems/:systemId/events/:eventId
  // Create Event Hook
  //
  /////////////////////////////////////////////////////////
  router.post('/systems/:systemId/events/:eventId',
    async (req, res) => {

    try {

      const { systemId, eventId } = req.params

      const params = req.body

      const forgeSvc =
        ServiceManager.getService(
          'ForgeSvc')

      const token =
        await forgeSvc.get3LeggedTokenMaster(
          req.session)

      const response =
        await forgeSvc.createEventHook(
          token, systemId, eventId, params)

      res.json(response)

    } catch (ex) {

      res.status(ex.statusCode || 500)
      res.json(ex)
    }
  })

  /////////////////////////////////////////////////////////
  // DELETE /hooks/systems/:systemId/events/:eventId/:hookId
  // Delete Hook
  //
  /////////////////////////////////////////////////////////
  router.delete('/systems/:systemId/events/:eventId/:hookId',
    async (req, res) => {

    try {

      const { systemId, eventId, hookId } = req.params

      const forgeSvc =
        ServiceManager.getService(
          'ForgeSvc')

      const token =
        await forgeSvc.get3LeggedTokenMaster(
          req.session)

      const response =
        await forgeSvc.removeHook(
          token, systemId, eventId, hookId)

      res.json(response)

    } catch (ex) {

      res.status(ex.statusCode || 500)
      res.json(ex)
    }
  })

  return router
}

    Finally another endpoint on my server is responsible for listening to the actual webhooks notifications sent by the Forge API. That endpoint will dispatch the payload to the correct logged in user based on the field hook.createdBy which contains the Forge userId of the user who created the hook:

  router.post('/callback/hooks', async (req, res) => {

    const socketSvc = ServiceManager.getService(
      'SocketSvc')

    const userId = req.body.hook.createdBy

    socketSvc.broadcastToUser(
      userId, 'forge.hook', req.body)

    res.status(200).end()
  })

    The rest of the demo is purely client-side, so it is very specific to my custom implementation. You can find the complete code at Viewing.Extension.WebHooks

    Another standalone sample from Augusto is exposed in that article: Webhooks for Data Management API - NodeJS sample

 

Related Article