Building an integration

This tutorial will guide you through the basic steps of integrating a 3rd party system or api in Weavy. This tutorial will not focus on developing a fully functional ui, but rather how one could connect and authenticate with OAuth2 against the api.

This tutorial will use Asana as an example.

What are we going to build?

In the end of this tutorial we have created a simple Task App that integrates to Asana and displays an authenticated user´s task. We should also be able to mark the tasks for completion. Other basic stuff like creating new task and so on is up to you to enhance the app experience.

Before you begin, please make sure you have a Weavy site up and running and that you understand the fundamentals of Weavy

Create an app

First of all we need to create the actual App representing the Asana integration. Create a new file under the Models folder and call it AsanaApp.cs. Replace the code with the code snippet below:

using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using Weavy.Core.Models;

namespace Wvy.Models {

    [Serializable]
    [Guid("B309F192-9F22-4E39-AAFC-DD59589227C7")]
    [App(Icon = "application", Name = "Asana tasks", Description = "An app integration demo", AllowMultiple = true)]

    public class AsanaApp: App {

        [Required]
        [Display(Name = "Client Id", Description = "The client Id of the Asana developer app you have created")]
        public string ClientId { get; set; }
    }
}

A custom App in Weavy must derive from the App base class. An App must implement three Attributes, Serializable, Guid and App. The Guid must be unique per app you create. The App Attribute requires a Name, but in this example we will also set the and Description, Icon and AllowMultiple. If we set the AllowMultiple to ´true´, the end user of the app will be able to create multiple apps of this type in a Space.

So far we have created an AsanaApp. At this moment we can compile and make sure the app shows up in Weavy.

Hit F5 in Visual Studio and the head over to a Space in Weavy. Click on Add tab and voilà, there’s our Asana App ;)

Adding the app to the Space will do… absolutely nothing right now. Weavy will just render the app with the default Controller and View which is empty by default. Now we want to add our own Controller and View to render some interesting content.

The ClientId property above is not really necessary, but we want to show that when adding properties to an App that are required, the user adding the app to a space must enter these values before continuing.

Add a Controller and View

By adding a Controller that inherits from AppController<T> we can take full controll over the rendering flow of an App in Weavy. The Controller is just like any MVC Controller and we add the Actions we need.

Create a new class in the Controllers folder and name it AsanaAppController.cs. Make sure it Inherits from AppController<AsanaApp>. The RoutePrefix attribute must be added to controller and the route should be exact apps/{id:int}/[app_guid] where app_guid is the guid of the AsanaApp created earlier. The controller should now look something like this:

/// <summary>
/// Controller for the <see cref="AsanaApp" />.
/// </summary>
 [RoutePrefix("apps/{id:int}/B309F192-9F22-4E39-AAFC-DD59589227C7")]
public class AsanaAppController : AppController<AsanaApp> {
    
}

The AppController base class implements two virtual methods, Get and Edit. We are going to override the Get method in which we're going to return the View.

Override the Get method and add the following code to it:

public override ActionResult Get(AsanaApp app, Query query) {                        
    return View(app);
}

Now it’s time to create the View. Add a new View called Get.cshtml to the Views\AsanaApp folder. Create the folder if it doesn’t exists. Add the following code:

@model Wvy.Models.AsanaApp
<div class="container">
<h1>@Model.Name</h1>
<p>Hello from the Asana app integration!</p>
</div>

Hit F5 in Visual Studio and try out the app so far. You should now be able to add a Asana app to a Space and have it rendered through the Controller and View we just created.

Authenticate with OAuth2

Asana uses OAuth2 to authenticate the user that want’s to use their api to get tasks and other data. Before we can use the api we must register an Asana Application to get a client id and client secret. You can read more about this here. Go ahead and complete the steps for registering an Asana Application before you continue this tutorial.

When registering an Asana Application, Asana asks for a redirect uri. Enter the following uri for our example https://yourhost/apps/B309F192-9F22-4E39-AAFC-DD59589227C7/auth where yourhost is replaced with the url you currently are using to host Weavy.

Now we need a way to trigger the authentication. We will just use a plain button to popup the Asana auth window. Add the following code the the Get view:

<div class="container my-4 asana_app">
<h3>Asana tasks</h3>
<button id="btn-auth" class="btn btn-primary">Authenticate</button>
</div>

<script>
    $(function () {
        var clientId = @Model.ClientId;

        $(document).on("click", "#btn-auth", function () {
            window.open(
                "https://app.asana.com/-/oauth_authorize?response_type=token&client_id=" + clientId + "&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2Fapps%2FB309F192-9F22-4E39-AAFC-DD59589227C7%2Fauth&state=somerandomstate",
                "authWindow",
                "width=500px,height=700px"
            );
        });
    });
</script>
The url that we open up above can be found when registering the Asana Application under Authentication Endpoint -> Implicit Grant. The we have replaced the client ID with our variable to allow for multiple Asana tasks apps. As stated in Create an app, the ClientID is not really necessary for our Weavy app, but shows a good example on how to use App properties.

Run the app and note that when we click on the Authentication button, an Asana sign in window is displayes asking for your credentials. After clickning Log In, the window will redirect to the Redirect uri we specified when registering the Asana Application. Since we havn’t done anything with that yet, now is the time

Create the redirect uri action

Go back to the AsanaAppController.cs file and add the following action:

/// <summary>
/// Handle OAuth2 response url
/// </summary>
/// <returns></returns>
[Route("~/apps/B309F192-9F22-4E39-AAFC-DD59589227C7/auth")]
public ActionResult Auth() {
    return View();
}

This action will take care of the OAuth2 redirect uri and display a view that we are going to create soon. Note that the Route attribute overrides the default RoutePrefix and doesn’t include the {id:int} parameter. We want to keep the redirect uri independant of wich specific Weavy App the user requested the authenticated from.

Create a new View under Views\AsanaApp and name it Auth.cshtml. The view will do nothing else than inform the parent (window.opener) window that the authentication was successfull. The token returned from Asana is in the url fragment and called access_token. Feel free to parse the hash params any way you like. Below is a small javasript snippet doing exactly that.

<script>
    $(function () {
        function getHashParams() {
            var hashParams = {};
            var e,
                a = /\+/g,  // Regex for replacing addition symbol with a space
                r = /([^&;=]+)=?([^&;]*)/g,
                d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
                q = window.location.hash.substring(1);

            while (e = r.exec(q)) {
                hashParams[d(e[1])] = d(e[2]);
            }

            return hashParams;
        }

        // get the credentials
        var credentials = getHashParams();

        // inform app that the user was authenticated
        window.opener.postMessage({ name: 'auth', token: credentials.access_token }, "*");

    });
</script>

Add the code above to the View. We use postMessage to post a message to the parent window.

Go back to the Get.cshtml and replace the script tag with the following code:

$(function () {
    var clientId = @Model.ClientId;
    var authWin = null;
    var token = null;

    $(document).on("click", "#btn-auth", function () {
        authWin = window.open(
            "https://app.asana.com/-/oauth_authorize?response_type=token&client_id=" + clientId + "&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2Fapps%2FB309F192-9F22-4E39-AAFC-DD59589227C7%2Fauth&state=somerandomstate",
            "authWindow",
            "width=500px,height=700px"
        );
    });


    window.addEventListener("message", function (e) {
        switch (e.data.name) {
            case "auth":
                token = e.data.token;

                if (authWin) {
                    authWin.close();
                }
                break;
        }
    });
});

The code above listens for a postMessage with the name auth and then gets the token from the data passed.

Now that we are authenticated, we can go get some tasks using the Asana Rest Api.

Using the API

Add the following code to the Get.cshtml script tag:

$(function () {
    var asanaBaseUrl = "https://app.asana.com/api/1.0";
    var clientId = @Model.ClientId;
    var authWin = null;
    var token = null;
    var me = null;

    $(document).on("click", "#btn-auth", function () {
        authWin = window.open(
            "https://app.asana.com/-/oauth_authorize?response_type=token&client_id=" + clientId + "&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2Fapps%2FB309F192-9F22-4E39-AAFC-DD59589227C7%2Fauth&state=somerandomstate",
            "authWindow",
            "width=500px,height=700px"
        );
    });

    var getData = function () {
        // get the user info and then all the tasks                    
        $.ajax({
            url: asanaBaseUrl + "/users/me/",
            contentType: "application/json",
            method: "GET",
            beforeSend: function (xhr) {
                xhr.setRequestHeader('Authorization', 'Bearer ' + token);
            }
        }).then(function (me) {
            me = me.data;

            return $.ajax({
                url: asanaBaseUrl + "/tasks?workspace=" + me.workspaces[0].id + "&assignee=" + me.id + "&opt_fields=id,name,assignee_status,completed",
                contentType: "application/json",
                method: "GET",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader('Authorization', 'Bearer ' + token);
                }
            });
        }).then(function (response) {
            tasks = _.map(response.data, function (t) {
                return { id: t.id, name: t.name, is_section: t.name.endsWith(":"), completed: t.completed };
            });
        }).fail(function () {
            console.error("Handle error...")
        });
    }

    window.addEventListener("message", function (e) {
        switch (e.data.name) {
            case "auth":
                token = e.data.token;
                getData();
                if (authWin) {
                    authWin.close();
                }
                break;
        }
    });
});

The code above demostrating the authentication flow and getting the tasks of the logged in user.

Next steps

The next steps would be to add more API endpoints to handle CRUD, ToggleComplete and so on. A fully functional example is available here which uses vue.js to handle the client side functionality.