Connect to Box Explorer Component using Ruby on Rails

Hello Everyone,

The target audience of this post is limited.

In this article of mine, I will write down the steps of how you can connect with Box Explorer Component to allow users to upload file into your Box account.

Requirement: We want to allow our users to upload file to somewhere in the cloud where we (me and my team) can see the files and take further actions on it

There are many solutions to this: You have AWS S3, DropBox, Google Cloud and lots more but if you do not want to spend much time on designing and engineering the front-end (by designing I mean building the entire UI for file management), you can use Box.com’s easy to integrate Box Explorer Component.

Related links:

  1. https://developer.box.com/guides/embed/ui-elements/explorer/
  2. https://app.box.com/developers/console
  3. https://app.box.com/master
  4. https://github.com/box-community/samples-docs-authenticate-with-jwt-api/blob/master/sample.rb

Prerequisites:

  1. Ruby >= 2
  2. Rails >= 5
  3. A Box.com account
  4. A Box.com App – You can create the same from Developer’s console (see Related links#2)
  5. Make sure your app is authorised. You can do the same from here: https://app.box.com/master/custom-apps
  6. A config.json file that you can download from: https://app.box.com/developers/console/app/:app_id/configuration. You’ll have to generate Public/Private keypair for this – You can do the same from the same page.
  7. You’ll have to add your domain to allowed origins via above link.

Sample Output / Demo: https://codepen.io/box-platform/pen/wdWWdN. In addition you’ll also see options to add / upload file / folder and perform operations like Search for uploads, Download, Share, Delete etc.

Once you are done setting up above things, let’s code this:

# lib/box_api.rb
require 'json'
require "openssl"
require 'securerandom'
require 'jwt'
require 'json'
require 'uri'
require 'net/https'

class BoxApi
  def initialize unique_identifier = nil
    @unique_identifier  = unique_identifier
    @access_token       = Rails.cache.fetch("users/#{@unique_identifier}/box/access_token")
  end

  def get_token
    return @access_token unless @access_token.nil?

    set_auth
    # Make the request
    uri = URI.parse(@authentication_url)
    http = Net::HTTP.start(uri.host, uri.port, use_ssl: true)
    request = Net::HTTP::Post.new(uri.request_uri)
    request.body = @params
    response = http.request(request)

    # Parse the JSON and extract the access token
    @access_token = Rails.cache.fetch("users/#{@unique_identifier}/box/access_token", expires_in: 60.minutes) do
      JSON.parse(response.body)['access_token']
    end
    return @access_token
  end

  def find_or_create_folder_by_folder_name folder_name = @unique_identifier
    root_folder     = find_folder "Client Uploads"
    root_folder     = create_folder "Client Uploads" if !root_folder
    child_folder    = find_folder folder_name, root_folder["id"]
    child_folder    = create_folder folder_name, root_folder["id"] if !child_folder

    return child_folder["id"]
  end

  def find_folder folder_name, parent_folder_id = nil
    base_url = "https://api.box.com/2.0/search?query='#{folder_name}'&type=folder"
    base_url += "&ancestor_folder_ids=#{parent_folder_id}" if parent_folder_id

    uri = URI.parse(base_url)
    request = Net::HTTP::Get.new(uri)
    request["Authorization"] = "Bearer #{@access_token}"

    req_options = {
      use_ssl: uri.scheme == "https",
    }

    response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
      http.request(request)
    end

    response_body = JSON.parse(response.body)

    return false if response_body["total_count"] == 0 && response_body["entries"].empty?
    return response_body["entries"][0]
  end

  def create_folder folder_name, parent_folder_id = nil
    uri = URI.parse("https://api.box.com/2.0/folders")
    request = Net::HTTP::Post.new(uri)
    request.content_type = "application/json"
    request["Authorization"] = "Bearer #{@access_token}"

    params = {}
    params["name"] = folder_name
    params["parent"] = {
      "id" => parent_folder_id ? parent_folder_id : "0"
    }

    request.body = JSON.dump(params)

    req_options = {
      use_ssl: uri.scheme == "https",
    }

    response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
      http.request(request)
    end

    response_body = JSON.parse(response.body)
    return response_body if response.code == "201"
  end

private
  def set_auth
    @config = JSON.parse(
      File.read("#{Rails.root.join}/config/box_config.json")
    )
    @config = Rails.env.production? ? @config["production"] : @config["base"]
    @appAuth = @config['boxAppSettings']['appAuth']
    key = OpenSSL::PKey::RSA.new(
      @appAuth['privateKey'],
      @appAuth['passphrase']
    )
    @authentication_url = 'https://api.box.com/oauth2/token'
    claims = {
      iss: @config['boxAppSettings']['clientID'],
      sub: @config['enterpriseID'],
      box_sub_type: 'enterprise',
      aud: @authentication_url,
      # This is an identifier that helps protect against
      # replay attacks
      jti: SecureRandom.hex(64),
      # We give the assertion a lifetime of 45 seconds
      # before it expires
      exp: Time.now.to_i + 45
    }
    keyId = @appAuth['publicKeyID']

    # Rather than constructing the JWT assertion manually, we are
    # using the pyjwt library.
    # The API support "RS256", "RS384", and "RS512" encryption
    assertion = JWT.encode(claims, key, 'RS512', { kid: keyId })

    # We are using the excellent axios package
    # to simplify the API call
    @params = URI.encode_www_form({
      # This specifies that we are using a JWT assertion
      # to authenticate
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      # Our JWT assertion
      assertion: assertion,
      # The OAuth 2 client ID and secret
      client_id: @config['boxAppSettings']['clientID'],
      client_secret: @config['boxAppSettings']['clientSecret']
    })
  end
end

Let’s call this api via controller’s method: `/app/controllers/home_controller.rb`

# /app/controllers/home_controller.rb
class HomeController < ApplicationController
  def connect_to_box
    api        = BoxApi.new(user.company.name)
    token      = api.get_token
    folder_id  = api.find_or_create_folder_by_folder_name

    respond_to do | wants |
      wants.json do
        render json: {
          success: true,
          access_token: token,
          folder_id: folder_id
        }
      end
    end
  end
end

and finally let’s load token and folder_id via Ajax call from view

/app/views/home/files.html.erb

<%= content_for :page_css do %>
  <link rel="stylesheet" href="https://cdn01.boxcdn.net/platform/elements/13.0.0/en-US/explorer.css" />
<% end %>

<%= content_for :page_js do %>
  <!-- polyfill.io only loads the polyfills your browser needs -->
  https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Intl
  https://cdn01.boxcdn.net/platform/elements/13.0.0/en-US/explorer.js
<% end %>

<div class=<%= "#{controller_name}_#{action_name}" %>>
  <div id="uploader-panel" class="box-light-blue">
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <div class="text-center result-overlay">
            <%= image_tag 'pageloader.svg' %>
          </div>
          <div class="box-list-container" style="width: 100%; height: 100%; margin: 0; padding: 0; min-width: 320px;"></div>
        </div>
      </div>
    </div>
  </div>
</div>

<script type="text/javascript">
  $(document).ready(function() {
    //connecting to box to fetch required token and folder
    $.ajax({
      url: '/home/connect_to_box',
      type: 'POST',
      dataType: 'json',
      success: function(response) {
        if(response['success'] === true){
          $("div.result-overlay").hide();
          const folderId = response['folder_id'];
          const accessToken = response['access_token'];
          const logoUrl = "<%= asset_path('logo-blue.jpg') %>";
          const contentExplorer = new Box.ContentExplorer();

          // for more options visit: https://developer.box.com/guides/embed/ui-elements/uploader/#options
          contentExplorer.show(folderId, accessToken, {
            container: ".box-list-container",
            canDownload: true,
            canUpload: true,
            canCreateNewFolder: true,
            sortBy: "date",
            sortDirection: "DESC",
            logoUrl: logoUrl,
            onClose: null
          });

          contentExplorer.on('upload', (data) => {
            //notify via slack
            $.ajax({
              url: '/home/upload_notify',
              type: 'POST',
              beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
              dataType: "json",
              data: {files: JSON.stringify(data)}
            });
          });

          contentExplorer.on('error', (data) => {
            alert(`Error uploading file with name "${data.file.name}". The error was: "${data.error}"`);
          });
        }
      }
    })
  });
</script>

Here’s what’s happening:

  1. When the page loads we are firing Ajax call to get access_token and folder_id from Box. folder_id is the ID of the folder where the user will upload all files. We are giving different folder for each user who belongs to different company (see table below).
  2. The call to Box API checks if the access token is present for the company, it will return that else it will connect to Box, get the access token, set the cache and return the token – Important thing to note that the token returned from Box is valid for 60 minutes
  3. Based on the returned token, we will check if the folder is present for the company’s user or not!
  4. The last part of the Ajax call I have written will send the slack notification.
UserCompany NameFolder on Box
User-AABCABC
User-BXYZXYZ
User-CABCABC
User-DA123A123

That’s it. Hook it up and see it in action.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s